Skip to content

Commit

Permalink
add tabs in list view, include basic job definition table
Browse files Browse the repository at this point in the history
  • Loading branch information
dlqqq committed Oct 5, 2022
1 parent 0441c3e commit a4a67c4
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 65 deletions.
2 changes: 1 addition & 1 deletion src/components/advanced-table/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { AdvancedTable } from './advanced-table';
export * from './advanced-table';
27 changes: 27 additions & 0 deletions src/components/job-definition-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { JupyterFrontEnd } from '@jupyterlab/application';
import TableRow from '@mui/material/TableRow';
import TableCell from '@mui/material/TableCell';

import { Scheduler } from '../handler';

export function buildJobDefinitionRow(
jobDef: Scheduler.IDescribeJobDefinition,
app: JupyterFrontEnd,
openJobDefinitionDetail: (jobDefId: string) => unknown
) {
const cellContents: React.ReactNode[] = [
// name
<a onClick={() => openJobDefinitionDetail(jobDef.job_definition_id)}>
{jobDef.name}
</a>
];

return (
<TableRow>
{cellContents.map((cellContent, idx) => (
<TableCell key={idx}>{cellContent}</TableCell>
))}
</TableRow>
);
}
2 changes: 1 addition & 1 deletion src/components/job-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ function OutputFiles(props: {
);
}

