Skip to content

Commit 0f31780

Browse files
committed
Maintain state of task progress animation when workspace tabs are hidden & shown
1 parent 557315b commit 0f31780

File tree

9 files changed

+101
-37
lines changed

9 files changed

+101
-37
lines changed

cypress/component/cylc-icons.cy.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,15 @@ describe('Task component', () => {
103103
.should('have.css', 'animation-duration', `${MEAN_ELAPSED_TIME}s`)
104104
// the offset should be set to the "percent" of the expected job duration
105105
.should('have.css', 'animation-delay')
106-
.and('match', /([\d.]+)s/) // NOTE the delay should be negative
106+
.and('match', /([\d.-]+)s/)
107107
.then((number) => {
108-
// convert the duration string into a number that we can test
109-
cy.wrap(Number(number.match(/([\d.]+)s/)[1]))
108+
expect(parseInt(number)).to.be.closeTo(
110109
// ensure this number is ±5 from the expected value
111110
// (give it a little bit of margin to allow for timing error)
112-
.should('closeTo', MEAN_ELAPSED_TIME * (percent / 100), 5)
111+
// NOTE the delay should be negative
112+
-MEAN_ELAPSED_TIME * (percent / 100),
113+
5
114+
)
113115
})
114116
}
115117
})

cypress/component/utils/task.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@
1919
export const MEAN_ELAPSED_TIME = 10000
2020

2121
export function getStartTime (percent) {
22-
return String(
23-
new Date(
24-
// the current time in ms
25-
Date.now() -
26-
// minus the elapsed time in ms
27-
(
28-
(MEAN_ELAPSED_TIME * 1000) *
29-
(percent / 100)
30-
)
31-
).toISOString()
32-
)
22+
return new Date(
23+
// the current time in ms
24+
Date.now() -
25+
// minus the elapsed time in ms
26+
(
27+
(MEAN_ELAPSED_TIME * 1000) *
28+
(percent / 100)
29+
)
30+
).toISOString()
3331
}

src/components/cylc/SVGTask.vue

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
6969
r="16"
7070
stroke-width="50"
7171
stroke-dasharray="157"
72-
:style="getRunningStyle()"
72+
:style="runningStyle"
7373
/>
7474
</g>
7575
<!-- dot
@@ -247,6 +247,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
247247
</template>
248248

