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

Auto-flatten single-child directories in GScan sidebar #1416

Merged
merged 17 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ ones in. -->
### Enhancements

[#1472](https://github.com/cylc/cylc-ui/pull/1472) -
Added GScan refresh button
Added refresh button to the sidebar.

[#1416](https://github.com/cylc/cylc-ui/pull/1416) -
Single-child directories in the sidebar are now flattened to reduce visual clutter.

### Fixes

Expand All @@ -27,11 +30,6 @@ Fix a couple of issues which could cause errors in the GUI.
[#1502](https://github.com/cylc/cylc-ui/pull/1502) -
Fixed bug where toggle buttons in view toolbars would not change state.

-------------------------------------------------------------------------------
## __cylc-ui-2.2.0 (<span actions:bind='release-date'>Upcoming</span>)__

### Fixes

[#1434](https://github.com/cylc/cylc-ui/pull/1434) -
Small accessibility/appearance improvements.

Expand Down Expand Up @@ -60,8 +58,8 @@ Add landing page for unauthorised users.
[#1269](https://github.com/cylc/cylc-ui/pull/1269) -
Upgraded Vue and Vuetify frameworks to v3.

[1240](https://github.com/cylc/cylc-ui/pull/1240) - Allow edit-runtime
for ``[root]`` family by clicking on Cycle icon.
[#1240](https://github.com/cylc/cylc-ui/pull/1240) - Allow edit-runtime
MetRonnie marked this conversation as resolved.
Show resolved Hide resolved
for `[root]` family by clicking on Cycle icon.

[#1345](https://github.com/cylc/cylc-ui/pull/1345) -
Added setting to choose the default view for workflows (tree, table etc.).
Expand Down
2 changes: 1 addition & 1 deletion src/components/cylc/cylcObject/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ export default {
*/
onClickOutside (e) {
this.closeMenu()
if (e.target?.classList.contains('c-interactive')) {
if (e.target?.getAttribute('data-c-interactive')) {
this.showMenu = true
}
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/cylc/cylcObject/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function bind (el, binding, vnode) {
})
}
el.addEventListener('click', listener)
el.classList.add('c-interactive')
el.dataset.cInteractive = true
listeners.set(el, listener)
}

Expand Down
121 changes: 5 additions & 116 deletions src/components/cylc/gscan/GScan.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,79 +101,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
v-if="!isLoading"
class="c-gscan-workflows flex-grow-1 pl-2"
>
<tree
<Tree
:filterable="false"
:expand-collapse-toggle="false"
:auto-collapse="true"
:workflows="workflows"
:stopOn="['workflow']"
:autoExpandTypes="['workflow-part', 'workflow']"
tree-item-component="GScanTreeItem"
class="c-gscan-workflow ma-0 pa-0"
ref="tree"
:indent="18"
>
<template v-slot:node="scope">
<workflow-icon
class="mr-2 flex-shrink-0"
v-if="scope.node.type === 'workflow'"
:status="scope.node.node.status"
v-cylc-object="scope.node"
/>
<v-list-item
:to="workflowLink(scope.node)"
class="flex-grow-1 px-2"
>
<v-row class="align-center align-content-center flex-nowrap">
<v-col
v-if="scope.node.type === 'workflow-part'"
class="c-gscan-workflow-name"
>
<span>{{ scope.node.name || scope.node.id }}</span>
</v-col>
<v-col
v-else-if="scope.node.type === 'workflow'"
class="c-gscan-workflow-name"
>
<span>
{{ scope.node.name }}
<v-tooltip location="top">{{ scope.node.id }}</v-tooltip>
</span>
</v-col>
<!-- We check the latestStateTasks below as offline workflows won't have a latestStateTasks property -->
<v-col
v-if="scope.node.type === 'workflow' && scope.node.node.latestStateTasks"
class="d-flex text-right c-gscan-workflow-states flex-grow-0"
>
<!-- task summary tooltips -->
<span
v-for="[state, tasks] in getLatestStateTasks(Object.entries(scope.node.node.latestStateTasks))"
:key="`${scope.node.id}-summary-${state}`"
:class="getTaskStateClasses(scope.node.node, state)"
>
<!-- a v-tooltip does not work directly set on Cylc job component, so we use a div to wrap it -->
<div
class="ma-0 pa-0"
min-width="0"
min-height="0"
style="font-size: 120%; width: auto;"
>
<job :status="state" />
<v-tooltip location="top">
<!-- tooltip text -->
<span>
<span class="text-grey-lighten-1">{{ countTasksInState(scope.node.node, state) }} {{ state }}. Recent {{ state }} tasks:</span>
<br/>
<span v-for="(task, index) in tasks.slice(0, $options.maxTasksDisplayed)" :key="index">
{{ task }}<br v-if="index !== tasks.length -1" />
</span>
</span>
</v-tooltip>
</div>
</span>
</v-col>
</v-row>
</v-list-item>
</template>
</tree>
/>
</div>
<!-- when no workflows are returned in the GraphQL query -->
<div v-else>
Expand All @@ -187,11 +123,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.

<script>
import { mdiFilter, mdiFolderRefresh } from '@mdi/js'
import TaskState, { TaskStateUserOrder } from '@/model/TaskState.model'
import { TaskStateUserOrder } from '@/model/TaskState.model'
import { WorkflowState } from '@/model/WorkflowState.model'
import Job from '@/components/cylc/Job.vue'
import Tree from '@/components/cylc/tree/Tree.vue'
import WorkflowIcon from '@/components/cylc/gscan/WorkflowIcon.vue'
import { filterHierarchically } from '@/components/cylc/gscan/filters'
import { sortedWorkflowTree } from '@/components/cylc/gscan/sort.js'
import { mutate } from '@/utils/aotf'
Expand All @@ -200,9 +134,7 @@ export default {
name: 'GScan',

components: {
Job,
Tree,
WorkflowIcon
},

props: {
Expand Down Expand Up @@ -349,49 +281,6 @@ export default {
item.model = newValue
})
},

workflowLink (node) {
if (node.type === 'workflow') {
return `/workspace/${ node.tokens.workflow }`
}
return ''
},

/**
* Get number of tasks we have in a given state. The states are retrieved
* from `latestStateTasks`, and the number of tasks in each state is from
* the `stateTotals`. (`latestStateTasks` includes old tasks).
*
* @param {WorkflowGraphQLData} workflow - the workflow object retrieved from GraphQL
* @param {string} state - a workflow state
* @returns {number|*} - the number of tasks in the given state
*/
countTasksInState (workflow, state) {
if (Object.hasOwnProperty.call(workflow.stateTotals, state)) {
return workflow.stateTotals[state]
}
return 0
},

getTaskStateClasses (workflow, state) {
const tasksInState = this.countTasksInState(workflow, state)
return tasksInState === 0 ? ['empty-state'] : []
},

// TODO: temporary filter, remove after b0 - https://github.com/cylc/cylc-ui/pull/617#issuecomment-805343847
getLatestStateTasks (latestStateTasks) {
// Values found in: https://github.com/cylc/cylc-flow/blob/9c542f9f3082d3c3d9839cf4330c41cfb2738ba1/cylc/flow/data_store_mgr.py#L143-L149
const validValues = [
TaskState.SUBMITTED.name,
TaskState.SUBMIT_FAILED.name,
TaskState.RUNNING.name,
TaskState.SUCCEEDED.name,
TaskState.FAILED.name
]
return latestStateTasks.filter(entry => {
return validValues.includes(entry[0])
})
}
},

// Misc options
Expand Down
67 changes: 47 additions & 20 deletions src/components/cylc/gscan/sort.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import {
} from '@/components/cylc/common/sort'
import { WorkflowState, WorkflowStateOrder } from '@/model/WorkflowState.model'

/* Return an integer suitable for alphabetical sorting of workflow states.
/**
* Return an integer suitable for alphabetical sorting of workflow states.
*
* This looks at all workflows contained in this tree and returns a status
* value for it as defined in WORKFLOW_STATE_ORDER.
*
*/
export function getWorkflowTreeSortValue (node) {
if (node.type === 'workflow') {
Expand All @@ -35,7 +35,7 @@ export function getWorkflowTreeSortValue (node) {
let item
const stack = [...node.children]
while (
ret !== WorkflowStateOrder.get(WorkflowState.RUNNING.name) &&
ret > WorkflowStateOrder.get(WorkflowState.RUNNING.name) &&
stack.length
) {
// NOTE: if one workflow is running (top sort order) then we don't
Expand All @@ -53,35 +53,62 @@ export function getWorkflowTreeSortValue (node) {
return ret
}

/* Return a string suitable for alphabetical sorting of workflows
/**
* Return a string suitable for alphabetical sorting of workflows
*
* Sorts workflows by:
* 1) State
* 2) Type (i.e. sort "workflow-part" before "workflow")
* 3) ID
* 2) ID
*/
function gscanWorkflowCompValue (node) {
const typeValue = node.type === 'workflow-part' ? 'a' : 'z'
return `${getWorkflowTreeSortValue(node)}_${typeValue}_${node.id}`
return `${getWorkflowTreeSortValue(node)}_${node.id}`
}

/* Returns a sorted list of top-level workflows / workflow-parts.
/**
* Returns a sorted list of top-level workflows / workflow-parts.
*
* Sorts according to getWorkflowTreeSortValue.
*/
export function sortedWorkflowTree (cylcTree) {
const tree = []
for (const workflowTree of cylcTree.children[0].children) {
// insert this workflow / workflow-part in sort order
tree.splice(
sortedIndexBy(
tree,
workflowTree,
(n) => gscanWorkflowCompValue(n)
),
0,
workflowTree
)
for (let node of cylcTree.children[0].children) {
node = flattenWorkflowParts(node)
if (node) {
// insert this workflow / workflow-part in sort order
tree.splice(
sortedIndexBy(tree, node, gscanWorkflowCompValue),
0,
node
)
}
}
return tree
}

/**
* Flatten workflow-parts nodes that only have 1 child.
*
* E.g. foo, containing only run1, becomes foo/run1
*
* @param {Object} node
* @returns {Object=} flattened node, or the original node if it has multiple children.
* Warning: can be undefined if a workflow-part has no children (this can happen sometimes but should be transitory).
*/
export function flattenWorkflowParts (node) {
if (node.type !== 'workflow-part') {
return node
}
if (node.children.length === 1) {
const child = node.children[0]
return flattenWorkflowParts({
...child,
name: child.id.substring(node.parent.length + 1), // (parent ID doesn't include slash so add 1)
parent: node.parent,
})
} else if (node.children.length > 1) {
return {
...node,
children: node.children.map((n) => flattenWorkflowParts(n))
}
}
}
Loading