123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- <template lang='pug'>
- q-page.admin-terminal
- .row.q-pa-md.items-center
- .col-auto
- img.admin-icon.animated.fadeInLeft(src='/_assets/icons/fluent-bot.svg')
- .col.q-pl-md
- .text-h5.text-primary.animated.fadeInLeft {{ t('admin.scheduler.title') }}
- .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.scheduler.subtitle') }}
- .col-auto.flex
- q-btn-toggle.q-mr-md(
- v-model='state.displayMode'
- push
- no-caps
- :disable='state.loading > 0'
- :toggle-color='$q.dark.isActive ? `white` : `black`'
- :toggle-text-color='$q.dark.isActive ? `black` : `white`'
- :text-color='$q.dark.isActive ? `white` : `black`'
- :color='$q.dark.isActive ? `dark-1` : `white`'
- :options=`[
- { label: t('admin.scheduler.schedule'), value: 'scheduled' },
- { label: t('admin.scheduler.upcoming'), value: 'upcoming' },
- { label: t('admin.scheduler.active'), value: 'active' },
- { label: t('admin.scheduler.completed'), value: 'completed' },
- { label: t('admin.scheduler.failed'), value: 'failed' },
- ]`
- )
- q-separator.q-mr-md(vertical)
- q-btn.q-mr-sm.acrylic-btn(
- icon='las la-question-circle'
- flat
- color='grey'
- :href='siteStore.docsBase + `/admin/scheduler`'
- target='_blank'
- type='a'
- )
- q-btn.q-mr-sm.acrylic-btn(
- icon='las la-redo-alt'
- flat
- color='secondary'
- :loading='state.loading > 0'
- @click='load'
- )
- q-separator(inset)
- .q-pa-md.q-gutter-md
- template(v-if='state.displayMode === `scheduled`')
- q-card.rounded-borders(
- v-if='state.scheduledJobs.length < 1'
- flat
- :class='$q.dark.isActive ? `bg-dark-5 text-white` : `bg-grey-3 text-dark`'
- )
- q-card-section.items-center(horizontal)
- q-card-section.col-auto.q-pr-none
- q-icon(name='las la-info-circle', size='sm')
- q-card-section.text-caption {{ t('admin.scheduler.scheduledNone') }}
- q-card.shadow-1(v-else)
- q-table(
- :rows='state.scheduledJobs'
- :columns='scheduledJobsHeaders'
- row-key='name'
- flat
- hide-bottom
- :rows-per-page-options='[0]'
- :loading='state.loading > 0'
- )
- template(v-slot:body-cell-id='props')
- q-td(:props='props')
- q-spinner-clock.q-mr-sm(
- color='indigo'
- size='xs'
- )
- template(v-slot:body-cell-task='props')
- q-td(:props='props')
- strong {{props.value}}
- div: small.text-grey {{props.row.id}}
- template(v-slot:body-cell-cron='props')
- q-td(:props='props')
- q-chip(
- square
- size='md'
- color='blue'
- text-color='white'
- )
- span.font-robotomono {{ props.value }}
- template(v-slot:body-cell-type='props')
- q-td(:props='props')
- q-chip(
- square
- size='md'
- dense
- color='deep-orange'
- text-color='white'
- )
- small.text-uppercase {{ props.value }}
- template(v-slot:body-cell-created='props')
- q-td(:props='props')
- span {{props.value}}
- div: small.text-grey {{humanizeDate(props.row.createdAt)}}
- template(v-slot:body-cell-updated='props')
- q-td(:props='props')
- span {{props.value}}
- div: small.text-grey {{humanizeDate(props.row.updatedAt)}}
- template(v-else-if='state.displayMode === `upcoming`')
- q-card.rounded-borders(
- v-if='state.upcomingJobs.length < 1'
- flat
- :class='$q.dark.isActive ? `bg-dark-5 text-white` : `bg-grey-3 text-dark`'
- )
- q-card-section.items-center(horizontal)
- q-card-section.col-auto.q-pr-none
- q-icon(name='las la-info-circle', size='sm')
- q-card-section.text-caption {{ t('admin.scheduler.upcomingNone') }}
- q-card.shadow-1(v-else)
- q-table(
- :rows='state.upcomingJobs'
- :columns='upcomingJobsHeaders'
- row-key='name'
- flat
- hide-bottom
- :rows-per-page-options='[0]'
- :loading='state.loading > 0'
- )
- template(v-slot:body-cell-id='props')
- q-td(:props='props')
- q-icon(name='las la-clock', color='primary', size='sm')
- template(v-slot:body-cell-task='props')
- q-td(:props='props')
- strong {{props.value}}
- div: small.text-grey {{props.row.id}}
- template(v-slot:body-cell-waituntil='props')
- q-td(:props='props')
- span {{ props.value }}
- div: small.text-grey {{humanizeDate(props.row.waitUntil)}}
- template(v-slot:body-cell-retries='props')
- q-td(:props='props')
- span #[strong {{props.value + 1}}] #[span.text-grey / {{props.row.maxRetries}}]
- template(v-slot:body-cell-useworker='props')
- q-td(:props='props')
- template(v-if='props.value')
- q-icon(name='las la-microchip', color='brown', size='sm')
- small.q-ml-xs.text-brown Worker
- template(v-else)
- q-icon(name='las la-leaf', color='teal', size='sm')
- small.q-ml-xs.text-teal In-Process
- template(v-slot:body-cell-date='props')
- q-td(:props='props')
- span {{props.value}}
- div
- i18n-t.text-grey(keypath='admin.scheduler.createdBy', tag='small')
- template(#instance)
- strong {{props.row.createdBy}}
- template(v-else)
- q-card.rounded-borders(
- v-if='state.jobs.length < 1'
- flat
- :class='$q.dark.isActive ? `bg-dark-5 text-white` : `bg-grey-3 text-dark`'
- )
- q-card-section.items-center(horizontal)
- q-card-section.col-auto.q-pr-none
- q-icon(name='las la-info-circle', size='sm')
- q-card-section.text-caption {{ t('admin.scheduler.' + state.displayMode + 'None') }}
- q-card.shadow-1(v-else)
- q-table(
- :rows='state.jobs'
- :columns='jobsHeaders'
- row-key='name'
- flat
- hide-bottom
- :rows-per-page-options='[0]'
- :loading='state.loading > 0'
- )
- template(v-slot:body-cell-id='props')
- q-td(:props='props')
- q-avatar(
- v-if='props.row.state === `completed`'
- icon='las la-check'
- color='positive'
- text-color='white'
- size='sm'
- rounded
- )
- q-avatar(
- v-else-if='props.row.state === `failed`'
- icon='las la-times'
- color='negative'
- text-color='white'
- size='sm'
- rounded
- )
- q-avatar(
- v-else-if='props.row.state === `interrupted`'
- icon='las la-square-full'
- color='orange'
- text-color='white'
- size='sm'
- rounded
- )
- q-circular-progress(
- v-else-if='props.row.state === `active`'
- indeterminate
- size='sm'
- :thickness='0.4'
- color='blue'
- track-color='blue-1'
- center-color='blue-2'
- )
- template(v-slot:body-cell-task='props')
- q-td(:props='props')
- strong {{props.value}}
- div: small.text-grey {{props.row.id}}
- template(v-slot:body-cell-state='props')
- q-td(:props='props')
- template(v-if='props.value === `completed`')
- i18n-t(keypath='admin.scheduler.completedIn', tag='span')
- template(#duration)
- strong {{humanizeDuration(props.row.startedAt, props.row.completedAt)}}
- div: small.text-grey {{ humanizeDate(props.row.completedAt) }}
- template(v-else-if='props.value === `active`')
- em.text-grey {{ t('admin.scheduler.pending') }}
- template(v-else)
- strong.text-negative {{ props.value === 'failed' ? t('admin.scheduler.error') : t('admin.scheduler.interrupted') }}
- div: small {{ props.row.lastErrorMessage }}
- template(v-slot:body-cell-attempt='props')
- q-td(:props='props')
- span #[strong {{props.value}}] #[span.text-grey / {{props.row.maxRetries}}]
- template(v-slot:body-cell-useworker='props')
- q-td(:props='props')
- template(v-if='props.value')
- q-icon(name='las la-microchip', color='brown', size='sm')
- small.q-ml-xs.text-brown Worker
- template(v-else)
- q-icon(name='las la-leaf', color='teal', size='sm')
- small.q-ml-xs.text-teal In-Process
- template(v-slot:body-cell-date='props')
- q-td(:props='props')
- span {{props.value}}
- div: small.text-grey {{humanizeDate(props.row.startedAt)}}
- div
- i18n-t.text-grey(keypath='admin.scheduler.createdBy', tag='small')
- template(#instance)
- strong {{props.row.executedBy}}
- </template>
- <script setup>
- import { onMounted, reactive, watch } from 'vue'
- import { useMeta, useQuasar } from 'quasar'
- import { useI18n } from 'vue-i18n'
- import gql from 'graphql-tag'
- import { DateTime, Duration, Interval } from 'luxon'
- import { useSiteStore } from 'src/stores/site'
- // QUASAR
- const $q = useQuasar()
- // STORES
- const siteStore = useSiteStore()
- // I18N
- const { t } = useI18n()
- // META
- useMeta({
- title: t('admin.scheduler.title')
- })
- // DATA
- const state = reactive({
- displayMode: 'completed',
- scheduledJobs: [],
- upcomingJobs: [],
- jobs: [],
- loading: 0
- })
- const scheduledJobsHeaders = [
- {
- align: 'center',
- field: 'id',
- name: 'id',
- sortable: false,
- style: 'width: 15px; padding-right: 0;'
- },
- {
- label: t('common.field.task'),
- align: 'left',
- field: 'task',
- name: 'task',
- sortable: true
- },
- {
- label: t('admin.scheduler.cron'),
- align: 'left',
- field: 'cron',
- name: 'cron',
- sortable: true
- },
- {
- label: t('admin.scheduler.type'),
- align: 'left',
- field: 'type',
- name: 'type',
- sortable: true
- },
- {
- label: t('admin.scheduler.createdAt'),
- align: 'left',
- field: 'createdAt',
- name: 'created',
- sortable: true,
- format: v => DateTime.fromISO(v).toRelative()
- },
- {
- label: t('admin.scheduler.updatedAt'),
- align: 'left',
- field: 'updatedAt',
- name: 'updated',
- sortable: true,
- format: v => DateTime.fromISO(v).toRelative()
- }
- ]
- const upcomingJobsHeaders = [
- {
- align: 'center',
- field: 'id',
- name: 'id',
- sortable: false,
- style: 'width: 15px; padding-right: 0;'
- },
- {
- label: t('common.field.task'),
- align: 'left',
- field: 'task',
- name: 'task',
- sortable: true
- },
- {
- label: t('admin.scheduler.waitUntil'),
- align: 'left',
- field: 'waitUntil',
- name: 'waituntil',
- sortable: true,
- format: v => DateTime.fromISO(v).toRelative()
- },
- {
- label: t('admin.scheduler.attempt'),
- align: 'left',
- field: 'retries',
- name: 'retries',
- sortable: true
- },
- {
- label: t('admin.scheduler.useWorker'),
- align: 'left',
- field: 'useWorker',
- name: 'useworker',
- sortable: true
- },
- {
- label: t('admin.scheduler.scheduled'),
- align: 'left',
- field: 'createdAt',
- name: 'date',
- sortable: true,
- format: v => DateTime.fromISO(v).toRelative()
- }
- ]
- const jobsHeaders = [
- {
- align: 'center',
- field: 'id',
- name: 'id',
- sortable: false,
- style: 'width: 15px; padding-right: 0;'
- },
- {
- label: t('common.field.task'),
- align: 'left',
- field: 'task',
- name: 'task',
- sortable: true
- },
- {
- label: t('admin.scheduler.result'),
- align: 'left',
- field: 'state',
- name: 'state',
- sortable: true
- },
- {
- label: t('admin.scheduler.attempt'),
- align: 'left',
- field: 'attempt',
- name: 'attempt',
- sortable: true
- },
- {
- label: t('admin.scheduler.useWorker'),
- align: 'left',
- field: 'useWorker',
- name: 'useworker',
- sortable: true
- },
- {
- label: t('admin.scheduler.startedAt'),
- align: 'left',
- field: 'startedAt',
- name: 'date',
- sortable: true,
- format: v => DateTime.fromISO(v).toRelative()
- }
- ]
- // WATCHERS
- watch(() => state.displayMode, (newValue) => {
- load()
- })
- // METHODS
- function humanizeDate (val) {
- return DateTime.fromISO(val).toFormat('fff')
- }
- function humanizeDuration (start, end) {
- const dur = Interval.fromDateTimes(DateTime.fromISO(start), DateTime.fromISO(end))
- .toDuration(['hours', 'minutes', 'seconds', 'milliseconds'])
- return Duration.fromObject({
- ...dur.hours > 0 && { hours: dur.hours },
- ...dur.minutes > 0 && { minutes: dur.minutes },
- ...dur.seconds > 0 && { seconds: dur.seconds },
- ...dur.milliseconds > 0 && { milliseconds: dur.milliseconds }
- }).toHuman({ unitDisplay: 'narrow', listStyle: 'short' })
- }
- async function load () {
- state.loading++
- try {
- if (state.displayMode === 'scheduled') {
- const resp = await APOLLO_CLIENT.query({
- query: gql`
- query getSystemJobsScheduled {
- systemJobsScheduled {
- id
- task
- cron
- type
- createdAt
- updatedAt
- }
- }
- `,
- fetchPolicy: 'network-only'
- })
- state.scheduledJobs = resp?.data?.systemJobsScheduled
- } else if (state.displayMode === 'upcoming') {
- const resp = await APOLLO_CLIENT.query({
- query: gql`
- query getSystemJobsUpcoming {
- systemJobsUpcoming {
- id
- task
- useWorker
- retries
- maxRetries
- waitUntil
- isScheduled
- createdBy
- createdAt
- updatedAt
- }
- }
- `,
- fetchPolicy: 'network-only'
- })
- state.upcomingJobs = resp?.data?.systemJobsUpcoming
- } else {
- const states = state.displayMode === 'failed' ? ['FAILED', 'INTERRUPTED'] : [state.displayMode.toUpperCase()]
- const resp = await APOLLO_CLIENT.query({
- query: gql`
- query getSystemJobs (
- $states: [SystemJobState]
- ) {
- systemJobs (
- states: $states
- ) {
- id
- task
- state
- useWorker
- wasScheduled
- attempt
- maxRetries
- lastErrorMessage
- executedBy
- createdAt
- startedAt
- completedAt
- }
- }
- `,
- variables: {
- states
- },
- fetchPolicy: 'network-only'
- })
- state.jobs = resp?.data?.systemJobs?.map(j => ({ ...j, state: j.state.toLowerCase() }))
- }
- } catch (err) {
- $q.notify({
- type: 'negative',
- message: 'Failed to load scheduled jobs.',
- caption: err.message
- })
- }
- state.loading--
- }
- // MOUNTED
- onMounted(() => {
- load()
- })
- </script>
|