{"id":21646736,"url":"https://github.com/cylc/cylc-ui","last_synced_at":"2025-04-09T15:07:39.057Z","repository":{"id":37735004,"uuid":"158632466","full_name":"cylc/cylc-ui","owner":"cylc","description":"Web app for monitoring and controlling Cylc workflows","archived":false,"fork":false,"pushed_at":"2025-04-09T09:56:07.000Z","size":35728,"stargazers_count":37,"open_issues_count":225,"forks_count":29,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-09T15:07:37.094Z","etag":null,"topics":["cylc","dashboard","hacktoberfest","javascript","vue","vuejs"],"latest_commit_sha":null,"homepage":"https://cylc.github.io","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cylc.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-11-22T02:35:32.000Z","updated_at":"2025-04-09T09:55:24.000Z","dependencies_parsed_at":"2023-09-23T07:21:29.063Z","dependency_job_id":"802acb61-e24a-4b17-bf25-e4fb89cb0bb4","html_url":"https://github.com/cylc/cylc-ui","commit_stats":{"total_commits":2015,"total_committers":21,"mean_commits":95.95238095238095,"dds":0.3662531017369727,"last_synced_commit":"9679dc650b2ea2372621f698c16ffd09d6cd0397"},"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cylc%2Fcylc-ui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cylc%2Fcylc-ui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cylc%2Fcylc-ui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cylc%2Fcylc-ui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cylc","download_url":"https://codeload.github.com/cylc/cylc-ui/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248055284,"owners_count":21040157,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cylc","dashboard","hacktoberfest","javascript","vue","vuejs"],"created_at":"2024-11-25T06:46:29.692Z","updated_at":"2025-04-09T15:07:39.036Z","avatar_url":"https://github.com/cylc.png","language":"JavaScript","funding_links":[],"categories":["Building"],"sub_categories":["Workflows"],"readme":"[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/cylc/cylc-ui)](https://github.com/cylc/cylc-ui/releases)\n[![Build Status](https://github.com/cylc/cylc-ui/actions/workflows/main.yml/badge.svg?branch=master)](https://github.com/cylc/cylc-ui/actions/workflows/main.yml?query=branch%3Amaster)\n[![codecov](https://codecov.io/gh/cylc/cylc-ui/branch/master/graph/badge.svg)](https://codecov.io/gh/cylc/cylc-ui)\n\n# Cylc UI\n\n## Installation\n\nInstall the [UI Server](https://github.com/cylc/cylc-uiserver) which bundles\nthe UI.\n\n## Copyright and Terms of Use\n\nCopyright (C) 2018-\u003cspan actions:bind='current-year'\u003e2025\u003c/span\u003e NIWA \u0026 British Crown (Met Office) \u0026 Contributors.\n\nCylc is free software: you can redistribute it and/or modify it under the terms\nof the GNU General Public License as published by the Free Software Foundation,\neither version 3 of the License, or (at your option) any later version.\n\nCylc is distributed in the hope that it will be useful, but WITHOUT ANY\nWARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A\nPARTICULAR PURPOSE.  See the GNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License along with\nCylc.  If not, see [GNU licenses](http://www.gnu.org/licenses/).\n\n## Development\n\n### Install \u0026 Build\n\n```bash\n# Project setup\nyarn install\n\n# start dev server in offline mode (uses mock data, auto-updates the browser page on change)\nyarn run serve\n\n# pass options to vite (e.g. to use a different port or expose host)\nVITE_OPTIONS='--host myhost' yarn run serve\n\n# build for production\nyarn run build\n\n# build for development (rebuilds on change)\nyarn run build:watch\n# and launch using\ncylc gui --ui-build-dir=\u003ccylc-ui-path\u003e/dist/\n\n# start dev server in offline mode, using the build instead of source files\nyarn run preview\n```\n\nNote the incremental rebuild is quite slow so an alternative to `yarn run build:watch` is\nto run the Vite development server while using the Cylc UI Server live data:\n\n```bash\n# First launch the gui to authenticate with the URL token\ncylc gui --port=3000 --ServerApp.allow_origin='http://localhost:5173'\n# Close that tab once it's loaded\n# Now launch using\nyarn run serve:vue --mode development\n# (you must access via http://localhost:5173)\n```\n\n### Tests\n\nThere are three groups of tests:\n\n* Unit tests\n  * Simple unit tests for individual functions and classes.\n  * **Framework:** [Vitest](https://vitest.dev/)\n  * **Assertions:** [Chai](https://www.chaijs.com/)/[Vitest](https://vitest.dev/api/expect.html)\n  * **Path:** `tests/unit`\n  * **Command:** `yarn run test:unit` (watches by default, only re-runs changed file)\n    * (To prevent watching, use `yarn vitest run`)\n* Component tests\n  * In-browser tests which mount a single Vue component standalone.\n  * **Framework:** [Cypress](https://docs.cypress.io/guides/overview/why-cypress)\n  * **Assertions:** Chai\n  * **Path:** `tests/component`\n  * **Command:** `yarn run test:component`\n    * (For \"headless\" mode use `yarn cypress run --component --config video=false`)\n* End to end tests\n  * In-browser tests which load entire pages of the UI using mocked data.\n  * **Framework:** Cypress\n  * **Assertions:** Chai\n  * **Path:** `tests/e2e`\n  * **Command:** `yarn run test:e2e`\n    * (For \"headless\" mode use `yarn run serve cy:run`)\n\nFor coverage:\n```bash\nyarn run coverage:unit\nyarn run coverage:e2e\n```\n\n### Mocked Data\n\nThe \"offline\" mode (aka `yarn run serve`) which is also used for the end to end\ntests is powered by a \"mock\" data server.\n\nYou can find the index of mocked data here:\n[`src/services/mock/json/index.cjs`](src/services/mock/json/index.cjs)\n\nMock data is automatically loaded when the subscription/query issued matches\nan entry in that file.\n\n### Code Style\n\nSee [`.eslintrc.cjs`](.eslintrc.cjs) for style. To test, run:\n\n```bash\nyarn run lint\n```\n\nOr to lint a particular file/directory:\n\n```bash\nyarn eslint path/to/file\n```\n\n### Project Setup\n\nWe are using [Vue](https://vuejs.org/).\nThe project was originally created with [vue-cli](https://cli.vuejs.org/), but\nhas switched to [Vite](https://vitejs.dev/) with the upgrade from Vue 2 to 3.\n\nThe configuration for how the app is served and built is defined in\n[`vite.config.js`](vite.config.js).\n\nWe are currently using the [Vuetify component library](https://vuetifyjs.com/en/).\nIts configuration is defined in [`src/plugins/vuetify.js`](src/plugins/vuetify.js).\n\nWe use [concurrently](https://github.com/open-cli-tools/concurrently) for\nconcurrently running the mock data json-server and the Vite dev server, and\nalso Cypress. This is configured in [`scripts/concurrently.cjs`](scripts/concurrently.cjs).\n\n### Browser compatibility\n\nThere are two aspects of browser compatibility:\n- ECMAScript syntax (e.g. does the browser support the optional chaining\noperator (`?.`)?)\n- API calls (e.g. does the browser support `Array.prototype.at()`?)\n\nThe former is [handled by Vite](https://vitejs.dev/guide/build.html#browser-compatibility).\nIt uses [esbuild](https://esbuild.github.io/api/#target) to transform instances\nof newer syntax when building.\n\nHowever, new APIs are not handled and must be polyfilled if deemed necessary.\n\nWe define a specification for browser compatibility in\n[`.browserslistrc`](.browserslistrc). See https://github.com/browserslist/browserslist.\n- We are not currently using it for the Vite/esbuild configuration because the\ndefault is good enough (but we could do in future using a plugin such as\n[esbuild-plugin-browserslist](https://www.npmjs.com/package/esbuild-plugin-browserslist)).\n- For polyfilling newer APIs, we could use\n[Babel + core-js](https://babeljs.io/docs/babel-preset-env#usebuiltins) which\nuses the browserslist specification. Or perhaps the simplest solution is to\nuse [polyfill.io](https://polyfill.io/v3/) which merely requires adding a\n`\u003cscript\u003e` tag to [`index.html`](index.html) which will fetch the listed\npolyfills only if needed by the user's browser. We could even leave it up to\nsites to patch their Cylc UI builds with the polyfills they require.\n\nRemember it is not just our source code that must meet our back-compat\nspecification, but our bundled dependencies (e.g.\n[Vuetify](https://vuetifyjs.com/en/getting-started/browser-support/)) too!\nVite/esbuild handles syntax for bundled dependencies.\n\nHowever the bottom line is that as of 2023, browser support is much less of an\nissue than it was even a couple of years ago, due to the proliferation of\nevergreen browsers. The only real concern is bleeding-edge API calls creeping\ninto our source code or runtime dependencies. To catch this, we are using\n[eslint-plugin-compat](https://github.com/amilajack/eslint-plugin-compat)\nin CI to scan the build for any such API calls.\n\n### Integration with the backend Cylc UI server\n\nRunning `yarn run build[:watch]` outputs the build into the `./dist/` folder.\nWhen running the Cylc Hub, you must remember to point\nthe static files directory to the location of your `./dist` folder.\n\nThis way with both Cylc Hub and Cylc UI running, you can work on either -\nor both - projects. Changes done in your Tornado application should reflect immediately\nor upon process restart. While the changes done in your Vue.js application\nwill be automatically handled by your `build:watch` command.\n\n### Internationalization\n\n\u003e [!NOTE]\n\u003e Internationalization is only partly implemented at the moment.\n\nThis project utilizes [vue-i18n](https://kazupon.github.io/vue-i18n/) for\ninternationalization. While this project is not part of Vue.js, it is maintained\nby one of the Vue.js core developers.\n\nMessages for internationalization are kept in JSON files. Look at\n`src/lang/` for each locale. For example, for British English, the message\nfiles are kept under `src/lang/en-GB`.\n\nThe locale is defined by a variable `$i18n`, which is accessible in each\ncomponent. So in a component you should be able to change the locale -\nif necessary - by calling `this.$i18n.locale = 'pt-BR'`.\n\n### Accessibility\n\nAfter applying changes to the code, might be a good idea to pass the new version of\nthe application through an accessibility tool such as [WAVE](https://wave.webaim.org/).\n\nThere is also a [browser](https://wave.webaim.org/extension/) extension which makes\ntesting the development version much easier.\n\n### TypeScript\n\nTypeScript is most likely the future for us. It can be adopted gradually.\nAt the moment we only have JSDoc comments which can provide type information\nin your IDE.\n\n## How The Data Is Provisioned\n\nThe Cylc UI connects to the GraphQL endpoint provided by the Cylc UI Server\nusing a websocket.\n\n### GraphQL Queries\n\nHere's the \"Hello World!\" of Cylc GraphQL queries, it returns the ID of every\nworkflow (under `~/cylc-run`):\n\n```graphql\nquery {\n  workflows {\n    id\n  }\n}\n```\n\n### GraphQL Subscriptions\n\nTo keep data up to date, we use subscriptions, a subscription is essentially a\nrepeating query. The way we've set it up, the server will only send new\nresponses when the data changes.\n\nThis subscription will send back the ID of every workflow, any time the list of\nworkflows changes (i.e. when you install a new workflow or clean an old one):\n\n```graphql\nsubscription {\n  workflows {\n    id\n  }\n}\n```\n\n### GraphQL Delta-Subscriptions\n\nTo avoid sending the entire list of workflow IDs every time the list changes\nwe subscribe to special \"delta\" objects. These allow us to track changes in\nthe list which is useful for efficiently synchronising data between the server\nand the web app.\n\nThis subscription will notify us when workflows are added, updated or removed:\n\n```graphql\nsubscription {\n  deltas {\n    added {\n      workflows {\n        id\n      }\n    }\n    updated {\n      workflows {\n        id\n      }\n    }\n    pruned {\n      id\n    }\n  }\n}\n```\n\n* The added-delta returns newly added data.\n* The updated-delta returns updated data.\n* The pruned-delta returns a list of IDs which have been removed.\n\n### How Views Request Data\n\nThe Cylc \"views\" (e.g. the Tree, Table and Graph views) define a subscription\nwhich defines **all** of the data they require.\n\nThis subscription is automatically registered with the WorkflowService when the\nview is loaded.\n\nThe WorkflowService will then issue and manage this subscription on your\nbehalf. The data you requested will become available in the data store\nwhen it arrives. The data store will be kept up to date whenever this data\nchanges.\n\nThe subscription will be cancelled when the view is closed.\n\n### Subscription Merging\n\nWhen a new view is opened, the WorkflowService will take the subscription for\nthis view merge it with any other active subscriptions from other views.\n\nE.G. If view-a has this subscription:\n\n```graphql\nsubscription {\n  deltas {\n    added {\n      taskProxies {\n        id\n        name\n        status\n        firstParent\n      }\n    }\n  }\n}\n```\n\nAnd view-b has this subscription:\n\n```graphql\nsubscription {\n  deltas {\n    added {\n      taskProxies {\n        id\n        status\n        isHeld\n        isRunahead\n        isQueued\n      }\n    }\n  }\n}\n```\n\nThen the WorkflowService will merge these subscriptions into:\n\n```graphql\nsubscription {\n  deltas {\n    added {\n      taskProxies {\n        id\n        name\n        status\n        firstParent\n        isHeld\n        isRunahead\n        isQueued\n      }\n    }\n  }\n}\n```\n\nThis is how we avoid requesting duplicate information about the same things for\ndifferent views.\n\nEach view can request whatever data it needs, however, filtering cannot be\nperformed in the subscription because that filtering would apply to all merged\nsubscriptions. If two subscriptions cannot be merged (e.g. different filtering\noptions) then an error will be raised. Perform filtering within the view\nwhere appropriate.\n\nWhen the UI Server sends deltas back to the WorkflowService, they are used to\nmaintain the data store.\n\nNote, ensure you request the `id` for **all** objects, the data store needs\nthis to operate.\n\n### The Data Store\n\nThe central data store contains all of the information requested by **all** of\nthe views. The WorkflowService keeps this up to date by applying the deltas it\nreceives from the UI Server to the store in order. Each delta is timestamped\nwhich allows us to detect transmission errors, the store will be rebuilt in the\nevent of error.\n\nThe data store is currently VueX but will probably be migrated to Pina in the\nfuture.\n\nThe data store contains an entry for every object requested where an object\nmight be a user, workflow, cycle-point, task or job. Every object has a unique\nID.\n\nFor example, a workflow in the data store might look like this:\n\n```js\n{\n  // the unique object ID as a string\n  id: '~me/my-workflow',\n  // the parsed ID as an object\n  tokens: {user: 'me', workflow: 'my-workflow, ...},\n  // the kind of node that this is\n  type: 'workflow',\n  // the last part of the ID\n  name: 'my-workflow',\n  // any data that has been requested\n  data: {\n    status: 'running',\n    host: 'myhost',\n    port: '1234',\n  },\n  // read on...\n  children: [],\n}\n```\n\nYou can access these objects via one of two ways, the index or the tree.\n\n#### The Index\n\nThis is a mapping which contains every object listed by its ID.\n\nE.G. something along the lines of:\n\n```js\n$index = {\n  '~me': Object,\n  '~me/my-workflow': Object,\n  '~me/my-workflow//cycle': Object,\n  '~me/my-workflow//cycle/task': Object,\n  '~me/my-workflow//cycle/task/job': Object,\n}\n```\n\nThis is useful if you know the ID of the object you want to retrieve.\n\n#### The Tree\n\nFor convenience, these nodes are also arranged into a hierarchy allowing you to\nwalk/iterate over them.\n\n* The children of a node are stored in `children`.\n* The ID of the parent node is stored in `parent`.\n\nE.G. the tree structure might look like this:\n\n* `~me`\n  * `workflow-one`\n    * `cycle-one`\n      * `task-a`\n        * `job-1`\n        * `job-2`\n      * `task-b`\n    * `cycle-one`\n      * `task-a`\n  * `workflow-two`\n\n### Family Hierarchies\n\nSome views (e.g. the Tree view) want access to the family hierarchy of tasks.\n\nE.G. for this workflow:\n\n```ini\n[runtime]\n  [A]\n  [[a1, a2]]\n    inherit = A\n```\n\nDefines this hierarchy:\n\n* root (implicit root family)\n  * A (user-defined family)\n    * a1 (task)\n    * a2 (task)\n\nIf you need to walk the family hierarchy down to a task (like the Tree view\ndoes), then add these fields to your `TaskProxy` subscription:\n\n```\nancestors\nFirstParent\n```\n\nThe data store will now automatically construct a family tree for you to\niterate in every Cycle object.\n\nAccess this using the `FamilyTree` property.\n\n### Edges \u0026 Namespaces\n\nSome views may require graph edges (i.e. the arrows in the Graph view).\n\nSome views may require namespaces (i.e. task definitions).\n\nThese are available via the `$edges` and `$namespaces` indexes which are\navailable on workflow objects.\n\n### Example View\n\nFor documentation on how to write a view see `src/views/SimpleTree.vue` which\ncontains a simple implementation of a minimal tree view.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcylc%2Fcylc-ui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcylc%2Fcylc-ui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcylc%2Fcylc-ui/lists"}