Skip to content

Commit

Permalink
Merge pull request #1802 from MetRonnie/task-animation
Browse files Browse the repository at this point in the history
Maintain state of task progress animation when workspace tabs are hidden & shown
  • Loading branch information
oliver-sanders authored May 23, 2024
2 parents 074a0a4 + c44944a commit 5fd5523
Show file tree
Hide file tree
Showing 16 changed files with 230 additions and 198 deletions.
1 change: 1 addition & 0 deletions changes.d/1802.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed bug where task progress animations would reset when switching between tabs in the workspace.
10 changes: 6 additions & 4 deletions cypress/component/cylc-icons.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,15 @@ describe('Task component', () => {
.should('have.css', 'animation-duration', `${MEAN_ELAPSED_TIME}s`)
// the offset should be set to the "percent" of the expected job duration
.should('have.css', 'animation-delay')
.and('match', /([\d.]+)s/) // NOTE the delay should be negative
.and('match', /([\d.-]+)s/)
.then((number) => {
// convert the duration string into a number that we can test
cy.wrap(Number(number.match(/([\d.]+)s/)[1]))
expect(parseInt(number)).to.be.closeTo(
// ensure this number is ±5 from the expected value
// (give it a little bit of margin to allow for timing error)
.should('closeTo', MEAN_ELAPSED_TIME * (percent / 100), 5)
// NOTE the delay should be negative
-MEAN_ELAPSED_TIME * (percent / 100),
5
)
})
}
})
Expand Down
20 changes: 9 additions & 11 deletions cypress/component/utils/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@
export const MEAN_ELAPSED_TIME = 10000

export function getStartTime (percent) {
return String(
new Date(
// the current time in ms
Date.now() -
// minus the elapsed time in ms
(
(MEAN_ELAPSED_TIME * 1000) *
(percent / 100)
)
).toISOString()
)
return new Date(
// the current time in ms
Date.now() -
// minus the elapsed time in ms
(
(MEAN_ELAPSED_TIME * 1000) *
(percent / 100)
)
).toISOString()
}
171 changes: 86 additions & 85 deletions src/components/cylc/SVGTask.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<g
class="c8-task"
:class="{
waiting: task.state === 'waiting',
preparing: task.state === 'preparing',
submitted: task.state === 'submitted',
running: task.state === 'running',
succeeded: task.state === 'succeeded',
failed: task.state === 'failed',
'submit-failed': task.state === 'submit-failed',
expired: task.state === 'expired',
[task.state]: true,
held: task.isHeld,
queued: task.isQueued && !task.isHeld,
runahead: task.isRunahead && !(task.isHeld || task.isQueued),
Expand All @@ -48,7 +41,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
The circle outline of the task icon.
NOTE: If changing the radius or stroke of the circle then the values
in getModiferTransform must be updated.
in getModifierTransform must be updated.
-->
<circle
class="outline"
Expand Down Expand Up @@ -76,7 +69,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
r="16"
stroke-width="50"
stroke-dasharray="157"
:style="getRunningStyle()"
:style="runningStyle"
/>
</g>
<!-- dot
Expand Down Expand Up @@ -163,15 +156,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<g
class="modifier"
:transform="getModiferTransform()"
:transform="modifierTransform"
>
<!-- modifier
The circle outline of the modifier which is displayed for some modifier
states.
NOTE: If changing the radius or stroke of the circle then the values
in getModiferTransform must be updated to match.
in getModifierTransform must be updated to match.
-->
<circle
class="outline"
Expand Down Expand Up @@ -253,84 +246,86 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</g>
</template>