export function buildTableRow(
export function buildJobRow(
job: Scheduler.IDescribeJob,
environmentList: Scheduler.IRuntimeEnvironment[],
app: JupyterFrontEnd,
Expand Down
100 changes: 58 additions & 42 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,60 @@ export class SchedulerService {
options.serverSettings || ServerConnection.makeSettings();
}

/**
* Serializes a query object into a URI query string. Assumes the keys and
* values of the query object as URI-encodable via `encoderURIComponent()`.
*/
private serializeToQueryString<
T extends { sort_by?: Scheduler.ISortField[] }
>(jobQuery: T): string {
return (
'?' +
(Object.keys(jobQuery) as (keyof T)[])
.map(prop => {
if (prop === 'sort_by') {
const sortList: T['sort_by'] | undefined = jobQuery.sort_by;
if (sortList === undefined) {
return null;
}

// Serialize sort_by as a series of parameters in the firm dir(name)
// where 'dir' is the direction and 'name' the sort field
return sortList
.map(
sort =>
`sort_by=${encodeURIComponent(
sort.direction
)}(${encodeURIComponent(sort.name)})`
)
.join('&');
}

const value = jobQuery[prop];
return `${encodeURIComponent(prop as any)}=${encodeURIComponent(
value as any
)}`;
})
.join('&')
);
}

async getJobDefinitions(
definition_id: string
): Promise<Scheduler.IDescribeJobDefinition[]> {
jobDefintionsQuery: Scheduler.IListJobDefinitionsQuery,
definition_id?: string
): Promise<Scheduler.IListJobDefinitionsResponse> {
let data;
let query = '';
if (definition_id) {
query = `/${definition_id}`;
}
const query = definition_id
? `/${definition_id}`
: this.serializeToQueryString(jobDefintionsQuery);
try {
data = await requestAPI(this.serverSettings, `job_definitions${query}`, {
method: 'GET'
});
} catch (e: any) {
console.error(e);
}
return data as Scheduler.IDescribeJobDefinition[];
return data as Scheduler.IListJobDefinitionsResponse;
}

async createJobDefinition(
Expand Down Expand Up @@ -63,41 +101,7 @@ export class SchedulerService {
job_id?: string
): Promise<Scheduler.IListJobsResponse> {
let data;
let query = '';

if (job_id) {
query = `/${job_id}`;
} else if (jobQuery) {
query =
'?' +
Object.keys(jobQuery)
.map(prop => {
if (prop === 'sort_by') {
const sortList: Scheduler.ISortField[] | undefined =
jobQuery.sort_by;
if (sortList === undefined) {
return null;
}

// Serialize sort_by as a series of parameters in the firm dir(name)
// where 'dir' is the direction and 'name' the sort field
return sortList
.map(
sort =>
`sort_by=${encodeURIComponent(
sort.direction
)}(${encodeURIComponent(sort.name)})`
)
.join('&');
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const value = jobQuery[prop];
return `${encodeURIComponent(prop)}=${encodeURIComponent(value)}`;
})
.join('&');
}
const query = job_id ? `/${job_id}` : this.serializeToQueryString(jobQuery);

try {
data = await requestAPI(this.serverSettings, `jobs${query}`, {
Expand Down Expand Up @@ -345,6 +349,18 @@ export namespace Scheduler {
total_count: number;
}

export interface IListJobDefinitionsQuery {
sort_by?: ISortField[];
max_items?: number;
next_token?: string;
}

export interface IListJobDefinitionsResponse {
job_definitions: IDescribeJobDefinition[];
next_token?: string;
total_count: number;
}

export interface IRuntimeEnvironment {
name: string;
label: string;
Expand Down
120 changes: 100 additions & 20 deletions src/mainviews/list-jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ import { JupyterFrontEnd } from '@jupyterlab/application';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';

import { Heading } from '../components/heading';
import { useTranslator } from '../hooks';
import { buildTableRow } from '../components/job-row';
import { buildJobRow } from '../components/job-row';
import { buildJobDefinitionRow } from '../components/job-definition-row';
import { ICreateJobModel, IListJobsModel } from '../model';
import { Scheduler, SchedulerService } from '../handler';
import { Cluster } from '../components/cluster';
import { AdvancedTable } from '../components/advanced-table';
import {
AdvancedTable,
AdvancedTableColumn
} from '../components/advanced-table';

interface INotebookJobsListBodyProps {
interface IListJobsTableProps {
startToken?: string;
app: JupyterFrontEnd;
// Function that results in the create job form being made visible
Expand All @@ -23,14 +29,7 @@ interface INotebookJobsListBodyProps {
showDetailView: (jobId: string) => void;
}

type GridColumn = {
sortField: string | null;
name: string;
};

export function NotebookJobsListBody(
props: INotebookJobsListBodyProps
): JSX.Element {
function ListJobsTable(props: IListJobsTableProps): JSX.Element {
const [jobsQuery, setJobsQuery] = useState<Scheduler.IListJobsQuery>({});
const [deletedRows, setDeletedRows] = useState<
Set<Scheduler.IDescribeJob['job_id']>
Expand Down Expand Up @@ -91,7 +90,7 @@ export function NotebookJobsListBody(
);

// Display column headers with sort indicators.
const columns: GridColumn[] = [
const columns: AdvancedTableColumn[] = [
{
sortField: 'name',
name: trans.__('Job name')
Expand Down Expand Up @@ -119,7 +118,7 @@ export function NotebookJobsListBody(
];

const renderRow = (job: Scheduler.IDescribeJob) =>
buildTableRow(
buildJobRow(
job,
environmentList,
props.app,
Expand Down Expand Up @@ -161,27 +160,108 @@ export function NotebookJobsListBody(
);
}

type ListJobDefinitionsTableProps = {
app: JupyterFrontEnd;
showJobDefinitionDetail: (jobDefId: string) => void;
};

function ListJobDefinitionsTable(props: ListJobDefinitionsTableProps) {
const trans = useTranslator('jupyterlab');
const [jobDefsQuery, setJobDefsQuery] =
useState<Scheduler.IListJobDefinitionsQuery>({});
const api = useMemo(() => new SchedulerService({}), []);

const columns: AdvancedTableColumn[] = [
{
sortField: 'name',
name: trans.__('Job definition name')
}
];

const reloadButton = (
<Cluster justifyContent="flex-end">
<Button
variant="contained"
size="small"
onClick={() => setJobDefsQuery({})}
>
{trans.__('Reload')}
</Button>
</Cluster>
);

const renderRow = (jobDef: Scheduler.IDescribeJobDefinition) =>
buildJobDefinitionRow(jobDef, props.app, props.showJobDefinitionDetail);

const emptyRowMessage = useMemo(
() => trans.__('There are no notebook job definitions.'),
[trans]
);

return (
<>
{reloadButton}
<AdvancedTable
query={jobDefsQuery}
setQuery={setJobDefsQuery}
request={api.getJobDefinitions.bind(api)}
extractRows={(payload: Scheduler.IListJobDefinitionsResponse) =>
payload?.job_definitions || []
}
renderRow={renderRow}
columns={columns}
emptyRowMessage={emptyRowMessage}
/>
</>
);
}

export interface IListJobsProps {
app: JupyterFrontEnd;
model: IListJobsModel;
handleModelChange: (model: IListJobsModel) => void;
showCreateJob: (newModel: ICreateJobModel) => void;
showDetailView: (jobId: string) => void;
showJobDetail: (jobId: string) => void;
showJobDefinitionDetail: (jobDefId: string) => void;
}

export function NotebookJobsList(props: IListJobsProps): JSX.Element {
const trans = useTranslator('jupyterlab');
const [tab, setTab] = useState<number>(0);

const jobsHeader = useMemo(() => trans.__('Notebook Jobs'), [trans]);
const jobDefinitionsHeader = useMemo(
() => trans.__('Notebook Job Definitions'),
[trans]
);

// Retrieve the initial jobs list
return (
<Box sx={{ p: 4 }} style={{ height: '100%', boxSizing: 'border-box' }}>
<Stack spacing={3} style={{ height: '100%' }}>
<Heading level={1}>{trans.__('Notebook Jobs')}</Heading>
<NotebookJobsListBody
app={props.app}
showCreateJob={props.showCreateJob}
showDetailView={props.showDetailView}
/>
<Tabs value={tab} onChange={(_, newTab) => setTab(newTab)}>
<Tab label={jobsHeader} />
<Tab label={jobDefinitionsHeader} />
</Tabs>
{tab === 0 && (
<>
<Heading level={1}>{jobsHeader}</Heading>
<ListJobsTable
app={props.app}
showCreateJob={props.showCreateJob}
showDetailView={props.showJobDetail}
/>
</>
)}
{tab === 1 && (
<>
<Heading level={1}>{jobDefinitionsHeader}</Heading>
<ListJobDefinitionsTable
app={props.app}
showJobDefinitionDetail={props.showJobDefinitionDetail}
/>
</>
)}
</Stack>
</Box>
);
Expand Down
5 changes: 4 additions & 1 deletion src/notebook-jobs-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ export class NotebookJobsPanel extends VDomRenderer<JobsModel> {
(this.model.listJobsModel = newModel)
}
showCreateJob={showCreateJob}
showDetailView={this.showDetailView.bind(this)}
showJobDetail={this.showDetailView.bind(this)}
showJobDefinitionDetail={jobDefinitionId => {
/* TODO */
}}
/>
)}
{this.model.jobsView === 'JobDetail' && (
Expand Down

0 comments on commit a4a67c4

Please sign in to comment.