249249
<script setup>
250+
import { computed, inject, ref } from 'vue'
250251
import TaskState from '@/model/TaskState.model'
251252
252253
const props = defineProps({
@@ -268,26 +269,30 @@ const props = defineProps({
268269
},
269270
})
270271
271-
function getRunningStyle () {
272+
/**
273+
* @type {import('vue').Ref<number>}
274+
* @see @/components/cylc/workspace/Widget.vue
275+
*/
276+
const animResetTime = inject('animResetTime', () => ref(0), true)
277+
278+
const runningStyle = computed(() => {
272279
if (
273280
props.task.state === TaskState.RUNNING.name &&
274281
props.startTime &&
275282
props.task.task?.meanElapsedTime
276283
) {
277-
// job start time in ms (UTC)
278-
const startTime = Date.parse(props.startTime)
279-
// current time in ms (UTC)
280-
const now = Date.now()
284+
// current time in ms (UTC); updates whenever widget is unhidden
285+
const now = Math.max(Date.now(), animResetTime.value)
281286
// job elapsed time in ms
282-
const elapsedTime = now - startTime
287+
const elapsedTime = now - Date.parse(props.startTime)
283288
return {
284289
animationDuration: `${props.task.task.meanElapsedTime}s`,
285290
animationDelay: `-${elapsedTime}ms`,
286291
animationFillMode: 'forwards',
287292
}
288293
}
289294
return {}
290-
}
295+
})
291296
292297
/**
293298
* Returns the translation required to position the ".modifier" nicely in

src/components/cylc/workflow/Lumino.vue renamed to src/components/cylc/workspace/Lumino.vue

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
<div ref="mainDiv" class="main pa-2 fill-height">
1919
<!-- Lumino box panel gets inserted here -->
2020
</div>
21-
<template
21+
<WidgetComponent
2222
v-for="[id, { name }] in views"
2323
:key="id"
24+
:id="id"
2425
>
25-
<Teleport :to="`#${id}`">
26-
<component
27-
:is="props.allViews.get(name).component"
28-
:workflow-name="workflowName"
29-
v-model:initial-options="views.get(id).initialOptions"
30-
class="h-100"
31-
/>
32-
</Teleport>
33-
</template>
26+
<component
27+
:is="props.allViews.get(name).component"
28+
:workflow-name="workflowName"
29+
v-model:initial-options="views.get(id).initialOptions"
30+
class="h-100"
31+
/>
32+
</WidgetComponent>
3433
</template>
3534

3635
<script setup>
@@ -42,7 +41,8 @@ import {
4241
} from 'vue'
4342
import { useStore } from 'vuex'
4443
import { startCase, uniqueId } from 'lodash'
45-
import LuminoWidget from '@/components/cylc/workflow/lumino-widget'
44+
import WidgetComponent from '@/components/cylc/workspace/Widget.vue'
45+
import LuminoWidget from '@/components/cylc/workspace/lumino-widget'
4646
import { BoxPanel, DockPanel, Widget } from '@lumino/widgets'
4747
import { when } from '@/utils'
4848
import { useDefaultView } from '@/views/views'
File renamed without changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
<!-- Component which is displayed in a lumino tab. -->
19+
20+
<template>
21+
<!-- Teleport to the div for the widget (which gets created & attached to dock panel separately) -->
22+
<Teleport :to="`#${id}`">
23+
<slot />
24+
</Teleport>
25+
</template>
26+
27+
<script setup>
28+
import { provide, readonly, ref } from 'vue'
29+
import { eventBus } from '@/services/eventBus'
30+
31+
const props = defineProps({
32+
id: {
33+
type: String,
34+
required: true,
35+
},
36+
})
37+
38+
/**
39+
* Ref used to indicate to children that the widget has become unhidden, and
40+
* that any task progress CSS animations should be given a new delay value
41+
* to maintain their state.
42+
*
43+
* This is because when an element goes from display:none to visible, CSS
44+
* animations start from scratch.
45+
* @see https://stackoverflow.com/a/37671302/3217306
46+
*/
47+
const animResetTime = ref(Date.now())
48+
provide('animResetTime', readonly(animResetTime))
49+
50+
eventBus.on(`lumino:show:${props.id}`, () => {
51+
animResetTime.value = Date.now()
52+
})
53+
</script>

src/components/cylc/workflow/lumino-widget.js renamed to src/components/cylc/workspace/lumino-widget.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,10 @@ export default class LuminoWidget extends Widget {
8181
eventBus.emit('lumino:deleted', this.id)
8282
super.onCloseRequest(msg)
8383
}
84+
85+
onAfterShow (msg) {
86+
// Emit an event so that the Vue component knows that this widget is visible again
87+
eventBus.emit(`lumino:show:${this.id}`)
88+
super.onAfterShow(msg)
89+
}
8490
}

src/views/Workspace.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ import { getPageTitle } from '@/utils/index'
4343
import graphqlMixin from '@/mixins/graphql'
4444
import subscriptionMixin from '@/mixins/subscription'
4545
import ViewState from '@/model/ViewState.model'
46-
import Lumino from '@/components/cylc/workflow/Lumino.vue'
47-
import Toolbar from '@/components/cylc/workflow/Toolbar.vue'
46+
import Lumino from '@/components/cylc/workspace/Lumino.vue'
47+
import Toolbar from '@/components/cylc/workspace/Toolbar.vue'
4848
import { toolbarHeight } from '@/utils/toolbar'
4949
5050
export default {

tests/unit/components/cylc/workflow/toolbar.vue.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { createVuetify } from 'vuetify'
1919
import { createStore } from 'vuex'
2020
import { mount } from '@vue/test-utils'
2121
import storeOptions from '@/store/options'
22-
import Toolbar from '@/components/cylc/workflow/Toolbar.vue'
22+
import Toolbar from '@/components/cylc/workspace/Toolbar.vue'
2323
import CylcObjectPlugin from '@/components/cylc/cylcObject/plugin'
2424
import sinon from 'sinon'
2525
import WorkflowService from '@/services/workflow.service'

0 commit comments

Comments
 (0)