Skip to content

Commit 7190c22

Browse files
Add Workflow Log View (#1158)
## Add Workflow Log View * job log search box added to log view * Adds an interface for opening views via the eventBus. * Adds an `initialData` prop for views to recieve arguments. * view: recompute subscription on query change * If a view's query changes (e.g. if its variables change) whilst it is running. The view will now be un-subscribed, then re-subscribed with the new query. Co-authored-by: Oliver Sanders <[email protected]>
1 parent fde1e0a commit 7190c22

File tree

21 files changed

+542
-102
lines changed

21 files changed

+542
-102
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ ones in. -->
1515

1616
### Enhancements
1717

18+
[#1158](https://github.com/cylc/cylc-ui/pull/1158) -
19+
Log view: A new view which displays workflow and job logs (similar
20+
to log view available in Cylc 7 cylc review).
21+
1822
[#1144](https://github.com/cylc/cylc-ui/pull/1144) - Add "Edit Runtime"
1923
command, a more convenient way to perform broadcasts by viewing and editing a
2024
task/family's runtime configuration.

src/components/cylc/cylcObject/Menu.vue

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
8080
<v-list-item-action>
8181
<v-btn
8282
icon
83-
:disabled="!authorised"
83+
:disabled="isEditable(authorised, mutation)"
8484
x-large
8585
class="float-right"
8686
@click.stop="openDialog(mutation)"
@@ -231,7 +231,31 @@ export default {
231231
},
232232
233233
methods: {
234+
isEditable (authorised, mutation) {
235+
if (!authorised || mutation.name === 'log') {
236+
return true
237+
} else {
238+
return false
239+
}
240+
},
234241
openDialog (mutation) {
242+
if (mutation.name === 'log') {
243+
this.showMenu = false
244+
this.$eventBus.emit(
245+
'add-view',
246+
{
247+
248+
viewName: 'Log',
249+
initialOptions: {
250+
workflow: this.node.tokens.workflow,
251+
task: this.node.tokens.relative_id,
252+
file: 'job.out'
253+
}
254+
}
255+
)
256+
return
257+
}
258+
235259
this.dialog = true
236260
this.dialogMutation = mutation
237261
// Tell Vue to re-render the dialog component:

src/components/cylc/log/Log.vue

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
<template>
19+
<div>
20+
<pre><span v-for="(log, index) in logs" :key="index">{{log}}</span></pre>
21+
</div>
22+
</template>
23+
24+
<script>
25+
26+
export default {
27+
name: 'LogComponent',
28+
props: {
29+
placeholder: {
30+
type: String,
31+
default: 'Waiting for logs',
32+
required: false
33+
},
34+
logs: {
35+
type: Array,
36+
required: true
37+
}
38+
},
39+
computed: {
40+
computedLogs () {
41+
if (this.logs.length > 0) {
42+
return this.logs
43+
} else {
44+
return [this.placeholder]
45+
}
46+
}
47+
}
48+
}
49+
50+
</script>

src/components/cylc/log/index.js

Whitespace-only changes.

src/components/cylc/workflow/Toolbar.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
110110
:id="`toolbar-add-${ view.name }-view`"
111111
v-for="view in views"
112112
:key="view.name"
113-
@click="$listeners['add'](view.name)"
113+
@click="$listeners['add']({ viewName: view.name })"
114114
class="py-0 px-8 ma-0 c-add-view"
115115
>
116116
<v-list-item-icon>

src/graphql/queries.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,19 @@ fragment UpdatedDelta on Updated {
196196
${WORKFLOW_DATA_FRAGMENT}
197197
`
198198

199+
/**
200+
* Query used to retrieve data for the Log view.
201+
*
202+
* @type {DocumentNode}
203+
*/
204+
const LOGS_SUBSCRIPTION = gql`
205+
subscription LogData ($workflowName: ID, $task: String, $file: String) {
206+
logs (workflow: $workflowName, task:$task, file: $file) {
207+
lines
208+
}
209+
}
210+
`
211+
199212
/**
200213
* Query used to retrieve data for the WorkflowsTable view.
201214
*
@@ -380,6 +393,7 @@ ${JOB_DATA_FRAGMENT}
380393
export {
381394
GSCAN_DELTAS_SUBSCRIPTION,
382395
DASHBOARD_DELTAS_SUBSCRIPTION,
396+
LOGS_SUBSCRIPTION,
383397
WORKFLOWS_TABLE_DELTAS_SUBSCRIPTION,
384398
WORKFLOW_TREE_DELTAS_SUBSCRIPTION,
385399
WORKFLOW_TABLE_DELTAS_SUBSCRIPTION

src/mixins/subscriptionComponent.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import subscriptionMixin from '@/mixins/subscription'
2222
* of such component is the GScan component, which declares a query used to
2323
* list the workflows of the system in the UI sidebar.
2424
*
25-
* Uses Vue component lifecycle methods (e.g. created, beforeDestroy) to coordinate
26-
* when a subscription is created in the WorkflowService service.
25+
* Uses Vue component lifecycle methods (e.g. created, beforeDestroy) to
26+
* coordinate when a subscription is created in the WorkflowService service.
2727
*
2828
* @see Subscription
2929
* @see SubscriptionQuery
@@ -33,7 +33,7 @@ export default {
3333
mixins: [
3434
subscriptionMixin
3535
],
36-
created () {
36+
beforeMount () {
3737
this.$workflowService.subscribe(this)
3838
},
3939
beforeDestroy () {

src/model/SubscriptionQuery.model.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,16 @@ class SubscriptionQuery {
3636
* @param {Object.<String, String>} variables
3737
* @param {String} name
3838
* @param {Array<DeltasCallback>} callbacks
39+
* @param {boolean} isDelta
40+
* @param {boolean} isGlobalCallback
3941
*/
40-
constructor (query, variables, name, callbacks) {
42+
constructor (query, variables, name, callbacks, isDelta, isGlobalCallback) {
4143
this.query = query
4244
this.variables = variables
4345
this.name = name
4446
this.callbacks = callbacks
47+
this.isDelta = isDelta
48+
this.isGlobalCallback = isGlobalCallback
4549
}
4650
}
4751

src/services/workflow.service.js

Lines changed: 85 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ class WorkflowService {
219219
subscription.subscribers[componentOrView._uid] = componentOrView
220220
// Then we recompute the query, checking if variables match, and action name is set.
221221
this.recompute(subscription)
222-
// regardless of whether this results in a restart, we take this opertunity to preset the componentOrView store if needed
222+
// regardless of whether this results in a restart, we take this opportunity to preset the componentOrView store if needed
223223
const errors = []
224224
// if the callbacks class has an init method defined, use it
225225
for (const callback of subscription.callbacks) {
@@ -273,63 +273,92 @@ class WorkflowService {
273273
}
274274
this.stopSubscription(subscription, true)
275275
}
276-
277-
const globalCallback = this.globalCallback
278-
279-
try {
280-
// Then start subscription.
281-
subscription.observable = this.startDeltasSubscription(
282-
subscription.query.query,
283-
subscription.query.variables,
284-
{
285-
next: function next (response) {
286-
const deltas = response.data.deltas || {}
287-
const added = deltas.added || {}
288-
const updated = deltas.updated || {}
289-
const pruned = deltas.pruned || {}
290-
const errors = []
291-
292-
// run the global callback first
293-
globalCallback.onAdded(added, store, errors)
294-
globalCallback.onUpdated(updated, store, errors)
295-
globalCallback.onPruned(pruned, store, errors)
296-
297-
// then run the local callbacks if there are any
298-
if (subscription.callbacks.length === 0) {
299-
return
300-
}
301-
for (const callback of subscription.callbacks) {
302-
callback.before(deltas, store, errors)
303-
callback.onAdded(added, store, errors)
304-
callback.onUpdated(updated, store, errors)
305-
callback.commit(store, errors)
306-
}
307-
for (const callback of [...subscription.callbacks].reverse()) {
308-
callback.onPruned(pruned, store, errors)
309-
callback.after(deltas, store, errors)
310-
callback.commit(store, errors)
276+
if (subscription.query.isDelta === false & subscription.query.isGlobalCallback === false) {
277+
try {
278+
// Then start subscription.
279+
subscription.observable = this.startCylcSubscription(
280+
subscription.query.query,
281+
subscription.query.variables,
282+
{
283+
next: function next (response) {
284+
if (subscription.callbacks.length === 0) {
285+
return
286+
}
287+
const errors = []
288+
for (const callback of subscription.callbacks) {
289+
callback.onAdded(response.data.logs, store, errors)
290+
callback.commit(store, errors)
291+
}
292+
},
293+
error: function error (err) {
294+
subscription.handleViewState(ViewState.ERROR, err)
311295
}
312-
for (const error of errors) {
313-
store.commit(
314-
'SET_ALERT',
315-
new Alert(error[0], null, 'error'),
316-
{ root: true }
317-
)
318-
// eslint-disable-next-line no-console
319-
console.warn(...error)
296+
}
297+
)
298+
this.subscriptions[subscription.query.name] = subscription
299+
// All done!
300+
subscription.handleViewState(ViewState.COMPLETE, null)
301+
subscription.reload = false
302+
} catch (e) {
303+
subscription.handleViewState(ViewState.ERROR, e)
304+
}
305+
} else {
306+
const globalCallback = this.globalCallback
307+
try {
308+
// Then start subscription.
309+
subscription.observable = this.startCylcSubscription(
310+
subscription.query.query,
311+
subscription.query.variables,
312+
{
313+
next: function next (response) {
314+
const deltas = response.data.deltas || {}
315+
const added = deltas.added || {}
316+
const updated = deltas.updated || {}
317+
const pruned = deltas.pruned || {}
318+
const errors = []
319+
320+
// run the global callback first
321+
globalCallback.onAdded(added, store, errors)
322+
globalCallback.onUpdated(updated, store, errors)
323+
globalCallback.onPruned(pruned, store, errors)
324+
325+
// then run the local callbacks if there are any
326+
if (subscription.callbacks.length === 0) {
327+
return
328+
}
329+
for (const callback of subscription.callbacks) {
330+
callback.before(deltas, store, errors)
331+
callback.onAdded(added, store, errors)
332+
callback.onUpdated(updated, store, errors)
333+
callback.commit(store, errors)
334+
}
335+
for (const callback of [...subscription.callbacks].reverse()) {
336+
callback.onPruned(pruned, store, errors)
337+
callback.after(deltas, store, errors)
338+
callback.commit(store, errors)
339+
}
340+
for (const error of errors) {
341+
store.commit(
342+
'SET_ALERT',
343+
new Alert(error[0], null, 'error'),
344+
{ root: true }
345+
)
346+
// eslint-disable-next-line no-console
347+
console.warn(...error)
348+
}
349+
},
350+
error: function error (err) {
351+
subscription.handleViewState(ViewState.ERROR, err)
320352
}
321-
},
322-
error: function error (err) {
323-
subscription.handleViewState(ViewState.ERROR, err)
324353
}
325-
}
326-
)
327-
this.subscriptions[subscription.query.name] = subscription
328-
// All done!
329-
subscription.handleViewState(ViewState.COMPLETE, null)
330-
subscription.reload = false
331-
} catch (e) {
332-
subscription.handleViewState(ViewState.ERROR, e)
354+
)
355+
this.subscriptions[subscription.query.name] = subscription
356+
// All done!
357+
subscription.handleViewState(ViewState.COMPLETE, null)
358+
subscription.reload = false
359+
} catch (e) {
360+
subscription.handleViewState(ViewState.ERROR, e)
361+
}
333362
}
334363
}
335364

@@ -343,7 +372,7 @@ class WorkflowService {
343372
* @param {SubscriptionOptions} subscriptionOptions - { next(), error() }
344373
* @returns {Subscription}
345374
*/
346-
startDeltasSubscription (query, variables, subscriptionOptions) {
375+
startCylcSubscription (query, variables, subscriptionOptions) {
347376
if (!query) {
348377
throw new Error('You must provide a query for the subscription')
349378
}

0 commit comments

Comments
 (0)