diff --git a/changes.d/1717.feat.md b/changes.d/1717.feat.md new file mode 100644 index 000000000..7938e3f9b --- /dev/null +++ b/changes.d/1717.feat.md @@ -0,0 +1 @@ +More view options are now remembered & restored when navigating between workflows. \ No newline at end of file diff --git a/src/views/Graph.vue b/src/views/Graph.vue index 0bb4e41fc..ce6c9d655 100644 --- a/src/views/Graph.vue +++ b/src/views/Graph.vue @@ -98,6 +98,10 @@ import { getPageTitle } from '@/utils/index' import { useJobTheme } from '@/composables/localStorage' import graphqlMixin from '@/mixins/graphql' import subscriptionComponentMixin from '@/mixins/subscriptionComponent' +import { + initialOptions, + useInitialOptions +} from '@/utils/initialOptions' import SubscriptionQuery from '@/model/SubscriptionQuery.model' // import CylcTreeCallback from '@/services/treeCallback' import GraphNode from '@/components/cylc/GraphNode.vue' @@ -224,9 +228,34 @@ export default { } }, - setup () { + props: { initialOptions }, + + setup (props, { emit }) { + /** + * The transpose toggle state. + * If true layout is left-right, else top-bottom + * @type {import('vue').Ref} + */ + const transpose = useInitialOptions('transpose', { props, emit }, false) + + /** + * The auto-refresh toggle state. + * If true the graph layout will be updated on a timer + * @type {import('vue').Ref} + */ + const autoRefresh = useInitialOptions('autoRefresh', { props, emit }, true) + + /** + * The node spacing state. + * @type {import('vue').Ref} + */ + const spacing = useInitialOptions('spacing', { props, emit }, 1.5) + return { jobTheme: useJobTheme(), + transpose, + autoRefresh, + spacing } }, @@ -236,8 +265,6 @@ export default { orientation: 'TB', // the auto-refresh timer refreshTimer: null, - // the spacing between nodes - spacing: 1.5, // the nodes end edges we render to the graph graphNodes: [], graphEdges: [], @@ -249,13 +276,50 @@ export default { graphID: null, // instance of system which provides pan/zoom/navigation support panZoomWidget: null, - // if true layout is left-right is false it is top-bottom - transpose: false, - // if true the graph layout will be updated on a timer - autoRefresh: true, // true if layout is in progress updating: false, - controlGroups: [ + // supports loading graph when component is mounted and autoRefresh is off. + // true if page is loading for the first time and nodeDimensions are yet to be calculated + initialLoad: true, + } + }, + + mounted () { + // compile & instantiate graphviz wasm + /** @type {Promise} */ + this.graphviz = Graphviz.load() + // allow render to happen before we go configuring svgPanZoom + this.$nextTick(() => { + this.refresh() + this.updateTimer() + }) + this.mountSVGPanZoom() + }, + + beforeUnmount () { + clearInterval(this.refreshTimer) + }, + + computed: { + ...mapGetters('workflows', ['getNodes']), + query () { + return new SubscriptionQuery( + QUERY, + this.variables, + 'workflow', + [], + /* isDelta */ true, + /* isGlobalCallback */ true + ) + }, + workflowIDs () { + return [this.workflowId] + }, + workflows () { + return this.getNodes('workflow', this.workflowIDs) + }, + controlGroups () { + return [ { title: 'Graph', controls: [ @@ -270,14 +334,14 @@ export default { title: 'Auto Refresh', icon: mdiTimer, action: 'toggle', - value: true, + value: this.autoRefresh, key: 'autoRefresh' }, { title: 'Transpose', icon: mdiFileRotateRight, action: 'toggle', - value: false, + value: this.transpose, key: 'transpose' }, { @@ -300,42 +364,7 @@ export default { } ] } - ], - } - }, - - mounted () { - // compile & instantiate graphviz wasm - /** @type {Promise} */ - this.graphviz = Graphviz.load() - // allow render to happen before we go configuring svgPanZoom - this.$nextTick(() => { - this.updateTimer() - }) - this.mountSVGPanZoom() - }, - - beforeUnmount () { - clearInterval(this.refreshTimer) - }, - - computed: { - ...mapGetters('workflows', ['getNodes']), - query () { - return new SubscriptionQuery( - QUERY, - this.variables, - 'workflow', - [], - /* isDelta */ true, - /* isGlobalCallback */ true - ) - }, - workflowIDs () { - return [this.workflowId] - }, - workflows () { - return this.getNodes('workflow', this.workflowIDs) + ] } }, @@ -394,7 +423,9 @@ export default { }, updateTimer () { // turn the timer on or off depending on the value of autoRefresh - if (this.autoRefresh) { + // if initialLoad is true we want to set a refresh interval + // regardles of autoRefresh state. + if (this.autoRefresh || this.initialLoad) { this.refreshTimer = setInterval(this.refresh, 2000) } else { clearInterval(this.refreshTimer) @@ -517,7 +548,7 @@ export default { // generate a hash for this list of nodes and edges return nonCryptoHash( nodes.map(n => n.id).reduce((x, y) => { return x + y }) + - edges.map(n => n.id).reduce((x, y) => { return x + y }, 1) + (edges || []).map(n => n.id).reduce((x, y) => { return x + y }, 1) ) }, reset () { @@ -558,10 +589,13 @@ export default { this.updating = true // extract the graph (non reactive lists of nodes & edges) - const nodes = this.getGraphNodes() + const nodes = await this.waitFor(() => { + const nodes = this.getGraphNodes() + return nodes.length ? nodes : false + }) const edges = this.getGraphEdges() - if (!nodes.length) { + if (!nodes || !nodes.length) { // we can't graph this, reset and wait for something to draw this.graphID = null this.updating = false @@ -592,16 +626,24 @@ export default { // obtain the node dimensions to use in the layout // NOTE: need to wait for the nodes to all be rendered before we can // measure them - let nodeDimensions - await this.waitFor(() => { + const nodeDimensions = await this.waitFor(() => { try { - nodeDimensions = this.getNodeDimensions(nodes) - return true // all nodes rendered + return this.getNodeDimensions(nodes) // all nodes rendered } catch { return false // one or more nodes awaiting render } }) + // if autoRefresh is off on page load no graph will be rendered. + // we let the page refresh on initial load + // once nodeDimensions have rendered for the first time + // we prevent further refreshing by setting initialLoad to false + if (nodeDimensions) { + if (this.initialLoad) { this.initialLoad = false } + } else { + return + } + // layout the graph try { await this.layout(nodes, edges, nodeDimensions) @@ -634,9 +676,8 @@ export default { // Will return when the callback returns something truthy. // OR after the configured number of retries for (let retry = 0; retry < retries; retry++) { - if (callback()) { - break - } + const ret = callback() + if (ret) return ret await new Promise(requestAnimationFrame) await this.$nextTick() } @@ -693,6 +734,13 @@ export default { autoRefresh () { // toggle the timer when autoRefresh is changed this.updateTimer() + }, + initialLoad () { + // when initialLoad changes from true to false + // do a final refresh + if (!this.autoRefresh) { + this.updateTimer() + } } } } diff --git a/tests/e2e/specs/graph.cy.js b/tests/e2e/specs/graph.cy.js index 5f30113f7..c12ee1f57 100644 --- a/tests/e2e/specs/graph.cy.js +++ b/tests/e2e/specs/graph.cy.js @@ -37,6 +37,31 @@ function checkGraphLayoutPerformed ($el, depth = 0) { } } +function addView (view) { + cy.get('[data-cy=add-view-btn]').click() + cy.get(`#toolbar-add-${view}-view`).click() + // wait for menu to close + .should('not.be.exist') +} + +function checkRememberToolbarSettings (selector, stateBefore, stateAfter) { + cy + .get(selector) + .find('.v-btn') + .should(stateBefore, 'text-blue') + .click() + // Navigate away + cy.visit('/#/') + cy.get('.c-dashboard') + // Navigate back + cy.visit('/#/workspace/one') + waitForGraphLayout() + cy + .get(selector) + .find('.v-btn') + .should(stateAfter, 'text-blue') +} + describe('Graph View', () => { it('should load', () => { cy.visit('/#/graph/one') @@ -69,4 +94,40 @@ describe('Graph View', () => { .should('have.length', 10) .should('be.visible') }) + + it('loads graph when switching between workflows', () => { + cy.visit('/#/workspace/one') + addView('Graph') + waitForGraphLayout() + cy.visit('/#/workspace/other/multi/run2') + addView('Graph') + waitForGraphLayout() + cy + // there should be 2 graph nodes (all on-screen) + .get('.c-graph:first') + .find('.graph-node-container') + .should('be.visible') + .should('have.length', 2) + cy.visit('/#/workspace/one') + cy + // there should be 7 graph nodes (all on-screen) + .get('.c-graph:first') + .find('.graph-node-container') + .should('be.visible') + .should('have.length', 7) + }) + + it('remembers autorefresh setting when switching between workflows', () => { + cy.visit('/#/workspace/one') + addView('Graph') + waitForGraphLayout() + checkRememberToolbarSettings('[data-cy=control-autoRefresh]', 'have.class', 'not.have.class') + }) + + it('remembers transpose setting when switching between workflows', () => { + cy.visit('/#/workspace/one') + addView('Graph') + waitForGraphLayout() + checkRememberToolbarSettings('[data-cy=control-transpose]', 'not.have.class', 'have.class') + }) })