Skip to content

Commit b7bb2b6

Browse files
set default log file by job outcome (#2025)
set default log file by job outcome Additionally: * UID Tokens validates job part and permits `NN` * Validate relative ID input in log view * Log view: make file input non-clearable - The clear icon takes up space and can cover the filename. I can't think of a reason why a user would want to clear this particular input. * Log view: ensure default job log file does not override `initialOptions` --------- Co-authored-by: Ronnie Dutta <[email protected]>
1 parent b6de071 commit b7bb2b6

File tree

23 files changed

+595
-242
lines changed

23 files changed

+595
-242
lines changed

changes.d/2025.feat.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added conditional default log file based on job outcome

src/components/cylc/Job.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const Job = (props, context) => {
3232
const DEFAULT_XY = '10'
3333
const PREVIOUS_STATE_ITEMS_XY = '25'
3434
const width = !isEmpty(props.previousState) ? PREVIOUS_STATE_ITEMS_SIZE : DEFAULT_SIZE
35+
const cJobClass = ['c-job', props.status]
3536
const jobStatusIcon = h(
3637
'rect',
3738
{
@@ -70,7 +71,7 @@ const Job = (props, context) => {
7071
if (props.svg) {
7172
return h(
7273
'g',
73-
{ class: 'c-job' },
74+
{ class: cJobClass },
7475
[
7576
h('g', { class: 'job' }, jobIconChildren)
7677
]
@@ -86,7 +87,7 @@ const Job = (props, context) => {
8687
)
8788
return h(
8889
'span',
89-
{ class: 'c-job' },
90+
{ class: cJobClass },
9091
[jobIconSvg]
9192
)
9293
}

src/components/cylc/commandMenu/Menu.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,25 @@ import { eventBus } from '@/services/eventBus'
128128
import CopyBtn from '@/components/core/CopyBtn.vue'
129129
import { upperFirst } from 'lodash-es'
130130
import { formatFlowNums } from '@/utils/tasks'
131+
import { getJobLogFileFromState } from '@/model/JobState.model'
132+
133+
/**
134+
* Return the appropriate log file for a job or task node, or nothing for other nodes.
135+
*
136+
* @param {Object} node - Cylc object node (i.e. workflow, cycle, family, task or job)
137+
*/
138+
export function getLogFileForNode (node) {
139+
let jobState
140+
if (node.type === 'job') {
141+
jobState = node.node.state
142+
} else if (node.type === 'task') {
143+
// Choose latest job (jobs are sorted by submit num descending in the store)
144+
jobState = node.children[0]?.node.state
145+
} else {
146+
return
147+
}
148+
return getJobLogFileFromState(jobState)
149+
}
131150
132151
export default {
133152
name: 'CommandMenu',
@@ -271,7 +290,8 @@ export default {
271290
{
272291
name: 'Log',
273292
initialOptions: {
274-
relativeID: this.node.tokens.relativeID || null
293+
relativeID: this.node.tokens.relativeID || null,
294+
file: getLogFileForNode(this.node),
275295
}
276296
}
277297
)

src/model/JobState.model.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,23 @@ class JobState extends Enumify {
4242

4343
export const JobStateNames = JobState.enumValues.map(({ name }) => name)
4444

45+
/**
46+
* Get the appropriate log file name for a job based on its state.
47+
*
48+
* @param {string=} state
49+
* @returns {string=} log file name if state is defined & valid, else undefined.
50+
*/
51+
export function getJobLogFileFromState (state) {
52+
switch (state) {
53+
case JobState.FAILED.name:
54+
return 'job.err'
55+
case JobState.SUBMITTED.name:
56+
case JobState.SUBMIT_FAILED.name:
57+
return 'job-activity.log'
58+
case JobState.RUNNING.name:
59+
case JobState.SUCCEEDED.name:
60+
return 'job.out'
61+
}
62+
}
63+
4564
export default JobState

src/plugins/vuetify.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const inputDefaults = Object.fromEntries(
4242
density: 'compact',
4343
variant: 'outlined',
4444
clearIcon: mdiClose,
45-
hideDetails: true,
45+
hideDetails: 'auto',
4646
}
4747
])
4848
)

src/services/mock/json/index.cjs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/*
22
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
33
*
44
* This program is free software: you can redistribute it and/or modify
@@ -18,31 +18,25 @@
1818
const IntrospectionQuery = require('./IntrospectionQuery.json')
1919
const taskProxy = require('./taskProxy.json')
2020
const familyProxy = require('./familyProxy.json')
21-
const workflowOne = require('./workflows/one')
22-
const workflowsMulti = require('./workflows/multi')
21+
const { one, workflows, Workflow } = require('./workflows/index.cjs')
2322
const { LogData } = require('./logData.cjs')
24-
const { LogFiles } = require('./logFiles.cjs')
23+
const { LogFiles, JobState } = require('./logFiles.cjs')
2524
const analysisQuery = require('./analysisQuery.json')
2625
const ganttQuery = require('./ganttQuery.json')
2726
const InfoViewSubscription = require('./infoView.json')
2827

29-
const workflows = [workflowOne, ...workflowsMulti]
30-
const analysisTaskQuery = analysisQuery.taskQuery
31-
const analysisJobQuery = analysisQuery.jobQuery
32-
3328
module.exports = {
3429
IntrospectionQuery,
3530
taskProxy,
3631
familyProxy,
3732
LogData,
3833
LogFiles,
34+
JobState,
3935
App: workflows,
40-
Workflow ({ workflowId }) {
41-
return workflows.find(({ deltas }) => deltas.id === workflowId) || {}
42-
},
43-
Test: workflowOne,
44-
analysisTaskQuery,
45-
analysisJobQuery,
36+
Workflow,
37+
GraphIQLTest: one,
38+
analysisTaskQuery: analysisQuery.taskQuery,
39+
analysisJobQuery: analysisQuery.jobQuery,
4640
analysisQuery,
4741
ganttQuery,
4842
InfoViewSubscription

src/services/mock/json/logFiles.cjs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/*
22
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
33
*
44
* This program is free software: you can redistribute it and/or modify
@@ -16,13 +16,15 @@
1616
*/
1717

1818
const { simulatedDelay } = require('./util.cjs')
19+
const { Workflow } = require('./workflows/index.cjs')
1920

2021
const deletedFile = 'deleted.log'
2122

2223
const jobLogFiles = [
2324
'job.out',
2425
'job.err',
2526
'job',
27+
'job-activity.log',
2628
]
2729

2830
const workflowLogFiles = [
@@ -48,7 +50,32 @@ const LogFiles = async ({ id }) => {
4850
}
4951
}
5052

53+
/**
54+
* Return a mock GQL response for job state.
55+
*
56+
* @param {{ id: string }} variables
57+
*/
58+
const JobState = async ({ id, workflowId }) => {
59+
if (!workflowId.startsWith('~')) {
60+
workflowId = `~user/${workflowId}`
61+
}
62+
const { deltas } = Workflow({ workflowId })
63+
const searchID = id.replace(
64+
/\/NN$/, ''
65+
).replace(
66+
/\/(\d+)$/, (match, p1) => `/${parseInt(p1)}` // strips leading zeroes
67+
)
68+
const { state } = deltas?.added?.jobs?.find((job) => job.id.includes(searchID)) ?? {}
69+
await simulatedDelay(500)
70+
return {
71+
data: {
72+
jobs: state ? [{ id, state }] : []
73+
}
74+
}
75+
}
76+
5177
module.exports = {
78+
JobState,
5279
LogFiles,
5380
deletedFile,
5481
jobLogFiles,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
const one = require('./one')
19+
const multi = require('./multi')
20+
21+
const workflows = [one, ...multi]
22+
23+
function Workflow ({ workflowId }) {
24+
return workflows.find(({ deltas }) => deltas.id === workflowId) || {}
25+
}
26+
27+
module.exports = {
28+
one,
29+
workflows,
30+
Workflow,
31+
}

src/utils/uid.js

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/*
22
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
33
*
44
* This program is free software: you can redistribute it and/or modify
@@ -123,6 +123,8 @@ const RELATIVE_ID = new RegExp(`
123123

124124
/* eslint-enable */
125125

126+
const JOB_ID = /^(\d+|NN)$/
127+
126128
function detokenise (tokens, workflow = true, relative = true) {
127129
let parts = []
128130
let ret = ''
@@ -159,7 +161,7 @@ function detokenise (tokens, workflow = true, relative = true) {
159161
return ret
160162
}
161163

162-
class Tokens {
164+
export class Tokens {
163165
/* Represents a Cylc UID.
164166
*
165167
* Provides the interfaces for parsing to and from string IDs.
@@ -262,19 +264,29 @@ class Tokens {
262264
this.job = undefined
263265
}
264266

267+
if (this.job && !JOB_ID.test(this.job)) {
268+
throw new Error(`Invalid job ID: ${this.job}`)
269+
}
270+
265271
this.workflowID = detokenise(this, true, false)
266272
this.relativeID = detokenise(this, false, true)
267273
}
268274

269275
set (fields) {
270-
for (const [key, value] of Object.entries(fields)) {
271-
if (Tokens.KEYS.indexOf(key) === -1) {
272-
throw new Error(`Invalid key: ${key}`)
276+
if (fields instanceof Tokens) {
277+
for (const key of Tokens.KEYS) {
278+
if (fields[key]) this[key] = fields[key]
273279
}
274-
if (typeof value !== 'string' && typeof value !== 'undefined') {
275-
throw new Error(`Invalid type for value: ${value}`)
280+
} else {
281+
for (const [key, value] of Object.entries(fields)) {
282+
if (!Tokens.KEYS.includes(key)) {
283+
throw new Error(`Invalid key: ${key}`)
284+
}
285+
if (typeof value !== 'string' && value != null) {
286+
throw new Error(`Invalid type for value: ${value}`)
287+
}
288+
this[key] = value ?? undefined
276289
}
277-
this[key] = value
278290
}
279291
this.compute()
280292
}
@@ -360,6 +372,19 @@ class Tokens {
360372
}
361373
return ret
362374
}
363-
}
364375

365-
export { Tokens }
376+
/**
377+
* Validate an ID string without throwing an error.
378+
*
379+
* @param {string} id
380+
* @returns {string=} Error message if invalid, otherwise nothing.
381+
*/
382+
static validate (id, relative = false) {
383+
try {
384+
// eslint-disable-next-line no-new
385+
new Tokens(id, relative)
386+
} catch (e) {
387+
return e.message
388+
}
389+
}
390+
}

0 commit comments

Comments
 (0)