<script>
<script setup>
import { computed, inject, ref } from 'vue'
import TaskState from '@/model/TaskState.model'
export default {
name: 'SVGTask',
props: {
task: {
required: true
},
startTime: {
// The start time as an ISO8601 date-time string in expanded format
// e.g. 2022-10-26T13:43:45Z
// TODO: aim to remove this in due course
// (we should be able to obtain this directly from the task)
type: String,
required: false
},
modifierSize: {
// Scale the size of the task state modifier
type: Number,
default: 0.7
},
const props = defineProps({
task: {
required: true
},
methods: {
getRunningStyle () {
if (
this.task.state === TaskState.RUNNING.name &&
this.startTime &&
this.task.task?.meanElapsedTime
) {
// job start time in ms
const startTime = Date.parse(this.startTime)
// current time in ms
const now = Date.now()
// job elapsed time in s
const elapsedTime = ((now - startTime) / 1000)
const ret = `
animation-name: c8-task-progress-animation;
animation-timing-function: steps(50);
animation-iteration-count: 1;
animation-duration: ${this.task.task.meanElapsedTime}s;
animation-delay: -${elapsedTime}s;
animation-fill-mode: forwards;
`
return ret.replace('\n', ' ')
}
return ''
},
getModiferTransform () {
// Returns the translation required to position the ".modifier" nicely in
// relation to the ".status".
// Both ".status" and ".modifier" are centered at (50, 50), we need to
// move ".modifier" up and to the left so that the two don't touch and
// have a sensible gap between them
// translation = -(
// # (1) the x/y translation to the edge of ".modifier"
// (
// (.modifier.outline.width + .modifier.outline.stroke)
// * modifierSize * sin(45)
// )
// # (2) the x/y translation to the edge of ".status"
// (.status.outline.width + .status.outline.stroke) * sin(45)
// )
const translation = -(
// (1) the x/y translation to the edge of ".modifier"
(35.35 * this.modifierSize) +
// (2) the x/y translation to the edge of ".status"
42.42
)
return `
scale(${this.modifierSize}, ${this.modifierSize})
translate(${translation}, ${translation})
`
},
startTime: {
// The start time as an ISO8601 date-time string in expanded format
// e.g. 2022-10-26T13:43:45Z
// TODO: aim to remove this in due course
// (we should be able to obtain this directly from the task)
type: String,
required: false
},
modifierSize: {
// Scale the size of the task state modifier
type: Number,
default: 0.7
},
})
/**
* @type {import('vue').Ref<number>}
* @see @/components/cylc/workspace/Widget.vue
*/
const animResetTime = inject('animResetTime', () => ref(0), true)
const runningStyle = computed(() => {
if (
props.task.state === TaskState.RUNNING.name &&
props.startTime &&
props.task.task?.meanElapsedTime
) {
// current time in ms (UTC); updates whenever widget is unhidden
const now = Math.max(Date.now(), animResetTime.value)
// job elapsed time in ms
const elapsedTime = now - Date.parse(props.startTime)
return {
animationDuration: `${props.task.task.meanElapsedTime}s`,
animationDelay: `-${elapsedTime}ms`,
animationFillMode: 'forwards',
}
}
return {}
})
/**
* Returns the translation required to position the ".modifier" nicely in
* relation to the ".status".
*
* Both ".status" and ".modifier" are centered at (50, 50), we need to
* move ".modifier" up and to the left so that the two don't touch and
* have a sensible gap between them.
*/
function _getModifierTransform () {
// translation = -(
// # (1) the x/y translation to the edge of ".modifier"
// (
// (.modifier.outline.width + .modifier.outline.stroke)
// * modifierSize * sin(45)
// )
// # (2) the x/y translation to the edge of ".status"
// (.status.outline.width + .status.outline.stroke) * sin(45)
// )
const translation = -(
// (1) the x/y translation to the edge of ".modifier"
(35.35 * props.modifierSize) +
// (2) the x/y translation to the edge of ".status"
42.42
)
return `
scale(${props.modifierSize}, ${props.modifierSize})
translate(${translation}, ${translation})
`
}
// Doesn't need to be reactive:
const modifierTransform = _getModifierTransform()
</script>
<style lang="scss">
Expand Down Expand Up @@ -460,6 +455,12 @@ export default {
fill: $foreground;
}
}
&.running .progress {
animation-name: c8-task-progress-animation;
animation-timing-function: steps(50);
animation-iteration-count: 1;
}
}
@keyframes c8-task-progress-animation {
Expand Down
7 changes: 4 additions & 3 deletions src/components/cylc/commandMenu/Menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ import {
} from '@mdi/js'
import { mapGetters, mapState } from 'vuex'
import WorkflowState from '@/model/WorkflowState.model'
import { eventBus } from '@/services/eventBus'
export default {
name: 'CommandMenu',
Expand Down Expand Up @@ -155,11 +156,11 @@ export default {
},
mounted () {
this.$eventBus.on('show-mutations-menu', this.showMutationsMenu)
eventBus.on('show-mutations-menu', this.showMutationsMenu)
},
beforeUnmount () {
this.$eventBus.off('show-mutations-menu', this.showMutationsMenu)
eventBus.off('show-mutations-menu', this.showMutationsMenu)
},
computed: {
Expand Down Expand Up @@ -258,7 +259,7 @@ export default {
workflowName: this.node.tokens.workflow
}
}).then(() => {
this.$eventBus.emit(
eventBus.emit(
'add-view',
{
name: 'Log',
Expand Down
3 changes: 2 additions & 1 deletion src/components/cylc/commandMenu/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

import { uniqueId } from 'lodash-es'
import { eventBus } from '@/services/eventBus'

/** Reference to closure listeners (needed as we are using variables from another scope) */
const listeners = new WeakMap()
Expand All @@ -26,7 +27,7 @@ function bind (el, binding, vnode) {
el.dataset.cInteractive = uniqueId()
const listener = function (e) {
e.stopPropagation() // prevents click event from bubbling up to parents
binding.instance.$eventBus.emit('show-mutations-menu', {
eventBus.emit('show-mutations-menu', {
node: binding.value,
target: el,
})
Expand Down
Loading

0 comments on commit 5fd5523

Please sign in to comment.