Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expose the cylc views standalone #376

Open
2 of 3 tasks
oliver-sanders opened this issue Jan 20, 2020 · 9 comments
Open
2 of 3 tasks

expose the cylc views standalone #376

oliver-sanders opened this issue Jan 20, 2020 · 9 comments
Milestone

Comments

@oliver-sanders
Copy link
Member

oliver-sanders commented Jan 20, 2020

This has been discussed before but @hjoliver made me aware we hadn't documented it anywhere.

The Cylc workflow views (tree, dot, graph, etc) and Cylc GScan should all be accessible via their own URL endpoints. This is currently true of the Graph view and should be fairly straight forward.

This is to allow the components to be used standalone for embedding and other purposes.

The main use case I'm aware of is that some users (namely operators) might want to have GScan open full-screen on one monitor.

Cylc views can be built to be responsive to make use of the available screen space.

Questions:

  • Use the workflow component to "serve" the standalone views?
  • Separate Lumino & the Workflow Component?
  • Change the workflow endpoint (to /#/workflow/tabs/<flow>)?

Pull requests welcome!

@oliver-sanders oliver-sanders added this to the 1.0 milestone Jan 20, 2020
@kinow
Copy link
Member

kinow commented Jan 21, 2020

I think there is a view that displays the Tree component too.

There is one for GScan but that name is confusing. I think that could be renamed actually to src/views/Workflows.vue, as that's used for the Workflows Table link from the Dashboard, and bound to the route /workflows in src/router/paths.js.

@kinow kinow mentioned this issue Jan 21, 2020
12 tasks
@hjoliver
Copy link
Member

hjoliver commented Feb 4, 2020

I did this for the treeview here: #378 ... but forgot to reference this issue.

So now we have:

  • #/workflows - workflows table view
  • #/workflows/toast - lumino tab view
  • #/tree/toast - tree view
  • #/graph/toast - graph view

What do we need to close this issue?

  • close it now?
  • just add a gscan-only route?
  • or some kind of general "view registration" mechanism that automatically creates routes to new views?

@hjoliver
Copy link
Member

hjoliver commented Feb 4, 2020

View registration with automatic route creation is probably overkill as we don't expect new views to be created very often!

@oliver-sanders
Copy link
Member Author

What do we need to close this issue?

  1. Definitely
  2. Would be nice, allows views as plugins

@oliver-sanders
Copy link
Member Author

Coming back to this after a discussion with @hjoliver about endpoints:

To close this issue properly, I think we need to separate the Lumino tabs layer from the Workflow Component. At the moment the standalone views are brokering their own subscriptions with the workflow service, they should go through the Workflow Component:

  • Nicer code.
  • Less duplication.
  • Easy to write [Cylc] views as plugins.

The architecture would look like this:

ui-arch

Note the URL endpoints, @hjoliver suggested "tabs" puts the standalone views at the same level as the workflow dashboard provided by Lumnio.

@oliver-sanders oliver-sanders mentioned this issue Feb 5, 2020
11 tasks
@oliver-sanders oliver-sanders added the question Flag this as a question for the next Cylc project meeting. label Feb 5, 2020
@kinow kinow mentioned this issue Jun 23, 2021
9 tasks
@kinow
Copy link
Member

kinow commented Jun 24, 2021

After the GScan + deltas PR is reviewed and merged, I think we should be able to have a simpler way to create views and add them to workflow lumino widgets with the following diff:

diff --git a/src/components/cylc/workflow/Toolbar.vue b/src/components/cylc/workflow/Toolbar.vue
index 6dd4da34..0385fea2 100644
--- a/src/components/cylc/workflow/Toolbar.vue
+++ b/src/components/cylc/workflow/Toolbar.vue
@@ -96,7 +96,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
             class="py-0 px-8 ma-0 c-add-view"
           >
             <v-list-item-icon>
-              <v-icon>{{ view.icon }}</v-icon>
+              <v-icon>{{ view.data().widget.icon }}</v-icon>
             </v-list-item-icon>
             <v-list-item-title>{{ view.name }}</v-list-item-title>
           </v-list-item>
@@ -124,8 +124,6 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 <script>
 import { mapGetters, mapState } from 'vuex'
 import {
-  mdiAppleKeyboardCommand,
-  mdiFileTree,
   mdiMicrosoftXboxControllerMenu,
   mdiPause,
   mdiPlay,
@@ -135,8 +133,6 @@ import {
 } from '@mdi/js'
 import toolbar from '@/mixins/toolbar'
 import WorkflowState from '@/model/WorkflowState.model'
-import TreeView from '@/views/Tree'
-import MutationsView from '@/views/Mutations'
 
 import {
   mutationStatus
@@ -144,11 +140,15 @@ import {
 
 export default {
   name: 'Toolbar',
-
+  props: {
+    views: {
+      type: Array,
+      required: true
+    }
+  },
   mixins: [
     toolbar
   ],
-
   data: () => ({
     extended: false,
     // FIXME: remove local state once we have this data in the workflow - https://github.com/cylc/cylc-ui/issues/221
@@ -164,21 +164,8 @@ export default {
       // store state from mutations in order to compute the "enabled" attrs
       paused: null,
       stop: null
-    },
-    views: [
-      {
-        name: TreeView.name,
-        icon: mdiFileTree
-
-      },
-      {
-        name: MutationsView.name,
-        icon: mdiAppleKeyboardCommand
-
-      }
-    ]
+    }
   }),
-
   computed: {
     ...mapState('app', ['title']),
     ...mapGetters('workflows', ['currentWorkflow']),
@@ -220,7 +207,6 @@ export default {
       }
     }
   },
-
   watch: {
     isPaused () {
       this.expecting.paused = null
@@ -229,7 +215,6 @@ export default {
       this.expecting.stop = null
     }
   },
-
   methods: {
     onClickReleaseHold () {
       const ret = this.$workflowService.mutate(
diff --git a/src/views/Mutations.vue b/src/views/Mutations.vue
index 6d782a4a..00f948e7 100644
--- a/src/views/Mutations.vue
+++ b/src/views/Mutations.vue
@@ -40,9 +40,9 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 <script>
 import gql from 'graphql-tag'
 import { getIntrospectionQuery as getGraphQLIntrospectionQuery, print } from 'graphql'
-
-import Mutation from '@/components/cylc/Mutation'
+import { mdiAppleKeyboardCommand } from '@mdi/js'
 import subscriptionViewMixin from '@/mixins/subscriptionView'
+import Mutation from '@/components/cylc/Mutation'
 
 export function associate (mutations, objects) {
   const associations = {}
@@ -117,6 +117,10 @@ export default {
       Job: [
         ['JobID', false]
       ]
+    },
+    widget: {
+      title: 'mutations',
+      icon: mdiAppleKeyboardCommand
     }
   }),
   computed: {
diff --git a/src/views/Tree.vue b/src/views/Tree.vue
index b6b279ab..746b9257 100644
--- a/src/views/Tree.vue
+++ b/src/views/Tree.vue
@@ -42,6 +42,7 @@ import SubscriptionQuery from '@/model/SubscriptionQuery.model'
 import TreeComponent from '@/components/cylc/tree/Tree'
 import CylcObjectMenu from '@/components/cylc/cylcObject/Menu'
 import { WORKFLOW_TREE_DELTAS_SUBSCRIPTION } from '@/graphql/queries'
+import { mdiFileTree } from '@mdi/js'
 
 export default {
   mixins: [
@@ -60,6 +61,14 @@ export default {
       title: this.getPageTitle('App.workflow', { name: this.workflowName })
     }
   },
+  data () {
+    return {
+      widget: {
+        title: 'tree',
+        icon: mdiFileTree
+      }
+    }
+  },
   computed: {
     ...mapState('workflows', ['workflow']),
     workflows () {
diff --git a/src/views/Workflow.vue b/src/views/Workflow.vue
index 51c5f20f..453a8470 100644
--- a/src/views/Workflow.vue
+++ b/src/views/Workflow.vue
@@ -19,6 +19,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   <div>
     <CylcObjectMenu />
     <toolbar
+      :views="views"
       v-on:add="this.addView"
     ></toolbar>
     <div class="workflow-panel fill-height">
@@ -28,24 +29,18 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
         tab-title-prop="tab-title"
       >
         <v-skeleton-loader
-          v-for="widgetId of treeWidgets"
-          :key="widgetId"
-          :id="widgetId"
+          v-for="(view, id) of widgets"
+          :key="id"
+          :id="id"
           :loading="isLoading"
+          :tab-title="view.data().widget.title"
           type="list-item-three-line"
-          tab-title="tree"
         >
-          <tree-view
+          <component
+            :is="view"
             :workflow-name="workflowName"
           />
         </v-skeleton-loader>
-        <mutations-view
-          v-for="widgetId of mutationsWidgets"
-          :key="widgetId"
-          :id="widgetId"
-          :workflow-name="workflowName"
-          tab-title="mutations"
-        />
       </lumino>
     </div>
   </div>
@@ -64,6 +59,7 @@ import Toolbar from '@/components/cylc/workflow/Toolbar'
 import CylcObjectMenu from '@/components/cylc/cylcObject/Menu'
 import MutationsView from '@/views/Mutations'
 import TreeView from '@/views/Tree'
+import WorkflowsTableView from '@/views/WorkflowsTable'
 import { mapState } from 'vuex'
 
 export default {
@@ -76,8 +72,6 @@ export default {
   components: {
     CylcObjectMenu,
     Lumino,
-    TreeView,
-    MutationsView,
     Toolbar
   },
   metaInfo () {
@@ -91,22 +85,22 @@ export default {
      *
      * @type {Object.<String, String>}
      */
-    widgets: {}
+    widgets: {},
+    views: [
+      TreeView,
+      MutationsView,
+      WorkflowsTableView
+    ]
   }),
+  created () {
+    // We need to load each view used by this view/component.
+    // See "local-registration" in Vue.js documentation.
+    this.views.forEach(view => {
+      this.$options.components[view.name] = view
+    })
+  },
   computed: {
-    ...mapState('workflows', ['workflow']),
-    treeWidgets () {
-      return Object
-        .entries(this.widgets)
-        .filter(([id, type]) => type === TreeView.name)
-        .map(([id, type]) => id)
-    },
-    mutationsWidgets () {
-      return Object
-        .entries(this.widgets)
-        .filter(([id, type]) => type === MutationsView.name)
-        .map(([id, type]) => id)
-    }
+    ...mapState('workflows', ['workflow'])
   },
   beforeRouteEnter (to, from, next) {
     next(vm => {
@@ -134,18 +128,17 @@ export default {
   methods: {
     /**
      * Add a new view widget.
+     *
+     * @type {String} viewName - the name of the view to be added (Vue component name).
      */
-    addView (view) {
-      switch (view) {
-      case TreeView.name:
-        Vue.set(this.widgets, createWidgetId(), TreeView.name)
-        break
-      case MutationsView.name:
-        Vue.set(this.widgets, createWidgetId(), MutationsView.name)
-        break
-      default:
-        throw Error(`Unknown view "${view}"`)
+    addView (viewName) {
+      const view = this.views
+        .filter(v => v.name === viewName)
+        .slice(0)[0]
+      if (!view) {
+        throw Error(`Unknown view "${viewName}"`)
       }
+      Vue.set(this.widgets, createWidgetId(), view)
       this.$nextTick(() => {
         // Views use navigation-guards to start the pending subscriptions. But we don't have
         // this in this view. We must pretend we are doing the beforeRouteEnter here, and
diff --git a/src/views/WorkflowsTable.vue b/src/views/WorkflowsTable.vue
index 13c31aa8..33e41bf1 100644
--- a/src/views/WorkflowsTable.vue
+++ b/src/views/WorkflowsTable.vue
@@ -29,7 +29,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
         md12
       >
           <v-alert
-            :icon="svgPath"
+            :icon="svgPath.table"
             prominent
             color="grey lighten-3"
           >
@@ -125,7 +125,13 @@ export default {
         value: 'port'
       }
     ],
-    svgPath: mdiTable
+    svgPath: {
+      table: mdiTable
+    },
+    widget: {
+      title: 'workflows table',
+      icon: mdiTable
+    }
   }),
   computed: {
     ...mapState('workflows', ['workflows']),

After that, we should be able to add any view as long as it

  • uses the subscription mixins if it uses GraphQL subscription data (not mandatory, the Mutations view doesn't have any mixin for GraphQL)
  • declares the widget title and icon
# file: Table.vue <- for the table view, for instance
import { mdiTable } from '@mdi/js'

export default {
...
  data () {
    return {
      widget: {
        title: 'table',
        icon: mdiTable
      }
    }
  }
...
}

And we would have to add it to the views/Workflow.vue data views array. That's it, two-steps and that's it.

@kinow kinow self-assigned this Jul 5, 2021
@kinow kinow modified the milestones: 1.0, 0.5.0 Jul 5, 2021
@kinow
Copy link
Member

kinow commented Jul 5, 2021

Should have a pull request tomorrow or Wednesday (have a performance review meeting tomorrow, and may need to write unit and/or e2e tests).

@kinow
Copy link
Member

kinow commented Jul 6, 2021

Copying a comment by @oliver-sanders with more things to be done as part of this ticket


@oliver-sanders let me know if you think there's more that needs to be done before we can close #376

I think there are three things left to do, we can bump them to their own issues

  • I think we should auto-generate the routing if/where possible so new views get their own pages automatically.
    (we might want to change the URL for the views to #/view/<view>[/<flow>])

  • The standalone "tree" and "workflows" views have the GScan sidebar which shouldn't be there
    (should only be one view per standalone page)

    FYI here are my thoughts on what we can achieve with the standalone views:

    • Operations centre have lots of screens, sometimes the would like one view-per-screen.
      (the big one for them is GScan, later on the workflow events view too)
    • When we convert the Cylc UIServer to a Jupyter Server extension I think this will allow us to expose Cylc views in Jupyter Lab via composition.
      (no cross-site scripting issues, both would be served from the same URL/server)
  • The GScan sidebar should be available standalone too.
    (we might want to give GScan the URL #/workflows and move the workflow table to something like #/workflow-table.

@oliver-sanders oliver-sanders modified the milestones: 0.5.0, 0.6.0 Jul 16, 2021
@kinow kinow removed their assignment Sep 10, 2021
@kinow kinow modified the milestones: 0.6.0, 1.0, 2.0 Sep 10, 2021
@oliver-sanders oliver-sanders modified the milestones: 2.0.0, Pending Jun 8, 2022
@oliver-sanders oliver-sanders removed the question Flag this as a question for the next Cylc project meeting. label Dec 22, 2022
@oliver-sanders
Copy link
Member Author

We are pretty much there on this one, just a couple of things remaining:

  • We should remove GScan from the standalone component views (tree, table, graph).
  • We should exposed GScan as a standalone component too.

Suggest:

  1. Renaming workflows to workspace (which is a closer description) - raised on element.
  2. Exposing GScan as workflows.

Note this commit splits GScan into view/component (similar to the tree & table views) which makes things a little easier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants