diff --git a/.gitignore b/.gitignore index 02eaabaf4e56d2dc411199f3aeba6045faa5767a..b6fc8ae02d3cde0017b1e32d6b43a7e2b88d6482 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # dependencies /node_modules +/frontend/node_modules /.pnp .pnp.js @@ -13,6 +14,7 @@ # local environment /frontend/local.env +/.vscode # misc .DS_Store @@ -25,6 +27,9 @@ yarn-error.log* .eslintcache cypress/videos/ +# KUBECONFIG values +backend/kubeconfig/* + # Helm dependencies deployment/helmchart/charts/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/README.md b/README.md index 3d6bd755527cfee84d3fe9921dc123644fd2fe73..153595f5a6d1da09257264e0a29ee27c8e1759ee 100644 --- a/README.md +++ b/README.md @@ -17,25 +17,25 @@ identity provider, login, consent and logout endpoints for the OpenID Connect The application relies on the following components: - - **Hydra**: Hydra is an open source OIDC server. - It means applications can connect to Hydra to start a session with a user. - Hydra provides the application with the username - and other roles/claims for the application. - Hydra is developed by Ory and has security as one of their top priorities. - - - **Kratos**: This is Identity Manager - and contains all the user profiles and secrets (passwords). - Kratos is designed to work mostly between UI (browser) and kratos directly, - over a public API endpoint. - Authentication, form-validation, etc. are all handled by Kratos. - Kratos only provides an API and not UI itself. - Kratos provides an admin API as well, - which is only used from the server-side flask app to create/delete users. - - - **MariaDB**: The login application, as well as Hydra and Kratos, need to store data. - This is done in a MariaDB database server. - There is one instance with three databases. - As all databases are very small we do not foresee resource limitation problems. +- **Hydra**: Hydra is an open source OIDC server. + It means applications can connect to Hydra to start a session with a user. + Hydra provides the application with the username + and other roles/claims for the application. + Hydra is developed by Ory and has security as one of their top priorities. + +- **Kratos**: This is Identity Manager + and contains all the user profiles and secrets (passwords). + Kratos is designed to work mostly between UI (browser) and kratos directly, + over a public API endpoint. + Authentication, form-validation, etc. are all handled by Kratos. + Kratos only provides an API and not UI itself. + Kratos provides an admin API as well, + which is only used from the server-side flask app to create/delete users. + +- **MariaDB**: The login application, as well as Hydra and Kratos, need to store data. + This is done in a MariaDB database server. + There is one instance with three databases. + As all databases are very small we do not foresee resource limitation problems. If Hydra hits a new session/user, it has to know if this user has access. To do so, the user has to login through a login application. @@ -67,10 +67,11 @@ These need to be available locally, because Kratos wants to run on the same domain as the front-end that serves the login interface. ### Setup + Before you start, make sure your machine has the required software installed, as per official documentation: https://docs.stackspin.net/en/v2/installation/install_cli.html#preparing-the-provisioning-machine. Please read through all subsections to set up your environment before -attempting to run the dashboard locally. +attempting to run the dashboard locally. #### 1. Stackspin cluster @@ -102,25 +103,49 @@ The application will run on `http://stackspin_proxy`. Add the following line to #### 4. Kubernetes access -The script needs you to have access to the Kubernetes cluster that runs -Stackspin. Point the `KUBECONFIG` environment variable to a kubectl config. -Attention points: +The `./run_app.sh` script needs to access the Kubernetes cluster that runs your Stackspin instance. If you followed the setup as above, you will have a YAML configuration file somewhere on your machine -- usually in the `clusters` directory of your Stackspin local repository -- called `kube_config_cluster.yml`. This file holds all the configuration information (URLs, domain names, certificate data) needed to connect to the instance. + +Copy that file into the `backend/kubeconfig` directory. + +If you wish to connect this dashboard to another Stackspin cluster, you can replace the `kube_config_cluster.yml` file with the one that's in that Stackspin's `clusters` directory. + +## 5. Build and run + +To recap, you now have: -* The kubeconfig will be mounted inside docker containers, so also make sure - your Docker user can read it. -* The bind-mount done by docker might not work if the file pointed to is - part of a filesystem such as sshfs. In that case, copy the file to a local - drive first. +- All the software and configurations as described above +- A running Stackspin cluster (a VPS somewhere in The Cloud) +- A `kube_config_cluster.yml` file in the `backend/kubeconfig` that will tell the script how to connect to your Stackspin cluster of choice +- Overrides for local dashboard development (by installing and running the [Dashboard Dev Overrides](https://open.greenhost.net/stackspin/dashboard-dev-overrides) repository, editing your `/etc/hosts` file, etc) +- A copy of the [Stackspin Dashboard repository](https://open.greenhost.net/stackspin/dashboard) on your device. -### Build and run +That's a lot of work! Good job. -After you've finished all setup steps, you can run everything using +### Setup your local dev environment - ./run_app.sh +Before you actually run the main script, `cd` into the `/frontend` directory and run`yarn install`. -This sets a few environment variables based on what is in your cluster -secrets, and run `docker compose up` to build and run all necessary components, -including a reverse proxy and the backend flask application. +This is not strictly necessary for development; the script already builds and installs all the necessary modules in the dashboard's docker container. But running `yarn install` locally will let your IDE enable all of its bells and whistles like linting, autocorrecting, intellisense etc. Without this step, your IDE will most probably complain it cannot find any modules to `import`, as there is no `node_modules` folder. + +### Let's Run this App + +After you've finished all setup steps, you can run everything using: + +``` +./run_app.sh +``` + +This script + +- sets a few environment variables based on the content in your cluster + secrets, and +- runs `docker compose up` to build and run all necessary components, including a reverse proxy and the backend flask application. + +If you're curious about what `docker compose up` does, you can check out the `docker-compose.yml` file. If you are curious about what `docker compose up` _means,_ you can start here: https://github.com/docker/compose or even here: https://en.wikipedia.org/wiki/Infrastructure_as_code. + +This should be it, congratulations!! If you're having issues, or if something is not working properly, please open an issue or get in touch: info@stackspin.net + +--- ## Testing as a part of Stackspin @@ -136,6 +161,7 @@ version in the Gitlab helm chart repo for the dashboard project, but in the have been merged to the `main` branch. Once your package is published, use it by + 1. changing the `spec.url` field of the `flux-system/dashboard` `HelmRepository` object in the cluster where you want to run this, replacing `stable` by `unstable`; and @@ -145,6 +171,7 @@ Once your package is published, use it by ## Release process To publish a new version of the helm chart: + 1. Increase the docker image tag in `deployment/helmchart/values.yaml` so it uses the new tag (to be created in a later step of this release). 2. Update the appVersion in `deployment/helmchart/Chart.yaml` to match that new tag version. diff --git a/backend/areas/__init__.py b/backend/areas/__init__.py index b90dfee2fde7711293a5e0a3394548182bcab958..2d449283aaad008d12438736faaa3c18b6fd420d 100644 --- a/backend/areas/__init__.py +++ b/backend/areas/__init__.py @@ -1,6 +1,9 @@ from flask import Blueprint, jsonify +import yaml from config import * +import helpers.kubernetes as k8s +import requests api_v1 = Blueprint("api_v1", __name__, url_prefix="/api/v1") @@ -17,3 +20,71 @@ def api_environment(): "KRATOS_PUBLIC_URL": KRATOS_PUBLIC_URL, } return jsonify(environment) + +# We want to know if +# 1. A release has happened recently and is already deployed on this cluster. +# 2. A release has happened recently but has not yet been deployed on this +# cluster -- that will then probably happen automatically during the next +# maintenance window. +# +# To get the last release, we get the contents of the `VERSION` file from +# the main branch. The `VERSION` file is only updated as part of the release +# process. +# +# To find out how long ago the currently running version was deployed, we look +# at the `lastUpdateTime` of the stackspin `GitRepo` object on the cluster. +@api_v1.route("/info") +def api_info(): + # Get static info from configmap on cluster. + static_info = k8s.get_kubernetes_config_map_data( + "stackspin-static-info", + "flux-system", + ) + results = static_info + + # Get app versions from apps configmaps on cluster. + results['appVersions'] = {} + apps = k8s.get_kubernetes_config_map_data( + "stackspin-apps", + "flux-system", + ) + for app, app_data in apps.items(): + data = yaml.safe_load(app_data) + if 'version' in data: + results['appVersions'][app] = data['version'] + apps_custom = k8s.get_kubernetes_config_map_data( + "stackspin-apps-custom", + "flux-system", + ) + if apps_custom is not None: + for app, app_data in apps_custom.items(): + data = yaml.safe_load(app_data) + if 'version' in data: + results['appVersions'][app] = data['version'] + + # Get last update time of stackspin GitRepo object on the cluster; that + # tells us when flux last updated the cluster based on changes in the + # stackspin git repo. + stackspin_repo = k8s.get_gitrepo('stackspin') + results['lastUpdated'] = stackspin_repo['status']['artifact']['lastUpdateTime'] + + # This is the branch (or other git ref, like tag or commit) that this + # cluster follows. + flux_ref = stackspin_repo['spec']['ref'] + # `flux_ref` is a structured object, though as far as we've seen always a + # dict with a single entry. The key can be `branch` or `tag` or `commit`. + # We reduce this to a single string git ref for simplicity in the + # front-end. + ref = next(iter(flux_ref.values())) + results['followingGit'] = ref + + # Get latest released version from gitlab. Whether it's considered + # "released" depends on which branch we're following, but usually that's + # the `vX` "production" branch. + git_release = 'Unknown' + result = requests.get(f"https://open.greenhost.net/stackspin/stackspin/-/raw/{ref}/VERSION", timeout=5) + if result.status_code == 200: + git_release = result.text.rstrip() + results['lastRelease'] = git_release + + return jsonify(results) diff --git a/backend/helpers/kubernetes.py b/backend/helpers/kubernetes.py index cba600effd6e7d0b71fd2361c0d955eabccd8a3a..1d54caaa60a2efdab2b13793dc36985680bebaf5 100644 --- a/backend/helpers/kubernetes.py +++ b/backend/helpers/kubernetes.py @@ -382,3 +382,30 @@ def get_kustomization(name, namespace='flux-system'): # Raise all non-404 errors raise error return resource + +def get_gitrepo(name, namespace='flux-system'): + """ + Returns all info on a Flux GitRepo. + + :param name: Name of the gitrepo + :type name: string + :param namespace: Namespace of the gitrepo + :type namespace: string + :return: gitrepo as returned by the API + :rtype: dict + """ + api = client.CustomObjectsApi() + try: + resource = api.get_namespaced_custom_object( + group="source.toolkit.fluxcd.io", + version="v1beta2", + name=name, + namespace=namespace, + plural="gitrepositories", + ) + except client.exceptions.ApiException as error: + if error.status == 404: + return None + # Raise all non-404 errors + raise error + return resource diff --git a/docker-compose.yml b/docker-compose.yml index c4e7030c189cacb1441fbe9168430e51b45de7db..be2676feaec1edbdef067e114f7765219092e052 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,9 +6,10 @@ services: env_file: ./frontend/local.env volumes: - ./frontend/src:/home/node/app/src + - ./frontend/public:/home/node/app/public ports: - "3000:3000" - command: "yarn run start" + command: "yarn start --watch --verbose" stackspin_proxy: image: nginx:1.25.1 ports: @@ -62,7 +63,12 @@ services: - 8000 volumes: - "$KUBECONFIG:/.kube/config" - entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address $$(hostname -i) service/kratos-admin 8000:80"] + entrypoint: + [ + "bash", + "-c", + "kubectl -n stackspin port-forward --address $$(hostname -i) service/kratos-admin 8000:80", + ] kube_port_hydra_admin: image: bitnami/kubectl:1.27.3 user: "${KUBECTL_UID}:${KUBECTL_GID}" @@ -70,7 +76,12 @@ services: - 4445 volumes: - "$KUBECONFIG:/.kube/config" - entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address $$(hostname -i) service/hydra-admin 4445:4445"] + entrypoint: + [ + "bash", + "-c", + "kubectl -n stackspin port-forward --address $$(hostname -i) service/hydra-admin 4445:4445", + ] kube_port_kratos_public: image: bitnami/kubectl:1.27.3 user: "${KUBECTL_UID}:${KUBECTL_GID}" @@ -80,7 +91,12 @@ services: - 8080 volumes: - "$KUBECONFIG:/.kube/config" - entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address 0.0.0.0 service/kratos-public 8080:80"] + entrypoint: + [ + "bash", + "-c", + "kubectl -n stackspin port-forward --address 0.0.0.0 service/kratos-public 8080:80", + ] kube_port_mysql: image: bitnami/kubectl:1.27.3 user: "${KUBECTL_UID}:${KUBECTL_GID}" @@ -88,4 +104,9 @@ services: - 3306 volumes: - "$KUBECONFIG:/.kube/config" - entrypoint: ["bash", "-c", "kubectl -n stackspin port-forward --address $$(hostname -i) service/single-sign-on-database-mariadb 3306:3306"] + entrypoint: + [ + "bash", + "-c", + "kubectl -n stackspin port-forward --address $$(hostname -i) service/single-sign-on-database-mariadb 3306:3306", + ] diff --git a/frontend/package.json b/frontend/package.json index 356be15cff73232ea2bc2b8aa65d1f1a297c706c..daaa1f9a4e1ff6abe55fc7a6acd7a14494a599cf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,6 +5,7 @@ "dependencies": { "@craco/craco": "^6.2.0", "@headlessui/react": "^1.3.0", + "@headlessui/tailwindcss": "^0.1.3", "@heroicons/react": "^1.0.3", "@hookform/resolvers": "^2.6.1", "@tailwindcss/forms": "^0.3.3", @@ -18,6 +19,7 @@ "@types/react-dom": "^17.0.2", "axios": "^0.21.1", "clsx": "^1.1.1", + "gray-matter": "^4.0.3", "lint-staged": "^11.1.1", "lodash": "^4.17.21", "prismjs": "^1.24.1", @@ -37,6 +39,7 @@ "redux": "^4.1.0", "redux-persist": "^6.0.0", "redux-thunk": "^2.3.0", + "remark-gfm": "^3.0.1", "tailwindcss": "npm:@tailwindcss/postcss7-compat", "typescript": "^4.1.2", "urlcat": "^2.0.4", diff --git a/frontend/public/assets/stackspin_white_logo_icon.svg b/frontend/public/assets/stackspin_white_logo_icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..a6e3e8f87bd5c30b707c65ab01fd5020376956ac --- /dev/null +++ b/frontend/public/assets/stackspin_white_logo_icon.svg @@ -0,0 +1,12 @@ +<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M524.983 411.015C535.179 405.936 544.236 398.116 550.046 388.744L550.047 388.923V494.022C550.047 508.307 541.971 521.361 529.197 527.723L338.902 622.499C297.993 642.874 249.956 613.08 249.956 567.331C249.956 555.398 256.703 544.493 267.374 539.178L360.458 492.817C361.91 492.186 363.355 491.513 364.794 490.797L524.983 411.015Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M440.425 320.391C479.816 300.772 524.639 318.888 542.315 354.745C546.412 363.055 545.592 371.715 541.313 379.739C536.968 387.886 529.109 395.17 519.758 399.827L359.569 479.609C320.179 499.227 275.358 481.111 257.682 445.256C253.585 436.947 254.405 428.286 258.684 420.263C263.029 412.115 270.888 404.831 280.239 400.174L440.425 320.391Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M275.017 388.985C264.82 394.064 255.764 401.884 249.954 411.256L249.953 411.077L249.953 305.978C249.953 291.693 258.029 278.639 270.803 272.277L461.098 177.501C502.007 157.126 550.044 186.92 550.044 232.669C550.044 244.602 543.297 255.507 532.626 260.822L439.542 307.183C438.09 307.814 436.645 308.487 435.206 309.203L275.017 388.985Z" fill="white"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="800" height="800" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/frontend/public/assets/stackspin_white_logo_vertical.svg b/frontend/public/assets/stackspin_white_logo_vertical.svg new file mode 100644 index 0000000000000000000000000000000000000000..20ff6018b1d9cdf7063b08be5053512c078c4db4 --- /dev/null +++ b/frontend/public/assets/stackspin_white_logo_vertical.svg @@ -0,0 +1,22 @@ +<svg width="800" height="609" viewBox="0 0 800 609" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path d="M87.7525 470.138C87.7525 484.586 99.8558 490.273 115.373 493.039L120.649 493.962C130.735 495.806 135.855 498.265 135.855 504.106C135.855 509.946 130.424 514.25 120.649 514.25C110.718 514.25 102.649 510.254 100.476 497.804L84.649 501.647C87.287 518.554 100.942 527.315 120.649 527.315C139.89 527.315 153.063 518.4 153.063 502.876C153.063 487.352 139.735 482.895 122.976 479.821L117.7 478.899C109.321 477.362 104.201 474.903 104.201 469.062C104.201 463.529 109.166 460.147 117.545 460.147C125.925 460.147 131.976 463.683 133.838 472.905L149.355 468.294C145.942 455.536 134.769 447.236 117.545 447.236C99.5455 447.236 87.7525 455.536 87.7525 470.138Z" fill="white"/> +<path d="M161.007 463.068V509.485C161.007 519.015 167.214 525.163 176.524 525.163H198.093V511.33H182.421C179.628 511.33 178.076 509.793 178.076 506.719V463.068H200.421V449.388H178.076V423.891H161.007V449.388V463.068Z" fill="white"/> +<path d="M281.362 525.163V449.388H264.604V460.455H263.052C258.397 453.538 250.638 447.236 236.518 447.236C217.742 447.236 201.449 462.607 201.449 487.199C201.449 511.945 217.742 527.315 236.518 527.315C250.638 527.315 258.397 521.013 263.052 513.943H264.604V525.163H281.362ZM241.483 512.713C228.139 512.713 218.518 503.337 218.518 487.199C218.518 471.214 228.139 461.684 241.483 461.684C254.828 461.684 264.604 471.214 264.604 487.199C264.604 503.184 254.828 512.713 241.483 512.713Z" fill="white"/> +<path d="M288.805 487.199C288.805 510.715 306.34 527.315 328.684 527.315C349.787 527.315 362.356 514.865 366.391 498.265L349.787 494.423C348.08 505.028 341.563 512.559 328.839 512.559C315.805 512.559 305.874 502.876 305.874 487.199C305.874 471.675 315.805 461.992 328.839 461.992C341.253 461.992 348.08 469.677 349.322 479.821L365.77 476.44C362.667 459.686 349.632 447.236 328.529 447.236C306.34 447.236 288.805 463.683 288.805 487.199Z" fill="white"/> +<path d="M451.588 449.388H426.295L390.916 480.129V423.891H374.002V525.163H390.916V500.724L403.329 490.119L430.019 525.163H450.967L415.588 479.821L451.588 449.388Z" fill="white"/> +<path d="M453.579 470.137C453.579 484.585 465.682 490.272 481.2 493.039L486.475 493.961C496.562 495.805 501.682 498.264 501.682 504.105C501.682 509.946 496.251 514.249 486.475 514.249C476.544 514.249 468.476 510.253 466.303 497.803L450.476 501.646C453.114 518.553 466.769 527.314 486.475 527.314C505.717 527.314 518.13 518.399 518.13 502.876C518.13 487.352 505.561 482.894 488.803 479.82L483.527 478.898C475.148 477.361 470.027 474.902 470.027 469.061C470.027 463.528 474.993 460.147 483.372 460.147C491.751 460.147 497.803 463.682 499.665 472.904L515.182 468.293C511.768 455.536 500.596 447.236 483.372 447.236C465.372 447.236 453.579 455.536 453.579 470.137Z" fill="white"/> +<path d="M525.814 550.64H542.728V514.403H544.28C548.625 521.012 556.383 527.314 570.504 527.314C589.279 527.314 605.572 511.944 605.572 487.198C605.572 462.606 589.279 447.236 570.504 447.236C556.383 447.236 548.625 453.537 543.969 460.454H542.418V449.388H525.814V550.64ZM565.538 512.712C552.349 512.712 542.573 503.183 542.573 487.198C542.573 471.213 552.349 461.684 565.538 461.684C578.883 461.684 588.503 471.213 588.503 487.198C588.503 503.337 578.883 512.712 565.538 512.712Z" fill="white"/> +<path d="M630.084 449.388H613.17V525.162H630.084V449.388Z" fill="white"/> +<path d="M642.489 449.388V525.162H659.402V486.276C659.402 470.598 667.626 461.837 680.35 461.837C691.678 461.837 698.35 467.832 698.35 480.589V525.162H715.419V479.667C715.419 460.3 703.005 448.004 685.471 448.004C670.885 448.004 664.057 454.46 660.644 461.069H659.092V449.388H642.489Z" fill="white"/> +<rect x="613.176" y="423.899" width="16.9395" height="16.778" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M477.083 234.34C483.369 231.209 488.952 226.388 492.534 220.61L492.534 220.721V285.512C492.534 294.318 487.556 302.366 479.681 306.288L362.369 364.714C337.149 377.275 307.536 358.908 307.536 330.705C307.536 323.348 311.695 316.626 318.274 313.349L375.658 284.769C376.553 284.38 377.444 283.965 378.33 283.523L477.083 234.34Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M424.956 178.473C449.239 166.378 476.871 177.546 487.768 199.651C490.294 204.774 489.788 210.113 487.15 215.059C484.472 220.082 479.627 224.572 473.862 227.443L375.109 276.627C350.827 288.721 323.196 277.553 312.299 255.449C309.773 250.326 310.279 244.987 312.916 240.041C315.595 235.018 320.44 230.528 326.204 227.657L424.956 178.473Z" fill="white"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M322.985 220.759C316.7 223.89 311.117 228.711 307.535 234.489L307.534 234.378L307.534 169.587C307.534 160.781 312.513 152.734 320.388 148.812L437.7 90.3847C462.919 77.8241 492.532 96.1913 492.532 124.394C492.532 131.751 488.373 138.473 481.795 141.75L424.411 170.331C423.516 170.719 422.625 171.134 421.738 171.576L322.985 220.759Z" fill="white"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="800" height="609" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/frontend/public/custom/assets/default.svg b/frontend/public/custom/assets/default.svg new file mode 100644 index 0000000000000000000000000000000000000000..8c8161f827739e200119b656a9432307c7ff1ede --- /dev/null +++ b/frontend/public/custom/assets/default.svg @@ -0,0 +1,12 @@ +<svg width="800" height="800" viewBox="0 0 800 800" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0)"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M524.983 411.015C535.179 405.936 544.236 398.116 550.046 388.744L550.047 388.923V494.022C550.047 508.307 541.971 521.361 529.197 527.723L338.902 622.499C297.993 642.874 249.956 613.08 249.956 567.331C249.956 555.398 256.703 544.493 267.374 539.178L360.458 492.817C361.91 492.186 363.355 491.513 364.794 490.797L524.983 411.015Z" fill="black"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M440.425 320.391C479.816 300.772 524.639 318.888 542.315 354.745C546.412 363.055 545.592 371.715 541.313 379.739C536.968 387.886 529.109 395.17 519.758 399.827L359.569 479.609C320.179 499.227 275.358 481.111 257.682 445.256C253.585 436.947 254.405 428.286 258.684 420.263C263.029 412.115 270.888 404.831 280.239 400.174L440.425 320.391Z" fill="black"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M275.017 388.985C264.82 394.064 255.764 401.884 249.954 411.256L249.953 411.077L249.953 305.978C249.953 291.693 258.029 278.639 270.803 272.277L461.098 177.501C502.007 157.126 550.044 186.92 550.044 232.669C550.044 244.602 543.297 255.507 532.626 260.822L439.542 307.183C438.09 307.814 436.645 308.487 435.206 309.203L275.017 388.985Z" fill="black"/> +</g> +<defs> +<clipPath id="clip0"> +<rect width="800" height="800" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/frontend/public/custom/assets/email.svg b/frontend/public/custom/assets/email.svg new file mode 100644 index 0000000000000000000000000000000000000000..180dbf2d138a4c4b99bb41413884f63c4db3d8e2 --- /dev/null +++ b/frontend/public/custom/assets/email.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="#157983"> + <path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /> + <path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" /> +</svg> \ No newline at end of file diff --git a/frontend/public/custom/assets/gitlab.svg b/frontend/public/custom/assets/gitlab.svg new file mode 100644 index 0000000000000000000000000000000000000000..95a22f101748e6eb643b4778eb8dca39b8b462ac --- /dev/null +++ b/frontend/public/custom/assets/gitlab.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 380"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg> \ No newline at end of file diff --git a/frontend/public/custom/markdown/README.md b/frontend/public/custom/markdown/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b2387d5188df75f7b142b2502cf9e94c36b248d5 --- /dev/null +++ b/frontend/public/custom/markdown/README.md @@ -0,0 +1,34 @@ +--- +title: 'Custom App' +tileExcerpt: 'This is the default card content for a custom app' +--- + +You're seeing the default template content for a custom app. This content shows +when Stackspin cannot find a markdown file that matches the name of the app. + +## Troubleshooting + +Open your Dashboard repository (https://open.greenhost.net/stackspin/dashboard) +and check the `frontend/custom` folder, there should be two sub-directories: +`assets` (for the logos) and `markdown` (for the app description and modal +window content). There should be a markdown `.md` file and a vector `.svg` +image file for your app, and they should be named like your app's slug, +something like `markdown/gitlab.md` and `assets/gitlab.svg`. (If you followed +the documentation and added `ext-` in front of your slugs, that's fine, we +check for that.) + +If the files aren't there, do please add them. For reference on markdown +formatting, you can follow the `default.md` structure (basically we need a +`title` and an `excerpt` in the frontmatter, and the rest of the file can be +whatever you like it to be). + +If the files are there and are not being picked up by your Stackspin instance, +please check out the Dashboard repository +(https://open.greenhost.net/stackspin/dashboard) for help. + +For more information on how to fix this, check the External Apps chapter of the +Documentation:https://docs.stackspin.net/en/v2/system_administration/external-app.html. + +If you're still struggling, please [open an issue in our +Gitlab](https://open.greenhost.net/stackspin), or send us an email: +info[@]stackspin.net. diff --git a/frontend/public/custom/markdown/gitlab.md b/frontend/public/custom/markdown/gitlab.md new file mode 100644 index 0000000000000000000000000000000000000000..c09a8ed22f42a124f695075a3e7ad3b018c7770c --- /dev/null +++ b/frontend/public/custom/markdown/gitlab.md @@ -0,0 +1,12 @@ +--- +title: 'Gitlab' +tileExcerpt: 'DevOps software package which can develop, secure, and operate software' +--- + +## Introduction + +> **The DevSecOps Platform:** Deliver better software faster with one platform for your entire software delivery lifecycle + +## Signing in + +## Using GitLab diff --git a/frontend/public/markdown/hedgedoc.md b/frontend/public/markdown/hedgedoc.md new file mode 100644 index 0000000000000000000000000000000000000000..5897fa41196a73c2ab993fab581fc59754eff875 --- /dev/null +++ b/frontend/public/markdown/hedgedoc.md @@ -0,0 +1,18 @@ +--- +title: 'HedgeDoc' +tileExcerpt: 'Quick collaborative writing pads, powered by Markdown.' +--- + +## Introduction + +[HedgeDoc](https://hedgedoc.org/) (formerly known as CodiMD) is an open-source, web-based, self-hosted, collaborative markdown editor. + +You can use it to easily collaborate on notes, graphs and even presentations in real-time. All you need to do is to share your note-link to your co-workers and they’re ready to go. + +## Signing in + +If this is the first time you're opening HedgeDoc, or if you have recently logged out of Stackspin, you will need to sign into Stackspin by clicking on the `Sign in` button on the top right of the screen. When asked to "Choose Method," select "Sign in via Stackspin." That's it! + +## Using HedgeDoc + +HedgeDoc is great for collaborative writing -- it's fast and easy to use, but for the more advanced features like styling it does require understanding the basics of Markdown. HedgeDoc provides a comprehensive list of features in, what do you know, a HedgeDoc pad: https://demo.hedgedoc.org/features. That is a great place to start to both understand the power of HedgeDoc, and to learn about Markdown in general. diff --git a/frontend/public/markdown/nextcloud.md b/frontend/public/markdown/nextcloud.md index 58a8e2c8a6e19ae02e3ad57a861367a33181393f..0e7950eab12deb3c95be269ae6cf8e10e595c132 100644 --- a/frontend/public/markdown/nextcloud.md +++ b/frontend/public/markdown/nextcloud.md @@ -1,37 +1,22 @@ -# Nextcloud - - - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel felis rutrum, congue orci non, dictum -augue. In hac habitasse platea dictumst. Donec enim neque, vehicula vel consequat non, facilisis sed mauris. -Quisque a ligula sed velit gravida tristique. Mauris id nisi convallis, porttitor ante sed, blandit odio. In -consequat faucibus dolor, id aliquam quam. Fusce a faucibus tellus. Ut vitae ligula a ex consectetur rutrum -ultricies ac velit. Nullam in efficitur velit, efficitur euismod nulla. Mauris feugiat posuere libero, quis -accumsan ipsum mollis quis. Quisque at sapien lacus. Etiam aliquet, enim non pulvinar rhoncus, enim dolor -consectetur risus, a pharetra eros risus sed velit. Phasellus tristique feugiat ipsum, eget rhoncus arcu -ultrices nec. Nam et quam et sem tempor semper dictum nec ipsum. Aenean lobortis mauris non fringilla laoreet. -Sed fringilla vel justo nec pellentesque. - -Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio -a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin -lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed -ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec -felis congue, ultrices eros sit amet, maximus lacus. - -Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam -vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam -porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc -massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis -malesuada. - -Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio -a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin -lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed -ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec -felis congue, ultrices eros sit amet, maximus lacus. - -Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam -vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam -porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc -massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis -malesuada. +--- +title: 'Nextcloud' +tileExcerpt: 'Shared online file storage with local sync, in-browser file editing, password manager and more.' +--- + +## Introduction + +> [Nextcloud](https://nextcloud.com/) is "the most popular self-hosted collaboration solution for tens of millions of users at thousands of organizations across the globe." + +## Signing in + +If this is your first time signing in, or if you have recently logged out, launch the app from Stackspin and then select "Login with Stackspin" from the Nextcloud login panel. That's it! + +## Using Nextcloud + +You can use Nextcloud from a browser, or with dedicated mobile and desktop apps. You can find more information here: https://nextcloud.com/install/. + +Learning about Nextcloud is best done through Nextcloud: for questions and community support, there is https://help.nextcloud.com/. User Documentation can be found here: https://docs.nextcloud.com/server/latest/user_manual/en/. + +### Nextcloud Addons + +Nextcloud has many, many addons and "apps." They vary in quality and usefulness, and some are excellent additions to team collaboration. Stackspin adds a limited number of plugins/addons that we consider safe and tested, and commit to update and keep secure with the rest of Nextcloud. If you wish to add more plugins, get in touch: info@stackspin.net. diff --git a/frontend/public/markdown/wekan.md b/frontend/public/markdown/wekan.md index b8734302a06bdb7e2f96115ce529be3d8508fa9c..608339fa5ea30f9e020e1a514353c63b66ecbc60 100644 --- a/frontend/public/markdown/wekan.md +++ b/frontend/public/markdown/wekan.md @@ -1,37 +1,24 @@ -# Wekan - - - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel felis rutrum, congue orci non, dictum -augue. In hac habitasse platea dictumst. Donec enim neque, vehicula vel consequat non, facilisis sed mauris. -Quisque a ligula sed velit gravida tristique. Mauris id nisi convallis, porttitor ante sed, blandit odio. In -consequat faucibus dolor, id aliquam quam. Fusce a faucibus tellus. Ut vitae ligula a ex consectetur rutrum -ultricies ac velit. Nullam in efficitur velit, efficitur euismod nulla. Mauris feugiat posuere libero, quis -accumsan ipsum mollis quis. Quisque at sapien lacus. Etiam aliquet, enim non pulvinar rhoncus, enim dolor -consectetur risus, a pharetra eros risus sed velit. Phasellus tristique feugiat ipsum, eget rhoncus arcu -ultrices nec. Nam et quam et sem tempor semper dictum nec ipsum. Aenean lobortis mauris non fringilla laoreet. -Sed fringilla vel justo nec pellentesque. - -Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio -a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin -lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed -ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec -felis congue, ultrices eros sit amet, maximus lacus. - -Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam -vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam -porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc -massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis -malesuada. - -Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio -a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin -lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed -ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec -felis congue, ultrices eros sit amet, maximus lacus. - -Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam -vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam -porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc -massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis -malesuada. +--- +title: 'Wekan' +tileExcerpt: 'Kanban-style project and task management, from the simplest to-do board to multi-user boards with sub-tasks and swimlanes.' +--- + +## Introduction + +> Experience efficient task management with [WeKan](https://wekan.github.io/) - the Open-Source, customizable, and privacy-focused kanban. + +## Signing in + +If this is the first time you're opening Wekan, or if you have recently logged out of Stackspin, you will need to sign into Wekan by choosing the option `Sign in with Oidc` on the loging window. That's it! + +## What is a kanban board? + +A [Kanban Board](https://en.wikipedia.org/wiki/Kanban_board) is a physical or virtual board that helps with tracking projects and tasks. The short, analogue version is this: + +- you write your tasks on post-its +- you divide your board into a couple of columns: let's say "To Do," "Doing," and "Done" +- you put your post-its inside the relevant column on the board, and you move them around as you progress work on them. + +A digital kanban board like Wekan gives you some extra powers, like the ability to be very detailed on the digital "post-it:" you can specify timelines, sub-tasks, who is responsible, you can chat about the task, etc. You can also change your columns whenever you want, add "[swimlanes](https://kanbanize.com/kanban-resources/kanban-software/kanban-swimlanes)" and many more features. + +If you're into kanban boards, Wekan is able to be a full-service task and project management platform for you and your team. diff --git a/frontend/public/markdown/wordpress.md b/frontend/public/markdown/wordpress.md index bff1dc545dce2b21da79ab26476b3e54fcd8c5a9..888e5bfcf4c09000f716e1f21a499c499b7cd053 100644 --- a/frontend/public/markdown/wordpress.md +++ b/frontend/public/markdown/wordpress.md @@ -1,37 +1,23 @@ -# Wordpress +--- +title: 'Wordpress' +tileExcerpt: 'The most used website platform in the world.' +--- - +## Introduction -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel felis rutrum, congue orci non, dictum -augue. In hac habitasse platea dictumst. Donec enim neque, vehicula vel consequat non, facilisis sed mauris. -Quisque a ligula sed velit gravida tristique. Mauris id nisi convallis, porttitor ante sed, blandit odio. In -consequat faucibus dolor, id aliquam quam. Fusce a faucibus tellus. Ut vitae ligula a ex consectetur rutrum -ultricies ac velit. Nullam in efficitur velit, efficitur euismod nulla. Mauris feugiat posuere libero, quis -accumsan ipsum mollis quis. Quisque at sapien lacus. Etiam aliquet, enim non pulvinar rhoncus, enim dolor -consectetur risus, a pharetra eros risus sed velit. Phasellus tristique feugiat ipsum, eget rhoncus arcu -ultrices nec. Nam et quam et sem tempor semper dictum nec ipsum. Aenean lobortis mauris non fringilla laoreet. -Sed fringilla vel justo nec pellentesque. +If you spent any time on the internet in the past twenty years, chances are you +have used Wordpress. It is by far the most common website CMS (Content +Management System). -Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio -a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin -lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed -ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec -felis congue, ultrices eros sit amet, maximus lacus. +Wordpress in Stackspin is usually set up to serve the website on the main stackspin URL. The Wordpress "Go to app" button on your Stackspin dashboard will take you directly to the wordpress admin (wp-admin) page. -Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam -vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam -porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc -massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis -malesuada. +## Signing in -Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio -a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin -lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed -ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec -felis congue, ultrices eros sit amet, maximus lacus. +If this is your first time signing in, or if you have logged out recently, +please click on "Login with OpenID Connect" to access the Wordpress dashboard +with your Stackspin user login. -Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam -vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam -porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc -massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis -malesuada. +Once inside the dashboard, you can set custom access privileges to users, as +well as create non-Stackspin users directly from Wordpress. This might be +useful if, for example, you have guest writers or editors that do not belong to +your organization. diff --git a/frontend/public/markdown/zulip.md b/frontend/public/markdown/zulip.md index ca37acd63d51640d1ad82ec279102061f077b64b..e90ffc0c1b4d25cb197300acbe992fdf1381123e 100644 --- a/frontend/public/markdown/zulip.md +++ b/frontend/public/markdown/zulip.md @@ -1,37 +1,16 @@ -# Zulip +--- +title: 'Zulip' +tileExcerpt: 'Team chat organized in topics and streams, with mobile and desktop apps. Unlimited channels and users.' +--- - +## Introduction -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel felis rutrum, congue orci non, dictum -augue. In hac habitasse platea dictumst. Donec enim neque, vehicula vel consequat non, facilisis sed mauris. -Quisque a ligula sed velit gravida tristique. Mauris id nisi convallis, porttitor ante sed, blandit odio. In -consequat faucibus dolor, id aliquam quam. Fusce a faucibus tellus. Ut vitae ligula a ex consectetur rutrum -ultricies ac velit. Nullam in efficitur velit, efficitur euismod nulla. Mauris feugiat posuere libero, quis -accumsan ipsum mollis quis. Quisque at sapien lacus. Etiam aliquet, enim non pulvinar rhoncus, enim dolor -consectetur risus, a pharetra eros risus sed velit. Phasellus tristique feugiat ipsum, eget rhoncus arcu -ultrices nec. Nam et quam et sem tempor semper dictum nec ipsum. Aenean lobortis mauris non fringilla laoreet. -Sed fringilla vel justo nec pellentesque. +> [Zulip](https://zulip.com/) combines the immediacy of real-time chat with an email threading model. With Zulip, you can catch up on important conversations while ignoring irrelevant ones. -Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio -a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin -lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed -ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec -felis congue, ultrices eros sit amet, maximus lacus. +## Signing in -Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam -vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam -porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc -massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis -malesuada. +If this is the first time you're opening Zulip, or if you have recently logged out of Stackspin, you will need to sign into Stackspin by clicking on the `Log in with Stackspin` button on the top right of the screen. When asked to "Choose Method," select "Sign in via Stackspin." That's it! -Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio -a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin -lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed -ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec -felis congue, ultrices eros sit amet, maximus lacus. +## Using Zulip -Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam -vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam -porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc -massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis -malesuada. +You can use Zulip in your browser, or you can use the official apps for all desktop and mobile devices. You can download the app of your choice here:https://zulip.com/apps/. When logging into the app for the first time, you will first enter the URL of your Stackspin's Zulip, and then enter your Stackspin username and password when prompted. diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 4d57a9aec1305b3a781976665d6dd3d13284a83e..3b360fc069b13d481956c7e212661444bf1fbf3a 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -3,6 +3,7 @@ import { Disclosure, Menu, Transition } from '@headlessui/react'; import { MenuIcon, XIcon } from '@heroicons/react/outline'; import { performApiCall } from 'src/services/api'; import { useAuth } from 'src/services/auth'; +import { useApps } from 'src/services/apps'; import Gravatar from 'react-gravatar'; import { Link, useLocation } from 'react-router-dom'; import clsx from 'clsx'; @@ -49,6 +50,12 @@ const Header: React.FC<HeaderProps> = () => { const { pathname } = useLocation(); + const { apps, loadApps } = useApps(); + useEffect(() => { + loadApps(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { let active = true; async function loadEnvironment() { @@ -81,6 +88,12 @@ const Header: React.FC<HeaderProps> = () => { const signOutUrl = `${environment.HYDRA_PUBLIC_URL}/oauth2/sessions/logout`; const kratosSettingsUrl = `${environment.KRATOS_PUBLIC_URL}/self-service/settings/browser`; + const adminTag = isAdmin ? ( + <span className="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-500 ring-1 ring-inset ring-gray-300"> + Admin + </span> + ) : null; + return ( <> <Disclosure as="nav" className="bg-white shadow relative z-10"> @@ -123,7 +136,9 @@ const Header: React.FC<HeaderProps> = () => { ))} </div> </div> + <div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0"> + {adminTag} {/* Profile dropdown */} <Menu as="div" className="ml-3 relative"> <div> @@ -216,7 +231,13 @@ const Header: React.FC<HeaderProps> = () => { </Disclosure> {currentUserModal && ( - <UserModal open={currentUserModal} onClose={currentUserModalClose} userId={currentUserId} setUserId={_.noop} /> + <UserModal + open={currentUserModal} + onClose={currentUserModalClose} + userId={currentUserId} + setUserId={_.noop} + apps={apps} + /> )} </> ); diff --git a/frontend/src/components/Modal/Modal/Modal.tsx b/frontend/src/components/Modal/Modal/Modal.tsx index d64acfecb2c30877b7187498af02dbd616307510..00e5ba031f395cca03cbb27744782169b01211b5 100644 --- a/frontend/src/components/Modal/Modal/Modal.tsx +++ b/frontend/src/components/Modal/Modal/Modal.tsx @@ -9,7 +9,8 @@ export const Modal: React.FC<ModalProps> = ({ onSave, saveButtonTitle = 'Save Changes', children, - title = '', + title, + img, useCancelButton = false, cancelButtonTitle = 'Cancel', isLoading = false, @@ -69,7 +70,10 @@ export const Modal: React.FC<ModalProps> = ({ {!useCancelButton && ( <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:items-center sm:justify-between"> - <div>{title}</div> + <div className="flex items-center"> + <img className="rounded-md" width={32} src={img} alt={title} /> + <span className="ml-2 uppercase font-bold">{title}</span> + </div> <button type="button" className="w-full inline-flex justify-center rounded-md border border-gray-200 p-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" diff --git a/frontend/src/components/Modal/Modal/types.ts b/frontend/src/components/Modal/Modal/types.ts index 52cf784b6f07871aa74f3a9c28a4b9c86e2d0a91..3c797a414ac20aae034c8a9402697c0ac4621fb3 100644 --- a/frontend/src/components/Modal/Modal/types.ts +++ b/frontend/src/components/Modal/Modal/types.ts @@ -4,6 +4,7 @@ export type ModalProps = { open: boolean; onClose: () => void; title?: string; + img?: string; onSave?: () => void; saveButtonTitle?: string; useCancelButton?: boolean; diff --git a/frontend/src/components/UserModal/UserModal.tsx b/frontend/src/components/UserModal/UserModal.tsx index b6ceed73710f45f3bda1e4218224c85fbec575fd..05679dcbf7cd6d56a362e42a0ce24e851bcb6618 100644 --- a/frontend/src/components/UserModal/UserModal.tsx +++ b/frontend/src/components/UserModal/UserModal.tsx @@ -6,11 +6,14 @@ import { Banner, Modal, ConfirmationModal, InfoModal } from 'src/components'; import { Input, Select } from 'src/components/Form'; import { User, UserRole, useUsers } from 'src/services/users'; import { useAuth } from 'src/services/auth'; +import { AppStatusEnum } from 'src/services/apps/types'; + import { HIDDEN_APPS } from 'src/modules/dashboard/consts'; -import { appAccessList, initialUserForm } from './consts'; +// import { initialUserForm } from './consts'; + import { UserModalProps } from './types'; -export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) => { +export const UserModal = ({ open, onClose, userId, setUserId, apps }: UserModalProps) => { const [deleteModal, setDeleteModal] = useState(false); const [passwordLinkModal, setPasswordLinkModal] = useState(false); const [isAdminRoleSelected, setAdminRoleSelected] = useState(true); @@ -30,6 +33,33 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) } = useUsers(); const { currentUser, isAdmin } = useAuth(); + // build initial app role list programmatially, + // pulling from list of installed apps on cluster + interface AppListInt { + name: string; + role: UserRole; + } + const appList: AppListInt[] = []; + const initialAppRoleLatest = () => { + apps + .filter((app) => app.status !== AppStatusEnum.NotInstalled) + .map((app) => + app.slug === 'monitoring' + ? appList.push({ name: app.slug, role: UserRole.NoAccess }) + : appList.push({ name: app.slug, role: UserRole.User }), + ); + }; + initialAppRoleLatest(); + + const initialUserForm = { + id: '', + name: '', + email: '', + app_roles: appList, + status: '', + }; + + // populate the initial "New User" window with installed apps and default roles const { control, reset, handleSubmit } = useForm<User>({ defaultValues: initialUserForm, }); @@ -57,9 +87,9 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) reset(user); } - return () => { - reset(initialUserForm); - }; + // return () => { + // reset(initialUserForm); + // }; }, [user, reset, open]); const dashboardRole = useWatch({ @@ -275,11 +305,11 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) <div className="flex-shrink-0 flex-1 flex items-center"> <img className="h-10 w-10 rounded-md overflow-hidden" - src={_.find(appAccessList, ['name', item.name!])?.image} + src={_.find(apps, ['slug', item.name!])?.assetSrc} alt={item.name ?? 'Image'} /> <h3 className="ml-4 text-md leading-6 font-medium text-gray-900"> - {_.find(appAccessList, ['name', item.name!])?.label} + {_.find(apps, ['slug', item.name!])?.name} </h3> </div> <div> diff --git a/frontend/src/components/UserModal/consts.ts b/frontend/src/components/UserModal/consts.ts index ebf5a1f93be1338ba981d95c5b8f29fd41e28eb7..bb750df0b4d7952d5c3d3e9a85f9651ce579a09a 100644 --- a/frontend/src/components/UserModal/consts.ts +++ b/frontend/src/components/UserModal/consts.ts @@ -1,4 +1,12 @@ -import { UserRole } from 'src/services/users'; +// This file is still being used in the AppSingle.tsx file +// to populate the single app card with URLs and images. +// Single App is not an active view at the moment, +// so automating this is not a priority at the moment. +// once we activate single app views, we will need to use the API call +// to populate the AppSingle card with this info. +// See UserModal.tsx for inspiration, search for initialAppRoleLatest() + +// import { UserRole } from 'src/services/users'; export const appAccessList = [ { @@ -48,41 +56,41 @@ export const allAppAccessList = [ ...appAccessList, ]; -export const initialAppRoles = [ - { - name: 'dashboard', - role: UserRole.User, - }, - { - name: 'hedgedoc', - role: UserRole.User, - }, - { - name: 'wekan', - role: UserRole.User, - }, - { - name: 'wordpress', - role: UserRole.User, - }, - { - name: 'nextcloud', - role: UserRole.User, - }, - { - name: 'zulip', - role: UserRole.User, - }, - { - name: 'monitoring', - role: UserRole.NoAccess, - }, -]; +// export const initialAppRoles = [ +// { +// name: 'dashboard', +// role: UserRole.User, +// }, +// { +// name: 'hedgedoc', +// role: UserRole.User, +// }, +// { +// name: 'wekan', +// role: UserRole.User, +// }, +// { +// name: 'wordpress', +// role: UserRole.User, +// }, +// { +// name: 'nextcloud', +// role: UserRole.User, +// }, +// { +// name: 'zulip', +// role: UserRole.User, +// }, +// { +// name: 'monitoring', +// role: UserRole.NoAccess, +// }, +// ]; -export const initialUserForm = { - id: '', - name: '', - email: '', - app_roles: initialAppRoles, - status: '', -}; +// export const initialUserForm = { +// id: '', +// name: '', +// email: '', +// app_roles: initialAppRoles, +// status: '', +// }; diff --git a/frontend/src/components/UserModal/types.ts b/frontend/src/components/UserModal/types.ts index f7804ae58e543185431fe57a25be0fe379fb1a94..e9b544986247a2855c5d4de39cb765bbb3953050 100644 --- a/frontend/src/components/UserModal/types.ts +++ b/frontend/src/components/UserModal/types.ts @@ -1,6 +1,9 @@ +import { App } from 'src/services/apps'; + export type UserModalProps = { open: boolean; onClose: () => void; userId: string | null; setUserId: any; + apps: App[]; }; diff --git a/frontend/src/modules/apps/AppSingle.tsx b/frontend/src/modules/apps/AppSingle.tsx index 92b84f5e488d5aa4a5db6c08c5b683927964d0d8..2b2c0d48eb0fcb874d5f4ce4cbe78d8aa809f9f3 100644 --- a/frontend/src/modules/apps/AppSingle.tsx +++ b/frontend/src/modules/apps/AppSingle.tsx @@ -92,7 +92,7 @@ export const AppSingle: React.FC = () => { return ( <div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden lg:flex lg:flex-row"> <div - className="block bg-white overflow-hidden shadow rounded-sm basis-4/12 mx-auto sm:px-6 lg:px-8 overflow-hidden block lg:flex-none" + className="block bg-white overflow-hidden shadow rounded-sm basis-4/12 mx-auto sm:px-6 lg:px-8 lg:flex-none" style={{ height: 'fit-content' }} > <div className="px-4 py-5 sm:p-6 flex flex-col"> diff --git a/frontend/src/modules/apps/Apps.tsx b/frontend/src/modules/apps/Apps.tsx index ba83667f65c3475aec4e5f62f0541f6f6800d91e..1f0a70a1abf71cdfb2ab389b2a38dad27fa67c40 100644 --- a/frontend/src/modules/apps/Apps.tsx +++ b/frontend/src/modules/apps/Apps.tsx @@ -66,7 +66,17 @@ export const Apps: React.FC = () => { const status = e.cell.row.original.status as AppStatusEnum; return ( <div className="flex items-center"> - <div className={`flex-shrink-0 h-4 w-4 rounded-full bg-${getConstForStatus(status, 'colorClass')}`} /> + {status === AppStatusEnum.External ? ( + <div + className={`flex-shrink-0 h-4 w-4 rounded-full bg-transparent border-2 border-${getConstForStatus( + status, + 'colorClass', + )}`} + /> + ) : ( + <div className={`flex-shrink-0 h-4 w-4 rounded-full bg-${getConstForStatus(status, 'colorClass')}`} /> + )} + {status === AppStatusEnum.Installing ? ( <div className={`ml-2 cursor-pointer text-sm text-${getConstForStatus(status, 'colorClass')}`} diff --git a/frontend/src/modules/apps/consts.tsx b/frontend/src/modules/apps/consts.tsx index 8a43f3f157945c085722bf296ca15e1f54545b00..59b5f92fc2ab98a63ee47a69f2337963724a470c 100644 --- a/frontend/src/modules/apps/consts.tsx +++ b/frontend/src/modules/apps/consts.tsx @@ -44,6 +44,12 @@ const tableConsts = [ buttonTitle: null, buttonIcon: null, }, + { + status: AppStatusEnum.External, + colorClass: 'gray-400', + buttonTitle: null, + buttonIcon: null, + }, ]; export const getConstForStatus = (appStatus: AppStatusEnum, paramName: string) => { diff --git a/frontend/src/modules/dashboard/Dashboard.tsx b/frontend/src/modules/dashboard/Dashboard.tsx index 47968ef603a359036b92330aee6356b25ad64eb5..45ce92b14e6703e4c223925201647fe641b1fdb8 100644 --- a/frontend/src/modules/dashboard/Dashboard.tsx +++ b/frontend/src/modules/dashboard/Dashboard.tsx @@ -5,10 +5,14 @@ * "Utilities" is a special section that links to the Stackspin documentation, * and that shows the "Monitoring" application if it is installed. */ + import React, { useEffect } from 'react'; import { useApps } from 'src/services/apps'; +import { useSysInfo } from 'src/services/sysInfo'; +import { useAuth } from 'src/services/auth'; + import { AppStatusEnum } from 'src/services/apps/types'; -import { DashboardCard, DashboardUtility } from './components'; +import { DashboardCard, DashboardUtility, UpdateAlert, VersionInfo } from './components'; import { DASHBOARD_QUICK_ACCESS, HIDDEN_APPS, UTILITY_APPS } from './consts'; export const Dashboard: React.FC = () => { @@ -16,36 +20,61 @@ export const Dashboard: React.FC = () => { const splitedDomain = host.split('.'); splitedDomain.shift(); const { apps, loadApps } = useApps(); + const { sysInfo, loadSysInfo } = useSysInfo(); - // Tell React to load the apps + // Tell React to load the apps and system information useEffect(() => { loadApps(); - + loadSysInfo(); return () => {}; }, []); + const { currentUser } = useAuth(); + + const appVersions = { ...sysInfo.sysInfo.appVersions }; + + const greet = () => { + const safeUserName = currentUser?.name !== '' ? currentUser?.name : currentUser?.email; + const myDate = new Date(); + const hours = myDate.getHours(); + const greeting = + hours < 12 ? 'morning' : hours >= 12 && hours <= 17 ? 'afternoon' : hours >= 17 && hours <= 24 ? 'evening' : null; + + return ( + <span> + Good {greeting}, {safeUserName}. + </span> + ); + }; + return ( <div className="relative"> + <UpdateAlert sysInfo={sysInfo.sysInfo} /> <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8"> - <div className="mt-6 pb-5 border-b border-gray-200 sm:flex sm:items-center sm:justify-between"> - <h1 className="text-3xl leading-6 font-bold text-gray-900">Dashboard</h1> + <div className="mt-6 pb-5 border-b border-gray-200 flex items-center justify-between"> + <h1 className="text-xl sm:text-3xl leading-6 font-bold text-gray-900">{greet()}</h1> + <div className="system-status text-xs font-medium text-gray-500 flex flex-col gap-2"> + <div className="flex items-center gap-1"> + <VersionInfo sysInfo={sysInfo.sysInfo} /> + </div> + </div> </div> </div> - <div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow"> - <div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-4 mb-10"> + <div className=" mt-5 grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-3 mb-10"> {apps .filter((app) => HIDDEN_APPS.concat(UTILITY_APPS).indexOf(app.slug) === -1) .filter((app) => app.status !== AppStatusEnum.NotInstalled) - .map((app) => ( - <DashboardCard app={app} key={app.name} /> - ))} + // .filter((app) => !app.external) + .map((app) => { + const version = appVersions[app.slug as keyof typeof appVersions]; + return <DashboardCard app={app} key={app.name} version={version} />; + })} </div> <div className="max-w-4xl mx-auto py-4 sm:px-6 lg:px-8 h-full flex-grow"> <div className="pb-4 border-b border-gray-200 sm:flex sm:items-center"> <h3 className="text-lg leading-6 font-medium text-gray-900">Utilities</h3> </div> - <dl className="mt-5 grid grid-cols-1 gap-2 sm:grid-cols-2"> {DASHBOARD_QUICK_ACCESS.map((item) => ( <DashboardUtility item={item} key={item.name} /> diff --git a/frontend/src/modules/dashboard/components/DashboardCard/DashboardCard.tsx b/frontend/src/modules/dashboard/components/DashboardCard/DashboardCard.tsx index 1b9b820090d41765ab6ba4bb21181eb0776aee6c..3e9ff5578bc8f41981a3f3ade83212c3b6222893 100644 --- a/frontend/src/modules/dashboard/components/DashboardCard/DashboardCard.tsx +++ b/frontend/src/modules/dashboard/components/DashboardCard/DashboardCard.tsx @@ -1,12 +1,20 @@ import React, { useState, useEffect } from 'react'; import { Modal } from 'src/components'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import matter from 'gray-matter'; +import { QuestionMarkCircleIcon, ExternalLinkIcon } from '@heroicons/react/outline'; -export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => { +type DashboardCardProps = { + app: any; + version: string | number; +}; + +export const DashboardCard = ({ app, version }: DashboardCardProps) => { const [readMoreVisible, setReadMoreVisible] = useState(false); - const [content, setContent] = useState(''); + const [rawMarkdown, setContent] = useState(''); - const onReadMoreCloseClick = () => setReadMoreVisible(false); + const onReadMoreToggleClick = () => setReadMoreVisible((current) => !current); useEffect(() => { fetch(app.markdownSrc) @@ -17,36 +25,95 @@ export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => { .catch(() => {}); }, [app.markdownSrc]); + const appDescription = matter(rawMarkdown); + + // Check if we're actually pulling markdown and if not, replace with default boilerplate file + + const isItMarkdown = appDescription.content.search('DOCTYPE'); + + if (isItMarkdown !== -1) { + fetch('/custom/markdown/default.md') + .then((res) => res.text()) + .then((md) => { + return setContent(md); + }) + .catch(() => {}); + } + + const launchButton = app.external ? ( + <> + <span>Go To App</span> + <ExternalLinkIcon className="w-4 h-4 ml-1" /> + </> + ) : ( + <> + <span>Go to App</span> + <img className="h-6" src="/assets/stackspin_white_logo_icon.svg" alt="Stackspin" /> + </> + ); + return ( <> - <div className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0" key={app.name}> - <div className="px-4 py-5 sm:p-6"> - <div className="mr-4 flex items-center"> - <img - className="h-16 w-16 rounded-md overflow-hidden mr-4 flex-shrink-0" - src={app.assetSrc} - alt={app.name} - /> - - <div> - <h2 className="text-xl leading-8 font-bold">{app.name}</h2> + <div + className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0 flex flex-col justify-between relative" + key={app.name} + > + <div className="upper-half"> + <div className="flex justify-between"> + {!app.external ? ( + // Core tag is invisible for the time being. I am leaving it in here + // because we might want to re-use this ternary for a different tag, such as "custom" + <span className="inline-flex invisible ml-3 items-center rounded-b-md bg-primary-100 px-2 py-1 text-xs font-medium text-primary-800 border border-t-0 "> + Core + </span> + ) : ( + <span className="inline-flex ml-3 items-center rounded-b-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-600 border border-t-0 "> + External + </span> + )} + + {isItMarkdown === -1 ? ( + <a href="#" onClick={onReadMoreToggleClick} className="inline-flex items-center justify-center mr-2"> + <QuestionMarkCircleIcon className="h-4 w-4 text-primary-600 hover:text-primary-800" /> + </a> + ) : null} + </div> + <div className="px-4 pt-4 pb-2"> + <div className="mr-4 flex items-center"> + <img + className="h-16 w-16 rounded-md overflow-hidden mr-4 flex-shrink-0" + src={app.assetSrc} + onError={(e) => { + e.currentTarget.src = 'custom/assets/default.svg'; + }} + alt={app.name} + /> + + <div className="flex justify-between gap-1 items-center"> + <h2 className="text-xl leading-8 font-bold">{app.name}</h2> + <p className="text-xs text-gray-400">{version}</p> + </div> </div> + + <p className="text-gray-500 mt-2 text-sm leading-5 font-normal">{appDescription.data.tileExcerpt}</p> </div> </div> <div className="px-2.5 py-2.5 sm:px-4 flex justify-end"> <a - href={app.url} + href={app.slug === 'wordpress' ? `${app.url}/wp-admin/` : app.url} target="_blank" rel="noreferrer" - className="inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" + className="inline-flex h-10 items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" > - Launch App + {launchButton} </a> </div> </div> - <Modal open={readMoreVisible} onClose={onReadMoreCloseClick} title={app.name}> - <ReactMarkdown className="prose">{content}</ReactMarkdown> + <Modal open={readMoreVisible} onClose={onReadMoreToggleClick} title={app.name} img={app.assetSrc}> + <ReactMarkdown className="prose prose-slate" remarkPlugins={[remarkGfm]}> + {appDescription.content} + </ReactMarkdown> </Modal> </> ); diff --git a/frontend/src/modules/dashboard/components/UpdateAlert/UpdateAlert.tsx b/frontend/src/modules/dashboard/components/UpdateAlert/UpdateAlert.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bd9f11fc5771da152abede150d5d542bb9d20fc8 --- /dev/null +++ b/frontend/src/modules/dashboard/components/UpdateAlert/UpdateAlert.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { LightningBoltIcon, NewspaperIcon, SparklesIcon } from '@heroicons/react/outline'; +import { SysInfoState } from 'src/services/sysInfo/redux/types'; + +export const UpdateAlert = (sysInfo: SysInfoState) => { + const updateTime: Date = new Date(sysInfo.sysInfo.lastUpdated); + const hoursSinceUpdate = Math.floor((new Date().getTime() - updateTime.getTime()) / 3600000); + const updateStatus: string = + sysInfo.sysInfo.lastRelease > sysInfo.sysInfo.version ? 'imminent' : hoursSinceUpdate <= 24 ? 'updated' : 'no'; + + const updateAlert = (status: string) => + status === 'imminent' ? ( + <div className="update-alert w-full md:h-5 h-12 md:py-3 px-5 bg-yellow-100 flex items-center justify-center text-xs font-medium text-gray-500 gap-2"> + <LightningBoltIcon className="h-4 w-4 flex-none text-primary-800" /> + <span className="py-3">Attention: your Stackspin instance will be updated in the next 24 hours. </span> + <a + className="hover:text-primary-500 underline py-3" + href={sysInfo.sysInfo.releaseNotesUrl} + target="_blank" + rel="noreferrer noopener" + > + Explore new features <NewspaperIcon className="h-4 w-4 inline" /> + </a> + </div> + ) : status === 'updated' ? ( + <div className="update-alert w-full h-5 py-3 bg-primary-200 flex items-center justify-center text-xs font-medium text-primary-700 gap-2"> + <SparklesIcon className="h-4 w-4 flex-none text-primary-800" /> + <p>Your Stackspin just got an update!</p> + <a + className="hover:text-primary-500 underline" + href={sysInfo.sysInfo.releaseNotesUrl} + target="_blank" + rel="noreferrer noopener" + > + See what's new <NewspaperIcon className="h-4 w-4 inline" /> + </a> + </div> + ) : null; + + return <>{updateAlert(updateStatus)}</>; +}; diff --git a/frontend/src/modules/dashboard/components/UpdateAlert/index.ts b/frontend/src/modules/dashboard/components/UpdateAlert/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a64428b088ac4da2792f2933045eae67d299392 --- /dev/null +++ b/frontend/src/modules/dashboard/components/UpdateAlert/index.ts @@ -0,0 +1 @@ +export { UpdateAlert } from './UpdateAlert'; diff --git a/frontend/src/modules/dashboard/components/VersionInfo/VersionInfo.tsx b/frontend/src/modules/dashboard/components/VersionInfo/VersionInfo.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8f7da1cb90f46c8758bdbe973ccf9f9154f22fad --- /dev/null +++ b/frontend/src/modules/dashboard/components/VersionInfo/VersionInfo.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { Popover, Transition } from '@headlessui/react'; +import { + CheckCircleIcon, + NewspaperIcon, + ChevronDownIcon, + CalendarIcon, + BeakerIcon, + HomeIcon, + RefreshIcon, +} from '@heroicons/react/outline'; +import { SysInfoState } from 'src/services/sysInfo/redux/types'; + +export const VersionInfo = (sysInfo: SysInfoState) => { + const branch = sysInfo.sysInfo.followingGit; + const updateTime: Date = new Date(sysInfo.sysInfo.lastUpdated); + + const branchInfoAvailable = typeof branch !== 'undefined'; + const stableBranch = branchInfoAvailable ? branch.match(/^v(\d)+$/) : false; + + const versionInfo = stableBranch ? ( + <> + <CheckCircleIcon className="h-4 w-4 text-primary-600" /> + <span>Stackspin v{sysInfo.sysInfo.version}</span> + </> + ) : branchInfoAvailable ? ( + <> + <span>Stackspin v{sysInfo.sysInfo.version}</span> + </> + ) : null; + + const updatesText = stableBranch ? ( + <> + <div className="px-4 py-2 flex items-center gap-1"> + <HomeIcon className="w-4 h-4 text-gray-500" /> + <span>Latest stable branch ({branch})</span> + </div> + <div className="px-4 py-2 flex items-center gap-1"> + <RefreshIcon className="w-4 h-4 text-gray-500" /> + <span>Automatic updates active</span> + </div> + </> + ) : branchInfoAvailable ? ( + <div className="px-4 py-2 flex items-center gap-1"> + <BeakerIcon className="w-4 h-4 text-gray-500" /> + <span>Custom branch ({branch})</span> + </div> + ) : null; + + return ( + <Popover className="relative flex items-center"> + <Popover.Button className="flex text-sm px-2.5 py-1.5 group items-center gap-1 border border-transparent hover:shadow-sm focus:shadow-sm font-medium rounded-md text-gray-500 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"> + {versionInfo} + <ChevronDownIcon className="h-4 w-4 text-primary-800" /> + </Popover.Button> + <Transition + enter="transition ease-out duration-200" + enterFrom="transform opacity-0 scale-95" + enterTo="transform opacity-100 scale-100" + leave="transition ease-in duration-75" + leaveFrom="transform opacity-100 scale-100" + leaveTo="transform opacity-0 scale-95" + className="z-10" + > + <Popover.Panel className="absolute z-10 top-5 mt-2 right-0 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"> + <div className="flex flex-col items-stretch p-4 w-60 divide-y divide-gray-100 text-xs text-gray-500"> + {updatesText} + <div className="px-4 py-2 flex items-center gap-1"> + <NewspaperIcon className="h-4 w-4 text-gray-500" /> + <a + className="hover:text-primary-500 underline" + href={sysInfo.sysInfo.releaseNotesUrl} + target="_blank" + rel="noreferrer noopener" + > + Changelog + </a> + </div> + <div className="px-4 py-2 flex items-center gap-1"> + <CalendarIcon className="h-4 w-4 text-gray-500" /> + <span>Last update:</span> + <span> + {/* {updateTime.getDate()}.{updateTime.getMonth()} {updateTime.getFullYear()} */} + {updateTime.toLocaleDateString('en-uk', { day: 'numeric', year: 'numeric', month: 'short' })} + </span> + </div> + </div> + </Popover.Panel> + </Transition> + </Popover> + ); +}; diff --git a/frontend/src/modules/dashboard/components/VersionInfo/index.ts b/frontend/src/modules/dashboard/components/VersionInfo/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..d573df80cdd98623869aadf0b1e303a3d0bba112 --- /dev/null +++ b/frontend/src/modules/dashboard/components/VersionInfo/index.ts @@ -0,0 +1 @@ +export { VersionInfo } from './VersionInfo'; diff --git a/frontend/src/modules/dashboard/components/index.ts b/frontend/src/modules/dashboard/components/index.ts index 2a1468292e7106bd4f50c42b37e8f42a27ed45d4..db323609b13ba45e746f672df221d368a67cb949 100644 --- a/frontend/src/modules/dashboard/components/index.ts +++ b/frontend/src/modules/dashboard/components/index.ts @@ -1,2 +1,4 @@ export { DashboardCard } from './DashboardCard'; export { DashboardUtility } from './DashboardUtility'; +export { UpdateAlert } from './UpdateAlert'; +export { VersionInfo } from './VersionInfo'; diff --git a/frontend/src/modules/users/Users.tsx b/frontend/src/modules/users/Users.tsx index 5d45dd526be0e7668e3d241c1bafb72e3b7108be..157b4cda9e61e2fe753134413a401a20551ce7e3 100644 --- a/frontend/src/modules/users/Users.tsx +++ b/frontend/src/modules/users/Users.tsx @@ -11,6 +11,7 @@ import { useUsers } from 'src/services/users'; import { Table } from 'src/components'; import { debounce } from 'lodash'; import { useAuth } from 'src/services/auth'; +import { useApps } from 'src/services/apps'; import { UserModal } from '../../components/UserModal'; import { MultipleUsersModal } from './components'; @@ -24,6 +25,12 @@ export const Users: React.FC = () => { const { users, loadUsers, userTableLoading } = useUsers(); const { isAdmin } = useAuth(); + const { apps, loadApps } = useApps(); + useEffect(() => { + loadApps(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleSearch = (event: any) => { setSearch(event.target.value); }; @@ -182,9 +189,17 @@ export const Users: React.FC = () => { </div> {configureModal && ( - <UserModal open={configureModal} onClose={configureModalClose} userId={userId} setUserId={setUserId} /> + <UserModal + open={configureModal} + onClose={configureModalClose} + userId={userId} + setUserId={setUserId} + apps={apps} + /> + )} + {multipleUsersModal && ( + <MultipleUsersModal open={multipleUsersModal} onClose={multipleUsersModalClose} apps={apps} /> )} - {multipleUsersModal && <MultipleUsersModal open={multipleUsersModal} onClose={multipleUsersModalClose} />} </div> </div> ); diff --git a/frontend/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx b/frontend/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx index 74f60c692ec53fc6347ea35a1ced1db40e22b59d..65b92c7348b1854f7d399aacf52d63be2eaee1b4 100644 --- a/frontend/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx +++ b/frontend/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx @@ -5,17 +5,40 @@ import { useFieldArray, useForm, useWatch } from 'react-hook-form'; import { Banner, StepsModal, ProgressSteps } from 'src/components'; import { Select, TextArea } from 'src/components/Form'; import { MultipleUsersData, UserRole, useUsers } from 'src/services/users'; -import { allAppAccessList } from 'src/components/UserModal/consts'; +import { AppStatusEnum } from 'src/services/apps'; import { ProgressStepInfo, ProgressStepStatus } from 'src/components/ProgressSteps/types'; -import { initialMultipleUsersForm, MultipleUsersModalProps } from './types'; +import { MultipleUsersModalProps } from './types'; -export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) => { +export const MultipleUsersModal = ({ open, onClose, apps }: MultipleUsersModalProps) => { const [steps, setSteps] = useState<ProgressStepInfo[]>([]); const [isAdminRoleSelected, setAdminRoleSelected] = useState(false); const { createUsers, userModalLoading } = useUsers(); + // build initial app role list programmatially, + // pulling from list of installed apps on cluster + interface AppListInt { + name: string; + role: UserRole; + } + const appList: AppListInt[] = []; + const initialAppRoleLatest = () => { + apps + .filter((app) => app.status !== AppStatusEnum.NotInstalled) + .map((app) => + app.slug === 'monitoring' + ? appList.push({ name: app.slug, role: UserRole.NoAccess }) + : appList.push({ name: app.slug, role: UserRole.User }), + ); + }; + initialAppRoleLatest(); + + const initialMultipleUsersFormNew = { + appRoles: appList, + }; + + // populate the initial "New User" window with installed apps and default roles const { control, handleSubmit } = useForm<MultipleUsersData>({ - defaultValues: initialMultipleUsersForm, + defaultValues: initialMultipleUsersFormNew, }); const { fields, update } = useFieldArray({ @@ -87,11 +110,11 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) = <div className="flex-shrink-0 flex-1 flex items-center"> <img className="h-10 w-10 rounded-md overflow-hidden" - src={_.find(allAppAccessList, ['name', item.name!])?.image} + src="/assets/logo-small.svg" alt={item.name ?? 'Image'} /> <h3 className="ml-4 text-md leading-6 font-medium text-gray-900"> - {_.find(allAppAccessList, ['name', item.name!])?.label} + {_.find(apps, ['slug', item.name!])?.name} </h3> </div> <div className="sm:col-span-2"> @@ -120,11 +143,11 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) = <div className="flex-shrink-0 flex-1 flex items-center"> <img className="h-10 w-10 rounded-md overflow-hidden" - src={_.find(allAppAccessList, ['name', item.name!])?.image} + src={_.find(apps, ['slug', item.name!])?.assetSrc} alt={item.name ?? 'Image'} /> <h3 className="ml-4 text-md leading-6 font-medium text-gray-900"> - {_.find(allAppAccessList, ['name', item.name!])?.label} + {_.find(apps, ['slug', item.name!])?.name} </h3> </div> <div className="sm:col-span-2"> diff --git a/frontend/src/modules/users/components/MultipleUsersModal/types.ts b/frontend/src/modules/users/components/MultipleUsersModal/types.ts index 643f10cc604b86b9e8a5222db4863299f61fa042..960840b594f12c0dc7805638a69471430d916a3c 100644 --- a/frontend/src/modules/users/components/MultipleUsersModal/types.ts +++ b/frontend/src/modules/users/components/MultipleUsersModal/types.ts @@ -1,10 +1,12 @@ -import { initialAppRoles } from 'src/components/UserModal/consts'; +// import { initialAppRoles } from 'src/components/UserModal/consts'; +import { App } from 'src/services/apps'; export type MultipleUsersModalProps = { open: boolean; onClose: () => void; + apps: App[]; }; -export const initialMultipleUsersForm = { - appRoles: initialAppRoles, -}; +// export const initialMultipleUsersForm = { +// appRoles: initialAppRoles, +// }; diff --git a/frontend/src/redux/store.ts b/frontend/src/redux/store.ts index 8e1c58d7e4fe763ff66996181348cd2c0c8db0e4..598fcfdef1f26db0068e9f1574b4974698c81ff3 100644 --- a/frontend/src/redux/store.ts +++ b/frontend/src/redux/store.ts @@ -1,4 +1,4 @@ -import { createStore, compose, applyMiddleware, combineReducers } from 'redux'; +import { legacy_createStore as createStore, compose, applyMiddleware, combineReducers } from 'redux'; import thunkMiddleware from 'redux-thunk'; import { persistStore, persistReducer } from 'redux-persist'; import storage from 'redux-persist/lib/storage'; @@ -7,6 +7,7 @@ import { reducer as authReducer } from 'src/services/auth'; import usersReducer from 'src/services/users/redux/reducers'; import appsReducer from 'src/services/apps/redux/reducers'; +import sysInfoReducer from 'src/services/sysInfo/redux/reducers'; import { State } from './types'; const persistConfig = { @@ -19,6 +20,7 @@ const appReducer = combineReducers<State>({ auth: authReducer, users: usersReducer, apps: appsReducer, + sysInfo: sysInfoReducer, }); const persistedReducer = persistReducer(persistConfig, appReducer); diff --git a/frontend/src/redux/types.ts b/frontend/src/redux/types.ts index 85057964407d1039c485391f99b46fd23b9ca76f..c958fc6c056f9db5cc008cc317e3feff611327d9 100644 --- a/frontend/src/redux/types.ts +++ b/frontend/src/redux/types.ts @@ -3,6 +3,7 @@ import { Store } from 'redux'; import { AuthState } from 'src/services/auth/redux'; import { UsersState } from 'src/services/users/redux'; import { AppsState } from 'src/services/apps/redux'; +import { SysInfoState } from 'src/services/sysInfo/redux/types'; export interface AppStore extends Store, State {} @@ -10,4 +11,5 @@ export interface State { auth: AuthState; users: UsersState; apps: AppsState; + sysInfo: SysInfoState; } diff --git a/frontend/src/services/api/redux/actions.ts b/frontend/src/services/api/redux/actions.ts index 8f1cfa6e9cd3ca3055933f03b6f758b7f781c382..b813792f2cbde481947b3a28cd4f5bbae64c0cc8 100644 --- a/frontend/src/services/api/redux/actions.ts +++ b/frontend/src/services/api/redux/actions.ts @@ -24,7 +24,7 @@ export const createApiAction = (apiConfig: ApiConfig, actionTypes: string[]) => payload, }); - const failureAction = (error: string): FailureAction => ({ + const failureAction = (error: any): FailureAction => ({ type: actionTypes[2], payload: { error }, }); diff --git a/frontend/src/services/apps/transformations.ts b/frontend/src/services/apps/transformations.ts index 6b98a878cd2190ff4021a2aa498c6070042c6be6..cc8b2538ed39c909de12e1b6dad4b7508edb26fd 100644 --- a/frontend/src/services/apps/transformations.ts +++ b/frontend/src/services/apps/transformations.ts @@ -1,21 +1,29 @@ import { App, AppStatus, AppStatusEnum } from './types'; -const transformAppStatus = (status: AppStatus) => { +const transformAppStatus = (ext: boolean, status: AppStatus) => { + if (ext) return AppStatusEnum.External; if (status.installed && status.ready) return AppStatusEnum.Installed; if (status.installed && !status.ready) return AppStatusEnum.Installing; return AppStatusEnum.NotInstalled; }; export const transformApp = (response: any): App => { + const assetSlug = !response.external + ? `/assets/${response.slug}.svg` + : `/custom/assets/${response.slug.replace('ext-', '')}.svg`; + const markdownSlug = !response.external + ? `/markdown/${response.slug}.md` + : `/custom/markdown/${response.slug.replace('ext-', '')}.md`; return { id: response.id ?? '', name: response.name ?? '', slug: response.slug ?? '', - status: transformAppStatus(response.status), + external: response.external ?? '', + status: transformAppStatus(response.external, response.status), url: response.url, automaticUpdates: response.automatic_updates, - assetSrc: `/assets/${response.slug}.svg`, - markdownSrc: `/markdown/${response.slug}.md`, + assetSrc: assetSlug, + markdownSrc: markdownSlug, }; }; diff --git a/frontend/src/services/apps/types.ts b/frontend/src/services/apps/types.ts index 9c5e0e22ca1ecaca969c6e111d4d281a20678459..a7e05c34707e46d16daad658ff356a1a026f43e1 100644 --- a/frontend/src/services/apps/types.ts +++ b/frontend/src/services/apps/types.ts @@ -2,6 +2,7 @@ export interface App { id: number; name: string; slug: string; + external: boolean; status?: AppStatusEnum; url: string; automaticUpdates: boolean; diff --git a/frontend/src/services/sysInfo/api.ts b/frontend/src/services/sysInfo/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..d3df645b43269132ddea945203c41acf696eccd4 --- /dev/null +++ b/frontend/src/services/sysInfo/api.ts @@ -0,0 +1,11 @@ +import { performApiCall } from 'src/services/api'; + +// import { SysInfo } from './types'; +// import { transformSysInfo } from './transformations'; + +export const fetchSysInfo = async () => { + const res = await performApiCall({ + path: `/api/info`, + }); + return res.data; +}; diff --git a/frontend/src/services/sysInfo/hooks/index.ts b/frontend/src/services/sysInfo/hooks/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f897d5f84432d959094300c2748cdc2eb15dfc61 --- /dev/null +++ b/frontend/src/services/sysInfo/hooks/index.ts @@ -0,0 +1 @@ +export { useSysInfo } from './use-sysInfo'; diff --git a/frontend/src/services/sysInfo/hooks/use-sysInfo.ts b/frontend/src/services/sysInfo/hooks/use-sysInfo.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfe762ccd6f203b9eb8a4352ac12632b8456c3d7 --- /dev/null +++ b/frontend/src/services/sysInfo/hooks/use-sysInfo.ts @@ -0,0 +1,17 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { fetchSysInfo } from '../redux'; +import { getSysInfo } from '../redux/selectors'; + +export function useSysInfo() { + const dispatch = useDispatch(); + const sysInfo = useSelector(getSysInfo); + + function loadSysInfo() { + return dispatch(fetchSysInfo()); + } + + return { + sysInfo, + loadSysInfo, + }; +} diff --git a/frontend/src/services/sysInfo/index.ts b/frontend/src/services/sysInfo/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1c2b488d619105f90006e2e25ebbfeb4da600ffe --- /dev/null +++ b/frontend/src/services/sysInfo/index.ts @@ -0,0 +1,7 @@ +export * from './types'; + +export { reducer } from './redux'; + +export { useSysInfo } from './hooks'; + +export { fetchSysInfo } from './api'; diff --git a/frontend/src/services/sysInfo/redux/actions.ts b/frontend/src/services/sysInfo/redux/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..49c093b3de51a37f0a4583b8b33ea1a14f7d14ff --- /dev/null +++ b/frontend/src/services/sysInfo/redux/actions.ts @@ -0,0 +1,23 @@ +import { Dispatch } from 'redux'; +import { performApiCall } from 'src/services/api'; +// import { transformSysInfo } from '../transformations'; + +export enum SysInfoActionTypes { + FETCH_SYSINFO = 'info', +} + +export const fetchSysInfo = () => async (dispatch: Dispatch<any>) => { + try { + const { data } = await performApiCall({ + path: '/info', + method: 'GET', + }); + + dispatch({ + type: SysInfoActionTypes.FETCH_SYSINFO, + payload: data, + }); + } catch (err) { + console.error(err); + } +}; diff --git a/frontend/src/services/sysInfo/redux/index.ts b/frontend/src/services/sysInfo/redux/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e0c98162d970747fea58b6da67f4f3070cb525e2 --- /dev/null +++ b/frontend/src/services/sysInfo/redux/index.ts @@ -0,0 +1,4 @@ +export * from './actions'; +export { default as reducer } from './reducers'; +export { getSysInfo } from './selectors'; +// export * from './types'; diff --git a/frontend/src/services/sysInfo/redux/reducers.ts b/frontend/src/services/sysInfo/redux/reducers.ts new file mode 100644 index 0000000000000000000000000000000000000000..475f9087d134a8b70d83cabf2ceec2e70a6f4870 --- /dev/null +++ b/frontend/src/services/sysInfo/redux/reducers.ts @@ -0,0 +1,19 @@ +import { SysInfoActionTypes } from './actions'; + +const initialSysInfoState: any = { + sysInfo: {}, +}; + +const sysInfoReducer = (state: any = initialSysInfoState, action: any) => { + switch (action.type) { + case SysInfoActionTypes.FETCH_SYSINFO: + return { + ...state, + sysInfo: action.payload, + }; + default: + return state; + } +}; + +export default sysInfoReducer; diff --git a/frontend/src/services/sysInfo/redux/selectors.ts b/frontend/src/services/sysInfo/redux/selectors.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb741f573e778dcc4298919b6cf869f8abe4b210 --- /dev/null +++ b/frontend/src/services/sysInfo/redux/selectors.ts @@ -0,0 +1,3 @@ +import { State } from 'src/redux'; + +export const getSysInfo = (state: State) => state.sysInfo; diff --git a/frontend/src/services/sysInfo/redux/types.ts b/frontend/src/services/sysInfo/redux/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..71f23a764845bfb572bfeebe38451244ea0c6d2c --- /dev/null +++ b/frontend/src/services/sysInfo/redux/types.ts @@ -0,0 +1,5 @@ +import { SysInfo } from '../types'; + +export interface SysInfoState { + sysInfo: SysInfo; +} diff --git a/frontend/src/services/sysInfo/transformations.ts b/frontend/src/services/sysInfo/transformations.ts new file mode 100644 index 0000000000000000000000000000000000000000..54733c35d6b40b4d9ccd17548661eaa98c921ec6 --- /dev/null +++ b/frontend/src/services/sysInfo/transformations.ts @@ -0,0 +1,12 @@ +import { SysInfo } from './types'; + +export const transformSysInfo = (response: any): SysInfo => { + return { + lastRelease: response.lastRelease, + version: response.version, + appVersions: response.appVersions, + followingGit: response.followingGit, + releaseNotesUrl: response.releaseNotesUrl, + lastUpdated: response.lastUpdated, + }; +}; diff --git a/frontend/src/services/sysInfo/types.ts b/frontend/src/services/sysInfo/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..c91789de034e6849091678db1bfae004ab255125 --- /dev/null +++ b/frontend/src/services/sysInfo/types.ts @@ -0,0 +1,13 @@ +export interface SysInfo { + lastRelease: number; + version: number; + appVersions: AppVersions; + followingGit: string; + releaseNotesUrl: string; + lastUpdated: Date; +} + +export interface AppVersions { + name: string; + version: number; +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 2b8dfae196c8b4e3a4ffabd52fd1beffa9b0dbc1..9e2487c95b0ead06810f8852ee625f6dadea06c7 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,6 +1,5 @@ module.exports = { purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], - darkMode: false, // or 'media' or 'class' theme: { extend: { colors: { @@ -27,5 +26,9 @@ module.exports = { tableLayout: ['hover', 'focus'], }, }, - plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')], + plugins: [ + require('@tailwindcss/forms'), // eslint-disable-line + require('@tailwindcss/typography'), // eslint-disable-line + require('@headlessui/tailwindcss')({ prefix: 'ui' }), // eslint-disable-line + ], }; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8d610539847dd0a10a86529a0060897eb17f8758..92616b45cbf0ea6149aced0eea9bab148cb745a3 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1194,6 +1194,11 @@ resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.3.tgz#853c598ff47b37cdd192c5cbee890d9b610c3ec0" integrity sha512-LGp06SrGv7BMaIQlTs8s2G06moqkI0cb0b8stgq7KZ3xcHdH3qMP+cRyV7qe5x4XEW/IGY48BW4fLesD6NQLng== +"@headlessui/tailwindcss@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@headlessui/tailwindcss/-/tailwindcss-0.1.3.tgz#a9b8b4c2677a7ef37889708d4401c7871b2e6105" + integrity sha512-3aMdDyYZx9A15euRehpppSyQnb2gIw2s/Uccn2ELIoLQ9oDy0+9oRygNWNjXCD5Dt+w1pxo7C+XoiYvGcqA4Kg== + "@heroicons/react@^1.0.3": version "1.0.6" resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324" @@ -3461,6 +3466,11 @@ case-sensitive-paths-webpack-plugin@2.3.0: resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz#23ac613cc9a856e4f88ff8bb73bbb5e989825cf7" integrity sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ== +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -4895,6 +4905,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -5904,6 +5919,16 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +gray-matter@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" + integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + dependencies: + js-yaml "^3.13.1" + kind-of "^6.0.2" + section-matter "^1.0.0" + strip-bom-string "^1.0.0" + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -7740,6 +7765,11 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -7827,6 +7857,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-table@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" + integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -7854,6 +7889,16 @@ mdast-util-definitions@^5.0.0: "@types/unist" "^2.0.0" unist-util-visit "^4.0.0" +mdast-util-find-and-replace@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" + integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== + dependencies: + "@types/mdast" "^3.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.0.0" + mdast-util-from-markdown@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz#84df2924ccc6c995dec1e2368b2b208ad0a76268" @@ -7872,6 +7917,72 @@ mdast-util-from-markdown@^1.0.0: unist-util-stringify-position "^3.0.0" uvu "^0.5.0" +mdast-util-gfm-autolink-literal@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" + integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== + dependencies: + "@types/mdast" "^3.0.0" + ccount "^2.0.0" + mdast-util-find-and-replace "^2.0.0" + micromark-util-character "^1.0.0" + +mdast-util-gfm-footnote@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" + integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + micromark-util-normalize-identifier "^1.0.0" + +mdast-util-gfm-strikethrough@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" + integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm-table@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" + integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== + dependencies: + "@types/mdast" "^3.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm-task-list-item@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" + integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + +mdast-util-gfm@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" + integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-gfm-autolink-literal "^1.0.0" + mdast-util-gfm-footnote "^1.0.0" + mdast-util-gfm-strikethrough "^1.0.0" + mdast-util-gfm-table "^1.0.0" + mdast-util-gfm-task-list-item "^1.0.0" + mdast-util-to-markdown "^1.0.0" + +mdast-util-phrasing@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" + integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== + dependencies: + "@types/mdast" "^3.0.0" + unist-util-is "^5.0.0" + mdast-util-to-hast@^11.0.0: version "11.3.0" resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-11.3.0.tgz#ea9220617a710e80aa5cc3ac7cc9d4bb0440ae7a" @@ -7887,6 +7998,27 @@ mdast-util-to-hast@^11.0.0: unist-util-position "^4.0.0" unist-util-visit "^4.0.0" +mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" + integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^3.0.0" + mdast-util-to-string "^3.0.0" + micromark-util-decode-string "^1.0.0" + unist-util-visit "^4.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" + integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-string@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9" @@ -7953,7 +8085,7 @@ microevent.ts@~0.1.1: resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== -micromark-core-commonmark@^1.0.1: +micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad" integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA== @@ -7975,6 +8107,85 @@ micromark-core-commonmark@^1.0.1: micromark-util-types "^1.0.1" uvu "^0.5.0" +micromark-extension-gfm-autolink-literal@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.4.tgz#3a8af48264be47138654ab0b8700a8e22785ef07" + integrity sha512-WCssN+M9rUyfHN5zPBn3/f0mIA7tqArHL/EKbv3CZK+LT2rG77FEikIQEqBkv46fOqXQK4NEW/Pc7Z27gshpeg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-extension-gfm-footnote@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.0.tgz#73e3db823db9defef25f68074cb4cf4bb9cf6a8c" + integrity sha512-RWYce7j8+c0n7Djzv5NzGEGitNNYO3uj+h/XYMdS/JinH1Go+/Qkomg/rfxExFzYTiydaV6GLeffGO5qcJbMPA== + dependencies: + micromark-core-commonmark "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-strikethrough@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.5.tgz#4db40b87d674a6fe1d00d59ac91118e4f5960f12" + integrity sha512-X0oI5eYYQVARhiNfbETy7BfLSmSilzN1eOuoRnrf9oUNsPRrWOAe9UqSizgw1vNxQBfOwL+n2610S3bYjVNi7w== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-table@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.6.tgz#22b2b18dff9db39bdb29d6017e53bdd370672c8e" + integrity sha512-92pq7Q+T+4kXH4M6kL+pc8WU23Z9iuhcqmtYFWdFWjm73ZscFpH2xE28+XFpGWlvgq3LUwcN0XC0PGCicYFpgA== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm-tagfilter@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" + integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== + dependencies: + micromark-util-types "^1.0.0" + +micromark-extension-gfm-task-list-item@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.4.tgz#4b66d87847de40cef2b5ceddb9f9629a6dfe7472" + integrity sha512-9XlIUUVnYXHsFF2HZ9jby4h3npfX10S1coXTnV035QGPgrtNYQq3J6IfIvcCIUAJrrqBVi5BqA/LmaOMJqPwMQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-gfm@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" + integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== + dependencies: + micromark-extension-gfm-autolink-literal "^1.0.0" + micromark-extension-gfm-footnote "^1.0.0" + micromark-extension-gfm-strikethrough "^1.0.0" + micromark-extension-gfm-table "^1.0.0" + micromark-extension-gfm-tagfilter "^1.0.0" + micromark-extension-gfm-task-list-item "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + micromark-factory-destination@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e" @@ -10551,6 +10762,16 @@ relateurl@^0.2.7: resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== +remark-gfm@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" + integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-gfm "^2.0.0" + micromark-extension-gfm "^2.0.0" + unified "^10.0.0" + remark-parse@^10.0.0: version "10.0.1" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775" @@ -10951,6 +11172,14 @@ schema-utils@^3.0.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" +section-matter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" + integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + dependencies: + extend-shallow "^2.0.1" + kind-of "^6.0.0" + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -11557,6 +11786,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-bom-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" @@ -12224,6 +12458,14 @@ unist-util-stringify-position@^3.0.0: dependencies: "@types/unist" "^2.0.0" +unist-util-visit-parents@^5.0.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.1.tgz#868f353e6fce6bf8fa875b251b0f4fec3be709bb" @@ -13048,3 +13290,8 @@ yup@^0.32.9: nanoclone "^0.2.1" property-expr "^2.0.4" toposort "^2.0.2" + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/run_app.sh b/run_app.sh index 26e94e18936b32d7a93103ed8586bc24b0707900..fa4d0b3d68c172bd83c20c0af6c7be0d541985a8 100755 --- a/run_app.sh +++ b/run_app.sh @@ -1,7 +1,16 @@ #!/usr/bin/env bash +if [ -f "./backend/kubeconfig/kube_config_cluster.yml" ]; then + echo "Local KUBECONFIG configuration file found, applying custom configuration." + export KUBECONFIG=./backend/kubeconfig/kube_config_cluster.yml +else + echo "no Local KUBECONFIG configuration file found, skipping custom configuration." +fi + set -euo pipefail + + dockerComposeArgs=$@ export DATABASE_PASSWORD=$(kubectl get secret -n flux-system stackspin-single-sign-on-variables -o jsonpath --template '{.data.dashboard_database_password}' | base64 -d)