From 6eb54999afb5a04d1ba12031854ad2d7d1965290 Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Thu, 2 May 2024 09:35:08 +0100 Subject: [PATCH 01/11] metadata view: introduce a minimal task metadata view * Bare bones task metadata view. * Display task metadata, prerequisites and outputs. * Support for viewing task configuration, broadcasts, etc to follow in future PRs. * Support for markup (e.g. markdown & reStructured text) to arrive in future PRs (pending a decision on how to configure the markup language). --- changes.d/1886.feat.md | 1 + src/components/cylc/commandMenu/Menu.vue | 19 +- src/utils/aotf.js | 16 +- src/views/Info.vue | 437 +++++++++++++++++++++++ src/views/Workspace.vue | 5 +- src/views/views.js | 26 +- 6 files changed, 492 insertions(+), 12 deletions(-) create mode 100644 changes.d/1886.feat.md create mode 100644 src/views/Info.vue diff --git a/changes.d/1886.feat.md b/changes.d/1886.feat.md new file mode 100644 index 000000000..9f20cc650 --- /dev/null +++ b/changes.d/1886.feat.md @@ -0,0 +1 @@ +Added an info view to display task information including metadata, prerequisites and outputs. diff --git a/src/components/cylc/commandMenu/Menu.vue b/src/components/cylc/commandMenu/Menu.vue index a75a8e065..a3e532ce9 100644 --- a/src/components/cylc/commandMenu/Menu.vue +++ b/src/components/cylc/commandMenu/Menu.vue @@ -224,7 +224,7 @@ export default { methods: { isEditable (mutation, authorised) { - return mutation.name !== 'log' && !this.isDisabled(mutation, authorised) + return mutation.name !== 'log' && mutation.name !== 'info' && !this.isDisabled(mutation, authorised) }, isDisabled (mutation, authorised) { if (!authorised) { @@ -271,6 +271,23 @@ export default { } ) }) + } else if (mutation.name === 'info') { + this.$router.push({ + name: 'Workspace', + params: { + workflowName: this.node.tokens.workflow + } + }).then(() => { + eventBus.emit( + 'add-view', + { + name: 'Info', + initialOptions: { + requestedTokens: this.node.tokens || undefined + } + } + ) + }) } else { mutate( mutation, diff --git a/src/utils/aotf.js b/src/utils/aotf.js index e864b2b95..ccf42a6ab 100644 --- a/src/utils/aotf.js +++ b/src/utils/aotf.js @@ -35,7 +35,7 @@ import { mdiDelete, mdiEmail, mdiFileDocumentOutline, - mdiVectorPolylineEdit, + mdiInformationOutline, mdiMinusCircleOutline, mdiPause, mdiPauseCircleOutline, @@ -44,7 +44,8 @@ import { mdiPlaylistEdit, mdiRefreshCircle, mdiReload, - mdiStop + mdiStop, + mdiVectorPolylineEdit, } from '@mdi/js' import { Alert } from '@/model/Alert.model' @@ -128,6 +129,7 @@ export function getMutationIcon (name) { case 'clean': return mdiDelete case 'editRuntime': return mdiPlaylistEdit case 'hold': return mdiPauseCircleOutline // to distinguish from pause + case 'info': return mdiInformationOutline case 'kill': return mdiCloseCircle case 'log': return mdiFileDocumentOutline case 'message': return mdiEmail @@ -188,6 +190,7 @@ export const primaryMutations = { 'trigger', 'kill', 'log', + 'info', 'set' ] } @@ -314,6 +317,13 @@ export const dummyMutations = [ _requiresInfo: false, _validStates: WorkflowStateNames, }, + { + name: 'info', + description: 'View task information.', + args: [], + _appliesTo: [cylcObjects.Namespace], + _requiresInfo: false + }, ] /** @@ -323,7 +333,7 @@ export const dummyMutations = [ */ const dummyMutationsPermissionsMap = Object.freeze({ broadcast: Object.freeze(['editRuntime']), - read: Object.freeze(['log']) + read: Object.freeze(['log', 'info']) }) /** diff --git a/src/views/Info.vue b/src/views/Info.vue new file mode 100644 index 000000000..c6eb8173f --- /dev/null +++ b/src/views/Info.vue @@ -0,0 +1,437 @@ + + + + + + + diff --git a/src/views/Workspace.vue b/src/views/Workspace.vue index 2e6a60d96..b2ce3d0c0 100644 --- a/src/views/Workspace.vue +++ b/src/views/Workspace.vue @@ -18,7 +18,7 @@ along with this program. If not, see . @@ -142,6 +168,7 @@ along with this program. If not, see . @@ -214,18 +250,22 @@ export default { .condition { opacity: 0.6; } - .condition.satisfied { + .condition.satisfied, .condition.blank { opacity: 1; } // prefixes a tick or cross before the entry .condition:before { - content: '\25CB'; - padding-right: 0.5em; + display: inline-block; + content: '\25CB'; /* empty circle */ + width: 1.5em; color: rgb(0, 0, 0); } .condition.satisfied:before { - content: '\25CF'; + content: '\25CF'; /* filled circle */ + } + .condition.blank:before { + content: ''; /* blank */ } // for prerequsite task "aliases" (used in conditional expressions) @@ -265,5 +305,11 @@ export default { list-style: none; } } + + .completion-panel { + li { + list-style: none; + } + } } diff --git a/src/utils/outputs.js b/src/utils/outputs.js new file mode 100644 index 000000000..deb0b1173 --- /dev/null +++ b/src/utils/outputs.js @@ -0,0 +1,71 @@ +/** + * Copyright (C) NIWA & British Crown (Met Office) & Contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** Functionality relating to task output formatting. **/ + +/** Format a completion expression for display. + * + * @param {str} completion - The task's completion expression. + * @param {Array} outputs - The task's outputs as obtained from GraphQL as an + * array of objects with "label" and "satisfied" attributes. + * + * @returns {Array} - [isSatisfied, indentLevel, text] + **/ + +export function formatCompletion (completion, outputs) { + // the array to return + const lines = [] + // indent level of the expression + let indent = 0 + // text yet to be added to the return result + let buffer = '' + + // break the completion expression down into parts and iterate over them + for (let part of completion.split(/(and|or|\(|\))/)) { + part = part.trim() + + if (!part) { + continue + } + + if (part === '(') { + // open bracket + lines.push([null, indent, `${buffer}(`]) + buffer = '' + indent = indent + 1 + } else if (part === ')') { + // close bracket + indent = indent - 1 + lines.push([null, indent, `${buffer})`]) + buffer = '' + } else if (part === 'and' || part === 'or') { + // local operator + buffer = `${part} ` + } else { + // Cylc output -> look it up in the outputs Array + for (const output of outputs) { + if (output.label === part) { + lines.push([output.satisfied, indent, `${buffer}${part}`]) + break + } + } + buffer = '' + } + } + + return lines +} diff --git a/src/views/Info.vue b/src/views/Info.vue index d3bcd1568..3b60bfa5e 100644 --- a/src/views/Info.vue +++ b/src/views/Info.vue @@ -94,6 +94,10 @@ fragment TaskProxyData on TaskProxy { label satisfied } + + runtime { + completion + } } fragment TaskDefinitionData on Task { From 9b339295ce09187c55f2434c8cfdba0692af9261 Mon Sep 17 00:00:00 2001 From: Oliver Sanders Date: Wed, 11 Dec 2024 11:06:30 +0000 Subject: [PATCH 10/11] info: prevent info view being selected as the default view --- src/views/UserProfile.vue | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/views/UserProfile.vue b/src/views/UserProfile.vue index 3d34de22d..c21f896ea 100644 --- a/src/views/UserProfile.vue +++ b/src/views/UserProfile.vue @@ -195,7 +195,7 @@ along with this program. If not, see . Date: Wed, 11 Dec 2024 11:19:22 +0000 Subject: [PATCH 11/11] Apply suggestions from code review Co-authored-by: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> --- cypress/component/info.cy.js | 24 ++++++++++++------------ src/services/mock/json/infoView.json | 6 +++++- tests/e2e/specs/info.cy.js | 7 +++---- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/cypress/component/info.cy.js b/cypress/component/info.cy.js index 132cecb6d..3bd79dc37 100644 --- a/cypress/component/info.cy.js +++ b/cypress/component/info.cy.js @@ -161,23 +161,23 @@ describe('Info component', () => { .find('.prerequisite-alias.condition') .should('have.length', 6) .then((selector) => { - expect(selector[0]).to.contain('(0 & 1) | 2') - expect(selector[0].classList.toString()).to.equal('prerequisite-alias condition') + expect(selector[0].innerText).to.equal('(0 & 1) | 2') + expect(selector[0]).to.not.have.class('satisfied') - expect(selector[0]).to.contain('0') - expect(selector[1].classList.toString()).to.equal('prerequisite-alias condition satisfied') + expect(selector[1].innerText).to.equal('0 a:succeeded') + expect(selector[1]).to.have.class('satisfied') - expect(selector[0]).to.contain('1') - expect(selector[2].classList.toString()).to.equal('prerequisite-alias condition') + expect(selector[2].innerText).to.equal('1 b:custom_output') + expect(selector[2]).to.not.have.class('satisfied') - expect(selector[0]).to.contain('2') - expect(selector[3].classList.toString()).to.equal('prerequisite-alias condition') + expect(selector[3].innerText).to.equal('2 a:expired') + expect(selector[3]).to.not.have.class('satisfied') - expect(selector[0]).to.contain('0') - expect(selector[4].classList.toString()).to.equal('prerequisite-alias condition satisfied') + expect(selector[4].innerText).to.equal('0') + expect(selector[4]).to.have.class('satisfied') - expect(selector[0]).to.contain('0') - expect(selector[5].classList.toString()).to.equal('prerequisite-alias condition satisfied') + expect(selector[5].innerText).to.equal('0 x:succeeded') + expect(selector[5]).to.have.class('satisfied') }) // the outputs panel diff --git a/src/services/mock/json/infoView.json b/src/services/mock/json/infoView.json index 6e3d86056..05b7d1bfa 100644 --- a/src/services/mock/json/infoView.json +++ b/src/services/mock/json/infoView.json @@ -73,7 +73,11 @@ "label": "expired", "satisfied": false } - ] + ], + + "runtime": { + "completion": "succeeded" + } } ] } diff --git a/tests/e2e/specs/info.cy.js b/tests/e2e/specs/info.cy.js index 29a637869..e5a6204e5 100644 --- a/tests/e2e/specs/info.cy.js +++ b/tests/e2e/specs/info.cy.js @@ -19,13 +19,12 @@ describe('Info View', () => { it('works', () => { // test opening the "Info View" from a task in the "Tree View" cy.visit('/#/workspace/one') - // click on task 20000102T0000Z/failed - .get('.c-treeitem .c-treeitem .c-treeitem:first') - .find('.c-task') + // click on task + .get('.node-data-task [data-c-interactive]:first') .click({ force: true }) // from the menu select the "Info" psudo-mutation - .get('.v-list > :nth-child(6)') + .get('.v-list-item') .contains('Info') .click({ force: true })