source/idea/idea-cluster-manager/webapp/src/pages/hpc/jobs.tsx (146 lines of code) (raw):

/* * 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://www.apache.org/licenses/LICENSE-2.0 * * or in the 'license' 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 React, { Component, RefObject } from "react"; import IdeaListView from "../../components/list-view"; import { TableProps } from "@cloudscape-design/components/table/interfaces"; import { DeleteJobRequest, DeleteJobResult, SocaJob } from "../../client/data-model"; import { AppContext } from "../../common"; import { SchedulerAdminClient, SchedulerClient } from "../../client"; import IdeaSplitPanel from "../../components/split-panel"; import { Box, ColumnLayout, Popover, StatusIndicator, Table, Tabs } from "@cloudscape-design/components"; import { KeyValue, KeyValueGroup } from "../../components/key-value"; import Utils from "../../common/utils"; import { JobUtils } from "./hpc-utils"; import { IdeaSideNavigationProps } from "../../components/side-navigation"; import IdeaAppLayout, { IdeaAppLayoutProps } from "../../components/app-layout"; import { withRouter } from "../../navigation/navigation-utils"; export const JOB_TABLE_COLUMN_DEFINITIONS: TableProps.ColumnDefinition<SocaJob>[] = [ { id: "id", header: "Job Id", cell: (job) => job.job_id, }, { id: "name", header: "Name", cell: (job) => job.name, }, { id: "owner", header: "Owner", cell: (job) => job.owner, }, { id: "queue", header: "Queue", cell: (job) => job.queue, }, { id: "project", header: "Project", cell: (job) => job.project, }, { id: "status", header: "Status", cell: (job) => { if (job.params?.compute_stack === "tbd") { if (Utils.isEmpty(job.error_message)) { return <StatusIndicator type="pending">Queued</StatusIndicator>; } else { return ( <Box color="text-status-error"> <Popover dismissAriaLabel="Close" header="Job cannot be provisioned currently ..." content={job.error_message}> <StatusIndicator type="info">Queued</StatusIndicator> </Popover> </Box> ); } } else if (job.params?.compute_stack !== "tbd") { if (job.state === "queued") { return ( <StatusIndicator type="in-progress" colorOverride="blue"> Provisioning </StatusIndicator> ); } else if (job.state === "running") { return <StatusIndicator type="success">Running</StatusIndicator>; } else { return ( <StatusIndicator type="success" colorOverride="grey"> Finished </StatusIndicator> ); } } }, }, { id: "queued-on", header: "Queue Time", cell: (job) => new Date(job.queue_time!).toLocaleString(), }, ]; export interface JobsProps extends IdeaAppLayoutProps, IdeaSideNavigationProps { type: string; // active, completed scope: string; // user, admin } export interface JobsState { splitPanelOpen: boolean; jobSelected: boolean; } class Jobs extends Component<JobsProps, JobsState> { listing: RefObject<IdeaListView>; constructor(props: JobsProps) { super(props); this.listing = React.createRef(); this.state = { splitPanelOpen: false, jobSelected: false, }; } schedulerAdmin(): SchedulerAdminClient { return AppContext.get().client().schedulerAdmin(); } scheduler(): SchedulerClient { return AppContext.get().client().scheduler(); } getListing(): IdeaListView { return this.listing.current!; } isSelected(): boolean { return this.state.jobSelected; } getSelected(): SocaJob | null { if (this.getListing() == null) { return null; } return this.getListing().getSelectedItem(); } isActiveJobs(): boolean { return this.props.type === "active"; } isCompletedJobs(): boolean { return this.props.type === "completed"; } buildListing() { let columnDefinitions = [...JOB_TABLE_COLUMN_DEFINITIONS]; if (this.isCompletedJobs()) { columnDefinitions.push({ id: "exit_code", header: "Exit Status", cell: (job) => job.exit_status, }); } return ( <IdeaListView ref={this.listing} preferencesKey={"hpc-jobs"} showPreferences={true} title={this.isActiveJobs() ? "Active Jobs" : "Completed Jobs"} description={this.isActiveJobs() ? "All active Jobs" : "All completed Jobs"} selectionType="single" // todo - commented until file picker UI is implemented in submit job form // primaryAction={{ // id: 'submit-job', // text: 'Submit Job', // onClick: () => { // this.props.history.push('/soca/jobs/submit-job') // } // }} secondaryActionsDisabled={this.isCompletedJobs()} secondaryActions={[ { id: "delete-job", text: "Delete Job", disabled: !this.isSelected(), onClick: () => { const deleteJob = (request: DeleteJobRequest): Promise<DeleteJobResult> => { if (this.props.scope === "admin") { return this.schedulerAdmin().deleteJob(request); } else { return this.scheduler().deleteJob(request); } }; deleteJob({ job_id: this.getSelected()?.job_id, }) .then(() => { this.props.onFlashbarChange({ items: [ { type: "info", content: `Job Id: ${this.getSelected()?.job_id} will be deleted shortly.`, dismissible: true, }, ], }); this.getListing().fetchRecords(); }) .catch((error) => { this.props.onFlashbarChange({ items: [ { type: "error", content: error.message, dismissible: true, }, ], }); }); }, }, ]} showPaginator={true} showFilters={true} showDateRange={this.props.type === "completed"} dateRange={{ type: "relative", amount: 1, unit: "month", }} dateRangeFilterKeyOptions={[{ value: "queue_time", label: "Queued" }]} filters={[ { key: "any", }, ]} onFilter={(filters) => { const filterString = Utils.asString(filters[0].value).trim(); if (Utils.isEmpty(filterString)) { return []; } else if (Utils.isPositiveInteger(filterString)) { return [ { key: "job_id", value: filterString, }, ]; } else if (filterString.includes(",")) { const jobIds = filterString.split(","); return [ { key: "job_id", value: jobIds.map((jobId) => jobId.trim().toLowerCase()), }, ]; } else { return [ { key: "$all", value: filterString, }, ]; } }} onRefresh={() => { this.setState( { jobSelected: false, }, () => { this.getListing().fetchRecords(); } ); }} onSelectionChange={() => { this.setState( { splitPanelOpen: true, jobSelected: true, }, () => {} ); }} onFetchRecords={() => { if (this.props.scope === "user") { if (this.props.type === "active") { return this.scheduler().listActiveJobs({ filters: this.getListing().getFilters(), paginator: this.getListing().getPaginator(), }); } else { return this.scheduler().listCompletedJobs({ filters: this.getListing().getFilters(), paginator: this.getListing().getPaginator(), date_range: this.getListing().getFormatedDateRange(), }); } } else { if (this.props.type === "active") { return this.schedulerAdmin().listActiveJobs({ filters: this.getListing().getFilters(), paginator: this.getListing().getPaginator(), }); } else { return this.schedulerAdmin().listCompletedJobs({ filters: this.getListing().getFilters(), paginator: this.getListing().getPaginator(), date_range: this.getListing().getFormatedDateRange(), }); } } }} columnDefinitions={columnDefinitions} /> ); } buildSplitPanelContent() { const selected = () => this.getSelected()!; const jobUtil = () => new JobUtils(selected()); const jobParams = () => selected().params!; return ( this.isSelected() && ( <IdeaSplitPanel title={`JobId: ${this.getSelected()?.job_id}`}> <Tabs tabs={[ { label: "Job Info", id: "job-info", content: ( <ColumnLayout columns={3} variant="text-grid"> <KeyValue title="State" value={selected().state} /> <KeyValue title="Job Id" value={selected().job_id} /> <KeyValue title="Job Group" value={selected().job_group} clipboard={true} /> <KeyValue title="Queue" value={selected().queue} /> <KeyValue title="Queue Type" value={selected().queue_type} /> <KeyValue title="Scaling Mode" value={selected().scaling_mode} /> <KeyValue title="Name" value={selected().name} /> <KeyValue title="Project" value={selected().project} /> <KeyValue title="Owner" value={selected().owner} /> <KeyValue title="Queue Time" value={selected().queue_time} type="date" /> <KeyValue title="Start Time" value={selected().start_time} type="date" /> <KeyValue title="End Time" value={selected().end_time} type="date" /> <KeyValue title="Comment" value={selected().comment} clipboard={true} /> </ColumnLayout> ), }, { label: "Compute Stack", id: "compute-stack", content: ( <ColumnLayout columns={2} variant="text-grid"> <KeyValueGroup title="Instance Info"> <KeyValue title="Base OS" value={jobParams().base_os} /> <KeyValue title="Instance AMI" value={jobParams().instance_ami} clipboard={true} /> <KeyValue title="Instance Types" value={jobParams().instance_types} /> <KeyValue title="Keep EBS Volumes" value={jobParams().keep_ebs_volumes} /> <KeyValue title="Root Storage Size" value={jobParams().root_storage_size} type="memory" /> <KeyValue title="Enable Elastic Fabric Adapter (EFA)" value={jobParams().enable_efa_support} /> <KeyValue title="Force Reserved Instances" value={jobParams().force_reserved_instances} /> <KeyValue title="Enable Hyper-Threading" value={jobParams().enable_ht_support} /> </KeyValueGroup> <KeyValueGroup title="Network and Security"> <KeyValue title="Subnet Ids" value={jobParams().subnet_ids} clipboard={true} /> <KeyValue title="Security Groups" value={jobParams().security_groups} clipboard={true} /> <KeyValue title="Instance Profile" value={jobParams().instance_profile} /> <KeyValue title="Enable Placement Group" value={jobParams().enable_placement_group} /> </KeyValueGroup> <KeyValueGroup title="Compute Requirements"> <KeyValue title="Nodes" value={jobParams().nodes} /> <KeyValue title="CPUs" value={jobParams().cpus} /> </KeyValueGroup> <KeyValueGroup title="Spot Fleet"> <KeyValue title="Is Spot?" value={jobParams().spot} /> {jobUtil().isEnableSpot() && <KeyValue title="Spot Price" value={jobParams().spot_price} type="amount" />} {jobUtil().isEnableSpot() && <KeyValue title="Spot Allocation Count" value={jobParams().spot_allocation_count} />} {jobUtil().isEnableSpot() && <KeyValue title="Spot Allocation Strategy" value={jobParams().spot_allocation_strategy} />} </KeyValueGroup> {!jobUtil().isScratchStorageEnabled() && ( <KeyValueGroup title="Scratch Storage"> <KeyValue title="Is Enabled?" value={false} /> </KeyValueGroup> )} {jobUtil().isScratchEBS() && ( <KeyValueGroup title="Scratch Storage: EBS"> <KeyValue title="EBS: Storage Size" value={jobParams().scratch_storage_size} type="memory" /> <KeyValue title="EBS Storage IOPS" value={jobParams().scratch_storage_iops} /> </KeyValueGroup> )} {jobUtil().isScratchExistingFsxLustre() && ( <KeyValueGroup title="Scratch Storage: Existing FSx for Lustre"> <KeyValue title="Existing FSx Lustre" value={jobParams().fsx_lustre?.existing_fsx} /> </KeyValueGroup> )} {jobUtil().isScratchNewFsxLustre() && ( <KeyValueGroup title="Scratch Storage: New FSx for Lustre"> <KeyValue title="S3 Backend" value={jobParams().fsx_lustre?.s3_backend} /> <KeyValue title="Import Path" value={jobParams().fsx_lustre?.import_path} /> <KeyValue title="Export Path" value={jobParams().fsx_lustre?.export_path} /> <KeyValue title="Deployment Type" value={jobParams().fsx_lustre?.deployment_type} /> <KeyValue title="Per Unit Throughput" value={jobParams().fsx_lustre?.per_unit_throughput} /> <KeyValue title="Size" value={jobParams().fsx_lustre?.size} type="memory" /> </KeyValueGroup> )} <KeyValueGroup title="Metrics"> <KeyValue title="Enable System Metrics" value={jobParams().enable_system_metrics} /> <KeyValue title="Enable Anonymous Metrics" value={jobParams().enable_anonymous_metrics} /> </KeyValueGroup> </ColumnLayout> ), }, { label: "Execution Hosts", id: "execution-hosts", content: ( <Table items={selected().execution_hosts ? selected().execution_hosts! : []} columnDefinitions={[ { id: "host", header: "Host", cell: (host) => host.host, }, { id: "instance-id", header: "Instance Id", cell: (host) => host.instance_id, }, { id: "instance-type", header: "Instance Type", cell: (host) => host.instance_type, }, { id: "capacity-type", header: "Capacity Type", cell: (host) => host.capacity_type, }, { id: "tenancy", header: "Tenancy", cell: (host) => host.tenancy, }, ]} /> ), }, { label: "Estimated Costs", id: "estimated-costs", content: ( <ColumnLayout columns={1}> <Table items={selected().estimated_bom_cost ? selected().estimated_bom_cost!.line_items! : []} columnDefinitions={[ { id: "title", header: "Item", cell: (item) => item.title, }, { id: "qty", header: "Qty", cell: (item) => item.quantity, }, { id: "unit", header: "Unit", cell: (item) => item.unit, }, { id: "unit-price", header: "Unit Price", cell: (item) => Utils.getFormattedAmount(item.unit_price), }, { id: "total-price", header: "Total Price", cell: (item) => Utils.getFormattedAmount(item.total_price), }, ]} /> <ColumnLayout columns={2}> <Box textAlign="left"> <h3>Estimated Total Cost</h3> </Box> <Box textAlign="right"> <h3>{Utils.getFormattedAmount(selected().estimated_bom_cost?.total)}</h3> </Box> </ColumnLayout> </ColumnLayout> ), }, ]} /> </IdeaSplitPanel> ) ); } render() { const breadcrumbs = () => { if (this.props.scope === "user") { if (this.props.type === "active") { return [ { text: "IDEA", href: "#/", }, { text: "Home", href: "#/", }, { text: "Active Jobs", href: "#/home/active-jobs", }, ]; } else { return [ { text: "IDEA", href: "#/", }, { text: "Home", href: "#/", }, { text: "Completed Jobs", href: "#/home/completed-jobs", }, ]; } } else { if (this.props.type === "active") { return [ { text: "IDEA", href: "#/", }, { text: "Scale-Out Computing", href: "#/soca/active-jobs", }, { text: "Active Jobs", href: "", }, ]; } else { return [ { text: "IDEA", href: "#/", }, { text: "Scale-Out Computing", href: "#/soca/active-jobs", }, { text: "Completed Jobs", href: "", }, ]; } } }; return ( <IdeaAppLayout ideaPageId={this.props.ideaPageId} toolsOpen={this.props.toolsOpen} tools={this.props.tools} onToolsChange={this.props.onToolsChange} onPageChange={this.props.onPageChange} sideNavHeader={this.props.sideNavHeader} sideNavItems={this.props.sideNavItems} onSideNavChange={this.props.onSideNavChange} onFlashbarChange={this.props.onFlashbarChange} flashbarItems={this.props.flashbarItems} breadcrumbItems={breadcrumbs()} content={<div>{this.buildListing()}</div>} splitPanelOpen={this.state.splitPanelOpen} splitPanel={this.buildSplitPanelContent()} onSplitPanelToggle={(event: any) => { this.setState({ jobSelected: false, splitPanelOpen: event.detail.open, }); }} /> ); } } function _ActiveJobs(props: JobsProps) { return <Jobs {...props} type="active" scope="user" />; } function _CompletedJobs(props: JobsProps) { return <Jobs {...props} type="completed" scope="user" />; } function _AdminActiveJobs(props: JobsProps) { return <Jobs {...props} type="active" scope="admin" />; } function _AdminCompletedJobs(props: JobsProps) { return <Jobs {...props} type="completed" scope="admin" />; } export const ActiveJobs = withRouter(_ActiveJobs); export const CompletedJobs = withRouter(_CompletedJobs); export const AdminActiveJobs = withRouter(_AdminActiveJobs); export const AdminCompletedJobs = withRouter(_AdminCompletedJobs);