// 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 {JobSummary, Job} from '../../types/jobs' import React from 'react' import { clearState, consoleDomain, getState, setState, ssmPolicy, useState, } from '../../store' import {QueueStatus, CancelJob, JobInfo} from '../../model' import {clusterDefaultUser, findFirst} from '../../util' import {useCollection} from '@cloudscape-design/collection-hooks' // UI Elements import { Button, Box, ColumnLayout, Container, Link, Modal, Pagination, SpaceBetween, Table, TextFilter, Header, } from '@cloudscape-design/components' // Components import {JobStatusIndicator} from '../../components/Status' import EmptyState from '../../components/EmptyState' import Loading from '../../components/Loading' import {useTranslation} from 'react-i18next' import {extendCollectionsOptions} from '../../shared/extendCollectionsOptions' // Key:Value pair (label / children) const ValueWithLabel = ({ label, children, }: { label: string children?: React.ReactNode }) => ( <div> <Box margin={{bottom: 'xxxs'}} color="text-label"> {label} </Box> <div>{children}</div> </div> ) function refreshQueues(callback?: (arg: any) => void) { const clusterName = getState(['app', 'clusters', 'selected']) const region = getState(['aws', 'region']) if (clusterName) { const clusterPath = ['clusters', 'index', clusterName] const cluster = getState(clusterPath) let user = clusterDefaultUser(cluster) const headNode = getState([...clusterPath, 'headNode']) headNode && region && QueueStatus(clusterName, headNode.instanceId, user, callback) } } function JobActions({ job, disabled, cancelCallback, }: { job: JobSummary disabled: boolean cancelCallback?: () => void }) { let pendingPath = [ 'app', 'clusters', 'queue', 'action', job.job_id, 'pending', ] const pending = useState(pendingPath) const cancelJob = (jobId: any, cancelCallback: any) => { const clusterName = getState(['app', 'clusters', 'selected']) const clusterPath = ['clusters', 'index', clusterName] const cluster = getState(clusterPath) let user = clusterDefaultUser(cluster) const headNode = getState([...clusterPath, 'headNode']) setState(pendingPath, true) CancelJob(headNode.instanceId, user, jobId, () => refreshQueues(() => { clearState(pendingPath) cancelCallback && cancelCallback() }), ) } return ( <div> {job.job_state !== 'COMPLETED' && job.job_state !== 'CANCELLED' && ( <div> <Button loading={pending} disabled={disabled} onClick={() => { cancelJob(job.job_id, cancelCallback) }} > Stop Job </Button> </div> )} </div> ) } function FileLink({path, isFile}: {path: string; isFile?: boolean}) { const clusterName = useState(['app', 'clusters', 'selected']) const clusterPath = ['clusters', 'index', clusterName] const defaultRegion = useState(['aws', 'region']) const region = useState(['app', 'selectedRegion']) || defaultRegion const headNode = useState([...clusterPath, 'headNode']) const {t} = useTranslation() if (!path) return <span>{t('cluster.scheduling.propertyNotAvailable')}</span> const linkPath = isFile ? path.slice(0, path.lastIndexOf('/')) : path return ( <a href={`${consoleDomain(region)}/systems-manager/managed-instances/${ headNode.instanceId }/file-system?region=${region}&osplatform=Linux#%7B%22path%22%3A%22${linkPath}%22%7D`} rel="noreferrer" target="_blank" > {path} </a> ) } function JobProperties({job}: {job: Job}) { return ( <Container> <ColumnLayout columns={3} variant="text-grid"> <SpaceBetween direction="vertical" size="l"> <ValueWithLabel label="Job Id">{job.JobId}</ValueWithLabel> <ValueWithLabel label="Job Name">{job.JobName}</ValueWithLabel> <ValueWithLabel label="Queue">{job.Partition}</ValueWithLabel> <ValueWithLabel label="User Id">{job.UserId}</ValueWithLabel> <ValueWithLabel label="Group Id">{job.GroupId}</ValueWithLabel> <ValueWithLabel label="Priority">{job.Priority}</ValueWithLabel> <ValueWithLabel label="Account">{job.Account}</ValueWithLabel> <ValueWithLabel label="State"> <JobStatusIndicator status={job.JobState} /> </ValueWithLabel> <ValueWithLabel label="Reason">{job.Reason}</ValueWithLabel> <ValueWithLabel label="Requeue">{job.Requeue}</ValueWithLabel> </SpaceBetween> <SpaceBetween direction="vertical" size="l"> <ValueWithLabel label="Node List">{job.NodeList}</ValueWithLabel> <ValueWithLabel label="Restarts">{job.Restarts}</ValueWithLabel> <ValueWithLabel label="Reboot">{job.Reboot}</ValueWithLabel> <ValueWithLabel label="ExitCode">{job.ExitCode}</ValueWithLabel> <ValueWithLabel label="RunTime">{job.RunTime}</ValueWithLabel> <ValueWithLabel label="TimeLimit">{job.TimeLimit}</ValueWithLabel> <ValueWithLabel label="SubmitTime">{job.SubmitTime}</ValueWithLabel> <ValueWithLabel label="WorkDir"> <FileLink path={job.WorkDir} /> </ValueWithLabel> <ValueWithLabel label="BatchHost">{job.BatchHost}</ValueWithLabel> </SpaceBetween> <SpaceBetween direction="vertical" size="l"> <ValueWithLabel label="EndTime">{job.EndTime}</ValueWithLabel> <ValueWithLabel label="NumNodes">{job.NumNodes}</ValueWithLabel> <ValueWithLabel label="NumCPUs">{job.NumCPUs}</ValueWithLabel> <ValueWithLabel label="NumTasks">{job.NumTasks}</ValueWithLabel> <ValueWithLabel label="CPUs/Task">{job['CPUs/Task']}</ValueWithLabel> <ValueWithLabel label="TRES">{job.TRES}</ValueWithLabel> <ValueWithLabel label="Command">{job.Command}</ValueWithLabel> <ValueWithLabel label="StdOut"> <FileLink path={job.StdOut} isFile={true} /> </ValueWithLabel> <ValueWithLabel label="StdErr"> <FileLink path={job.StdErr} isFile={true} /> </ValueWithLabel> </SpaceBetween> </ColumnLayout> </Container> ) } function JobModal() { const clusterName = useState(['app', 'clusters', 'selected']) const clusterPath = ['clusters', 'index', clusterName] const fleetStatus = useState([...clusterPath, 'computeFleetStatus']) const open = useState(['app', 'clusters', 'jobInfo', 'dialog']) const job: Job = useState(['app', 'clusters', 'jobInfo', 'data']) const close = () => { setState(['app', 'clusters', 'jobInfo', 'dialog'], false) } return ( <Modal onDismiss={close} visible={open} closeAriaLabel="Close modal" size="large" footer={ <Box float="right"> <SpaceBetween direction="horizontal" size="xs"> {job && ( <JobActions job={{ job_id: job.JobId, job_state: job.JobState, name: job.JobName, partition: job.Partition, nodes: job.NodeList, time: job.RunTime, }} disabled={fleetStatus !== 'RUNNING'} cancelCallback={close} /> )} <Button onClick={close}>Close</Button> </SpaceBetween> </Box> } header={`Job Info: ${job ? job.JobName : ''}`} > {job ? ( <JobProperties job={job} /> ) : ( <div style={{textAlign: 'center', paddingTop: '40px'}}> <Loading /> </div> )} </Modal> ) } export default function ClusterScheduling() { const {t} = useTranslation() const clusterName = useState(['app', 'clusters', 'selected']) const clusterPath = ['clusters', 'index', clusterName] const cluster = useState(clusterPath) const fleetStatus = useState([...clusterPath, 'computeFleetStatus']) const clusterMinor = cluster.version ? parseInt(cluster.version.split('.')[1]) : 0 const jobs: JobSummary[] = useState([ 'clusters', 'index', clusterName, 'jobs', ]) const defaultRegion = useState(['aws', 'region']) const region = useState(['app', 'selectedRegion']) || defaultRegion function isSsmPolicy(p: any) { return p.hasOwnProperty('Policy') && p.Policy === ssmPolicy(region) } const iamPolicies = useState([ ...clusterPath, 'config', 'HeadNode', 'Iam', 'AdditionalIamPolicies', ]) const ssmEnabled = iamPolicies && findFirst(iamPolicies, isSsmPolicy) React.useEffect(() => { const tick = () => { clusterMinor > 0 && ssmEnabled && refreshQueues() } clusterMinor > 0 && ssmEnabled && refreshQueues() const timerId = setInterval(tick, 10000) return () => { clearInterval(timerId) } }, [clusterMinor, ssmEnabled]) const selectJobCallback = (jobInfo: any) => { setState(['app', 'clusters', 'jobInfo', 'data'], jobInfo) } const selectJob = (jobId: string) => { const clusterName = getState(['app', 'clusters', 'selected']) if (clusterName) { const clusterPath = ['clusters', 'index', clusterName] const cluster = getState(clusterPath) let user = clusterDefaultUser(cluster) const headNode = getState([...clusterPath, 'headNode']) clearState(['app', 'clusters', 'jobInfo', 'data']) headNode && setState(['app', 'clusters', 'jobInfo', 'dialog'], true) headNode && JobInfo(headNode.instanceId, user, jobId, selectJobCallback) } } const { items, actions, filteredItemsCount, collectionProps, filterProps, paginationProps, } = useCollection( jobs || [], extendCollectionsOptions({ filtering: { empty: ( <EmptyState title={t('cluster.scheduling.filter.empty.title')} subtitle={t('cluster.scheduling.filter.empty.subtitle')} /> ), noMatch: ( <EmptyState title="No matches" subtitle="No jobs match the filters." action={ <Button onClick={() => actions.setFiltering('')}> Clear filter </Button> } /> ), }, sorting: { defaultState: { sortingColumn: { sortingField: 'job_id', }, }, }, selection: {}, }), ) return ( <SpaceBetween direction="vertical" size="s"> <JobModal /> {clusterMinor > 0 && ssmEnabled && (jobs ? ( <Table {...collectionProps} trackBy="job_id" header={ <Header variant="h3" counter={jobs && `(${jobs.length})`}> {t('cluster.scheduling.tableTitle')} </Header> } columnDefinitions={[ { id: 'id', header: t('cluster.scheduling.id'), cell: job => ( <Link onFollow={() => selectJob(job.job_id)}> {job.job_id} </Link> ), sortingField: 'job_id', }, { id: 'name', header: t('cluster.scheduling.name'), cell: job => job.name, sortingField: 'name', }, { id: 'partition', header: t('cluster.scheduling.partition'), cell: job => job.partition, sortingField: 'partition', }, { id: 'nodes', header: t('cluster.scheduling.nodes'), cell: job => job.nodes, sortingField: 'nodes', }, { id: 'state', header: t('cluster.scheduling.state'), cell: job => <JobStatusIndicator status={job.job_state} />, sortingField: 'job_state', }, { id: 'time', header: t('cluster.scheduling.runTime'), cell: job => job.time, sortingField: 'time', }, { id: 'actions', header: t('cluster.scheduling.actions'), cell: job => ( <JobActions disabled={fleetStatus !== 'RUNNING'} job={job} /> ), }, ]} items={items} loadingText={t('cluster.scheduling.loadingJobs')} pagination={<Pagination {...paginationProps} />} filter={ <TextFilter {...filterProps} countText={`Results: ${filteredItemsCount}`} filteringAriaLabel={t( 'cluster.scheduling.filter.filteringAriaLabel', )} filteringPlaceholder={t( 'cluster.scheduling.filter.filteringPlaceholder', )} /> } /> ) : ( <div style={{textAlign: 'center', paddingTop: '40px'}}> <Loading /> </div> ))} {clusterMinor === 0 && ( <div>{t('cluster.scheduling.schedulingEnabled')}</div> )} {!ssmEnabled && <div>{t('cluster.scheduling.ssmEnabled')}</div>} </SpaceBetween> ) }