Skip to content

set default log file by job outcome #2025

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

1 change: 1 addition & 0 deletions changes.d/2025.feat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added conditional default log file based on job outcome
5 changes: 3 additions & 2 deletions src/components/cylc/Job.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const Job = (props, context) => {
const DEFAULT_XY = '10'
const PREVIOUS_STATE_ITEMS_XY = '25'
const width = !isEmpty(props.previousState) ? PREVIOUS_STATE_ITEMS_SIZE : DEFAULT_SIZE
const cJobClass = ['c-job', props.status]
const jobStatusIcon = h(
'rect',
{
Expand Down Expand Up @@ -70,7 +71,7 @@ const Job = (props, context) => {
if (props.svg) {
return h(
'g',
{ class: 'c-job' },
{ class: cJobClass },
[
h('g', { class: 'job' }, jobIconChildren)
]
Expand All @@ -86,7 +87,7 @@ const Job = (props, context) => {
)
return h(
'span',
{ class: 'c-job' },
{ class: cJobClass },
[jobIconSvg]
)
}
Expand Down
22 changes: 21 additions & 1 deletion src/components/cylc/commandMenu/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,25 @@ import { eventBus } from '@/services/eventBus'
import CopyBtn from '@/components/core/CopyBtn.vue'
import { upperFirst } from 'lodash-es'
import { formatFlowNums } from '@/utils/tasks'
import { getJobLogFileFromState } from '@/model/JobState.model'

/**
* Return the appropriate log file for a job or task node, or nothing for other nodes.
*
* @param {Object} node - Cylc object node (i.e. workflow, cycle, family, task or job)
*/
export function getLogFileForNode (node) {
let jobState
if (node.type === 'job') {
jobState = node.node.state
} else if (node.type === 'task') {
// Choose latest job (jobs are sorted by submit num descending in the store)
jobState = node.children[0]?.node.state
} else {
return
}
return getJobLogFileFromState(jobState)
}

export default {
name: 'CommandMenu',
Expand Down Expand Up @@ -271,7 +290,8 @@ export default {
{
name: 'Log',
initialOptions: {
relativeID: this.node.tokens.relativeID || null
relativeID: this.node.tokens.relativeID || null,
file: getLogFileForNode(this.node),
}
}
)
Expand Down
19 changes: 19 additions & 0 deletions src/model/JobState.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,23 @@ class JobState extends Enumify {

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

/**
* Get the appropriate log file name for a job based on its state.
*
* @param {string=} state
* @returns {string=} log file name if state is defined & valid, else undefined.
*/
export function getJobLogFileFromState (state) {
switch (state) {
case JobState.FAILED.name:
return 'job.err'
case JobState.SUBMITTED.name:
case JobState.SUBMIT_FAILED.name:
return 'job-activity.log'
case JobState.RUNNING.name:
case JobState.SUCCEEDED.name:
Comment on lines +58 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] A little neater, but possibly a little safer (we have had issues with missing states):

Suggested change
case JobState.RUNNING.name:
case JobState.SUCCEEDED.name:
default:

return 'job.out'
}
}

export default JobState
2 changes: 1 addition & 1 deletion src/plugins/vuetify.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const inputDefaults = Object.fromEntries(
density: 'compact',
variant: 'outlined',
clearIcon: mdiClose,
hideDetails: true,
hideDetails: 'auto',
}
])
)
Expand Down
22 changes: 8 additions & 14 deletions src/services/mock/json/index.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/*
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
*
* This program is free software: you can redistribute it and/or modify
Expand All @@ -18,31 +18,25 @@
const IntrospectionQuery = require('./IntrospectionQuery.json')
const taskProxy = require('./taskProxy.json')
const familyProxy = require('./familyProxy.json')
const workflowOne = require('./workflows/one')
const workflowsMulti = require('./workflows/multi')
const { one, workflows, Workflow } = require('./workflows/index.cjs')
const { LogData } = require('./logData.cjs')
const { LogFiles } = require('./logFiles.cjs')
const { LogFiles, JobState } = require('./logFiles.cjs')
const analysisQuery = require('./analysisQuery.json')
const ganttQuery = require('./ganttQuery.json')
const InfoViewSubscription = require('./infoView.json')

const workflows = [workflowOne, ...workflowsMulti]
const analysisTaskQuery = analysisQuery.taskQuery
const analysisJobQuery = analysisQuery.jobQuery

module.exports = {
IntrospectionQuery,
taskProxy,
familyProxy,
LogData,
LogFiles,
JobState,
App: workflows,
Workflow ({ workflowId }) {
return workflows.find(({ deltas }) => deltas.id === workflowId) || {}
},
Test: workflowOne,
analysisTaskQuery,
analysisJobQuery,
Workflow,
GraphIQLTest: one,
analysisTaskQuery: analysisQuery.taskQuery,
analysisJobQuery: analysisQuery.jobQuery,
analysisQuery,
ganttQuery,
InfoViewSubscription
Expand Down
29 changes: 28 additions & 1 deletion src/services/mock/json/logFiles.cjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/*
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
*
* This program is free software: you can redistribute it and/or modify
Expand All @@ -16,13 +16,15 @@
*/

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

const deletedFile = 'deleted.log'

const jobLogFiles = [
'job.out',
'job.err',
'job',
'job-activity.log',
]

const workflowLogFiles = [
Expand All @@ -48,7 +50,32 @@ const LogFiles = async ({ id }) => {
}
}

/**
* Return a mock GQL response for job state.
*
* @param {{ id: string }} variables
*/
const JobState = async ({ id, workflowId }) => {
if (!workflowId.startsWith('~')) {
workflowId = `~user/${workflowId}`
}
const { deltas } = Workflow({ workflowId })
const searchID = id.replace(
/\/NN$/, ''
).replace(
/\/(\d+)$/, (match, p1) => `/${parseInt(p1)}` // strips leading zeroes
)
const { state } = deltas?.added?.jobs?.find((job) => job.id.includes(searchID)) ?? {}
await simulatedDelay(500)
return {
data: {
jobs: state ? [{ id, state }] : []
}
}
}

module.exports = {
JobState,
LogFiles,
deletedFile,
jobLogFiles,
Expand Down
31 changes: 31 additions & 0 deletions src/services/mock/json/workflows/index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

const one = require('./one')
const multi = require('./multi')

const workflows = [one, ...multi]

function Workflow ({ workflowId }) {
return workflows.find(({ deltas }) => deltas.id === workflowId) || {}
}

module.exports = {
one,
workflows,
Workflow,
}
45 changes: 35 additions & 10 deletions src/utils/uid.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/*
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
*
* This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -123,6 +123,8 @@ const RELATIVE_ID = new RegExp(`

/* eslint-enable */

const JOB_ID = /^(\d+|NN)$/

function detokenise (tokens, workflow = true, relative = true) {
let parts = []
let ret = ''
Expand Down Expand Up @@ -159,7 +161,7 @@ function detokenise (tokens, workflow = true, relative = true) {
return ret
}

class Tokens {
export class Tokens {
/* Represents a Cylc UID.
*
* Provides the interfaces for parsing to and from string IDs.
Expand Down Expand Up @@ -262,19 +264,29 @@ class Tokens {
this.job = undefined
}

if (this.job && !JOB_ID.test(this.job)) {
throw new Error(`Invalid job ID: ${this.job}`)
}

this.workflowID = detokenise(this, true, false)
this.relativeID = detokenise(this, false, true)
}

set (fields) {
for (const [key, value] of Object.entries(fields)) {
if (Tokens.KEYS.indexOf(key) === -1) {
throw new Error(`Invalid key: ${key}`)
if (fields instanceof Tokens) {
for (const key of Tokens.KEYS) {
if (fields[key]) this[key] = fields[key]
}
if (typeof value !== 'string' && typeof value !== 'undefined') {
throw new Error(`Invalid type for value: ${value}`)
} else {
for (const [key, value] of Object.entries(fields)) {
if (!Tokens.KEYS.includes(key)) {
throw new Error(`Invalid key: ${key}`)
}
if (typeof value !== 'string' && value != null) {
throw new Error(`Invalid type for value: ${value}`)
}
this[key] = value ?? undefined
}
this[key] = value
}
this.compute()
}
Expand Down Expand Up @@ -360,6 +372,19 @@ class Tokens {
}
return ret
}
}

export { Tokens }
/**
* Validate an ID string without throwing an error.
*
* @param {string} id
* @returns {string=} Error message if invalid, otherwise nothing.
*/
static validate (id, relative = false) {
try {
// eslint-disable-next-line no-new
new Tokens(id, relative)
} catch (e) {
return e.message
}
}
}
Loading