// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance // with the License. A copy of the License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and // limitations under the License. import {AccountingJobSummary} from '../../types/jobs' import React from 'react' import {useCollection} from '@cloudscape-design/collection-hooks' import {useState, getState, setState, clearState} from '../../store' import {SlurmAccounting} from '../../model' import {clusterDefaultUser, getIn, findFirst} from '../../util' // Components import {JobStatusIndicator} from '../../components/Status' import EmptyState from '../../components/EmptyState' import Loading from '../../components/Loading' // UI Elements import { Box, Button, ColumnLayout, Container, DateRangePicker, FormField, Header, Input, Link, Modal, Select, Pagination, SpaceBetween, Table, TextFilter, } from '@cloudscape-design/components' import InfoLink from '../../components/InfoLink' import TitleDescriptionHelpPanel from '../../components/help-panel/TitleDescriptionHelpPanel' import {useTranslation} from 'react-i18next' // Key:Value pair (label / children) const ValueWithLabel = ({label, children}: any) => (
{label}
{children}
) function refreshAccounting(args: any, callback: any, list: any) { const startTime = getState(['app', 'clusters', 'accounting', 'startTime']) const endTime = getState(['app', 'clusters', 'accounting', 'endTime']) const queue = getState(['app', 'clusters', 'accounting', 'queue']) const nodes = getState(['app', 'clusters', 'accounting', 'nodes']) const clusterUser = getState(['app', 'clusters', 'accounting', 'user']) const jobName = getState(['app', 'clusters', 'accounting', 'jobName']) const jobState = getState(['app', 'clusters', 'accounting', 'jobState']) const clusterName = getState(['app', 'clusters', 'selected']) const defaultArgs = !args || Object.keys(args).length === 0 ? {} : args if (startTime && startTime !== '') defaultArgs['starttime'] = startTime if (endTime && endTime !== '') defaultArgs['endtime'] = endTime if (queue) defaultArgs['partition'] = queue if (nodes && nodes !== '') defaultArgs['nodelist'] = nodes if (jobName && jobName !== '') defaultArgs['name'] = jobName if (clusterUser && clusterUser !== '') defaultArgs['user'] = clusterUser if (jobState && jobState !== '') defaultArgs['state'] = jobState if (list) clearState(['clusters', 'index', clusterName, 'accounting', 'jobs']) setState(['app', 'clusters', 'accounting', 'pending'], true) const defaultCallback = (data: any) => { if (list) setState(['clusters', 'index', clusterName, 'accounting'], data) else callback && callback(data) clearState(['app', 'clusters', 'accounting', 'pending']) } const failCallback = (data: any) => { console.log('fail: ', data) clearState(['app', 'clusters', 'accounting', 'pending']) } if (clusterName) { const clusterPath = ['clusters', 'index', clusterName] const cluster = getState(clusterPath) let user = clusterDefaultUser(cluster) const headNode = getState([...clusterPath, 'headNode']) headNode && SlurmAccounting( clusterName, headNode.instanceId, user, defaultArgs, defaultCallback, failCallback, ) } } function JobStateSelect() { const jobStates = [ 'BOOT_FAIL', 'CANCELLED', 'COMPLETED', 'DEADLINE', 'FAILED', 'NODE_FAIL', 'OUT_OF_MEMORY', 'PENDING', 'PREEMPTED', 'RUNNING', 'REQUEUED', 'RESIZING', 'REVOKED', 'SUSPENDED', 'TIMEOUT', ] const jobStatePath = ['app', 'clusters', 'accounting', 'jobState'] const jobState = useState(jobStatePath) let itemToOption = (item: any) => { if (!item) return return {label:
{item}
, value: item} } return ( <> { return x[0] === queue }) || ['[ANY]', '[ANY]'], )} onChange={({detail}) => { if (detail.selectedOption.value === '[ANY]') clearState(queuePath) else setState(queuePath, detail.selectedOption.value) refreshAccounting({}, null, true) }} // @ts-expect-error TS(2322) FIXME: Type '({ label: Element; value: any; } | undefined... Remove this comment to see the full error message options={queuesOptions.map(itemToOption)} selectedAriaLabel="Selected" /> ) } function JobStep({step}: any) { const reqs = getIn(step, ['tres', 'requested', 'max']) || [] return ( {reqs .filter((req: any) => req.type !== 'energy') .map((req: any) => ( {req.type}: {req.count}{' '} ))} ) } function JobSteps({job}: any) { const steps = job.steps || [] return (
{steps.map((step: any, i: any) => (
{i}:
))}
) } function CostEstimate({job}: any) { const {t} = useTranslation() return ( <>
${' '} {( getIn(job, ['price_estimate']) * getIn(job, ['allocation_nodes']) * (getIn(job, ['time', 'elapsed']) / 3600.0) ).toFixed(5)} } />
) } function JobProperties({job}: any) { console.log(job) return ( {job.job_id} {job.cluster} {job.group} {job.user} {getIn(job, ['time', 'elapsed'])} s {} {job.name} {job.nodes} {job.account} {job.allocation_nodes} {job.partition} {getIn(job, ['exit_code', 'return_code'])} { } {getIn(job, ['price_estimate']) && ( )} {} ) } function JobModal() { const clusterName = useState(['app', 'clusters', 'selected']) const open = useState([ 'clusters', 'index', clusterName, 'accounting', 'dialog', ]) const selectedJob = useState([ 'clusters', 'index', clusterName, 'accounting', 'selectedJob', ]) const job = useState([ 'clusters', 'index', clusterName, 'accounting', 'job', selectedJob, ]) const close = () => { setState(['clusters', 'index', clusterName, 'accounting', 'dialog'], false) } return ( } header={`Job Info: ${job ? job.name : ''}`} > {job && } {!job &&
Loading...
}
) } export default function ClusterAccounting() { const {t} = useTranslation() const clusterName = useState(['app', 'clusters', 'selected']) //const accounting = useState(['clusters', 'index', clusterName, 'accounting']); //const errors = useState(['clusters', 'index', clusterName, 'accounting', 'errors']) || []; const pending = useState(['app', 'clusters', 'accounting', 'pending']) //const startTime = useState(['app', 'clusters', 'accounting', 'startTime']) || ''; //const endTime = useState(['app', 'clusters', 'accounting', 'endTime']) || ''; const nodes = useState(['app', 'clusters', 'accounting', 'nodes']) || [] const user = useState(['app', 'clusters', 'accounting', 'user']) || '' const jobName = useState(['app', 'clusters', 'accounting', 'jobName']) || [] const jobs: AccountingJobSummary[] = useState([ 'clusters', 'index', clusterName, 'accounting', 'jobs', ]) React.useEffect(() => { refreshAccounting({}, null, true) }, []) const setDateRange = (val: any) => { if (!val) { clearState(['app', 'clusters', 'accounting', 'startTime']) clearState(['app', 'clusters', 'accounting', 'endTime']) } else { if (val.type === 'relative') { if (val.unit === 'month') { setState( ['app', 'clusters', 'accounting', 'startTime'], `now-${val.amount * 4}weeks`, ) setState(['app', 'clusters', 'accounting', 'endTime'], 'now') } else if (val.unit === 'year') { setState( ['app', 'clusters', 'accounting', 'startTime'], `now-${val.amount * 52}weeks`, ) setState(['app', 'clusters', 'accounting', 'endTime'], 'now') } else { setState( ['app', 'clusters', 'accounting', 'startTime'], `now-${val.amount}${val.unit}s`, ) setState(['app', 'clusters', 'accounting', 'endTime'], 'now') } } else { const start = new Date(val.startDate) const end = new Date(val.endDate) setState( ['app', 'clusters', 'accounting', 'startTime'], start.toISOString().substring(0, 19), ) setState( ['app', 'clusters', 'accounting', 'endTime'], end.toISOString().substring(0, 19), ) } } refreshAccounting({}, null, true) } const { items, actions, filteredItemsCount, collectionProps, filterProps, paginationProps, } = useCollection(jobs || [], { filtering: { empty: , noMatch: ( actions.setFiltering('')}> Clear filter } /> ), }, pagination: {pageSize: 10}, sorting: {}, selection: {}, }) const selectJob = (job_id: any) => { setState(['clusters', 'index', clusterName, 'accounting', 'dialog'], true) clearState(['clusters', 'index', clusterName, 'accounting', 'selectedJob']) clearState(['clusters', 'index', clusterName, 'accounting', 'job', job_id]) refreshAccounting( {jobs: job_id}, (ret: any) => { setState( ['clusters', 'index', clusterName, 'accounting', 'selectedJob'], job_id, ) setState( ['clusters', 'index', clusterName, 'accounting', 'job', job_id], ret.jobs[0], ) }, false, ) } const [dateValue, setDateValue] = React.useState(undefined) return ( <> refreshAccounting({}, null, true)} > Refresh } > Filters } >
e.key === 'Enter' && refreshAccounting({}, null, true) } > { setDateRange(detail.value) /* @ts-expect-error TS(2345) FIXME: Argument of type 'Value | null' is not assignable ... Remove this comment to see the full error message */ setDateValue(detail.value) }} /* @ts-expect-error FIXME: Argument of type 'Value | null' is not assignable ... Remove this comment to see the full error message */ value={dateValue} relativeOptions={[ { key: 'previous-5-minutes', amount: 5, unit: 'minute', type: 'relative', }, { key: 'previous-30-minutes', amount: 30, unit: 'minute', type: 'relative', }, { key: 'previous-1-hour', amount: 1, unit: 'hour', type: 'relative', }, { key: 'previous-6-hours', amount: 6, unit: 'hour', type: 'relative', }, ]} i18nStrings={{ todayAriaLabel: 'Today', nextMonthAriaLabel: 'Next month', previousMonthAriaLabel: 'Previous month', customRelativeRangeDurationLabel: 'Duration', customRelativeRangeDurationPlaceholder: 'Enter duration', customRelativeRangeOptionLabel: 'Custom range', customRelativeRangeOptionDescription: 'Set a custom range in the past', customRelativeRangeUnitLabel: 'Unit of time', formatRelativeRange: e => { const t = 1 === e.amount ? e.unit : `${e.unit}s` return `Last ${e.amount} ${t}` }, formatUnit: (e, t) => (1 === t ? e : `${e}s`), dateTimeConstraintText: 'Range must be between 6 - 30 days. Use 24 hour format.', relativeModeTitle: 'Relative range', absoluteModeTitle: 'Absolute range', relativeRangeSelectionHeading: 'Choose a range', startDateLabel: 'Start date', endDateLabel: 'End date', startTimeLabel: 'Start time', endTimeLabel: 'End time', clearButtonLabel: 'Clear', cancelButtonLabel: 'Cancel', applyButtonLabel: 'Apply', }} placeholder="Filter by a date and time range" />
e.key === 'Enter' && refreshAccounting({}, null, true) } > { setState( ['app', 'clusters', 'accounting', 'user'], detail.value, ) }} />
e.key === 'Enter' && refreshAccounting({}, null, true) } > { setState( ['app', 'clusters', 'accounting', 'nodes'], detail.value, ) }} />
e.key === 'Enter' && refreshAccounting({}, null, true) } > { setState( ['app', 'clusters', 'accounting', 'jobName'], detail.value, ) }} />
{jobs ? ( `${i.job_id}-${i.name}`} columnDefinitions={[ { id: 'id', header: t('cluster.accounting.id'), cell: job => ( selectJob(job.job_id)}> {job.job_id} ), sortingField: 'job_id', }, { id: 'name', header: t('cluster.accounting.name'), cell: job => job.name, sortingField: 'name', }, { id: 'queue', header: t('cluster.accounting.queue'), cell: job => job.partition, sortingField: 'partition', }, { id: 'user', header: t('cluster.accounting.user'), cell: job => job.user, sortingField: 'user', }, { id: 'state', header: t('cluster.accounting.state'), cell: job => ( ), sortingField: 'job_state', }, ]} items={items} loadingText={t('cluster.accounting.loadingJobs')} pagination={} filter={ } /> ) : (
)} ) }