Skip to content

Commit

Permalink
Merge pull request cylc#1717 from markgrahamdawson/lumino-adv-graph
Browse files Browse the repository at this point in the history
Graph view: use `initialOptions` to save & restore view state on navigation
  • Loading branch information
MetRonnie authored May 2, 2024
2 parents 2e84cea + 22406cf commit 37636d4
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 57 deletions.
1 change: 1 addition & 0 deletions changes.d/1717.feat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
More view options are now remembered & restored when navigating between workflows.
162 changes: 105 additions & 57 deletions src/views/Graph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<boolean>}
*/
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<boolean>}
*/
const autoRefresh = useInitialOptions('autoRefresh', { props, emit }, true)
/**
* The node spacing state.
* @type {import('vue').Ref<number>}
*/
const spacing = useInitialOptions('spacing', { props, emit }, 1.5)
return {
jobTheme: useJobTheme(),
transpose,
autoRefresh,
spacing
}
},
Expand All @@ -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: [],
Expand All @@ -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<Graphviz>} */
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: [
Expand All @@ -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'
},
{
Expand All @@ -300,42 +364,7 @@ export default {
}
]
}
],
}
},
mounted () {
// compile & instantiate graphviz wasm
/** @type {Promise<Graphviz>} */
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)
]
}
},
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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()
}
}
}
}
Expand Down
61 changes: 61 additions & 0 deletions tests/e2e/specs/graph.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')
})
})

0 comments on commit 37636d4

Please sign in to comment.