Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Th2-5212] Add option to change default view type of result group,#display-table field, option to view last N results of Notebook, file path and timestamp types #572

Merged
merged 19 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/api/ApiSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ export interface BooksApiSchema {
export interface JSONViewerApiSchema {
getLinks: (type: string, dir?: string) => Promise<{ directories: string[]; files: string[] }>;
getParameters: (path: string) => Promise<NotebookParameters>;
getResults: (taskId: string) => Promise<{ status: string; result: string }>;
launchNotebook: (path: string, parameters?: Object) => Promise<{ path: string; task_id: string }>;
getResults: (taskId: string) => Promise<{ status: string; result: string; path?: string }>;
getFile: (path: string) => Promise<{ result: string }>;
launchNotebook: (path: string, parameters?: Object) => Promise<{ task_id: string }>;
stopNotebook: (taskId: string) => Promise<boolean>;
}

Expand Down
8 changes: 8 additions & 0 deletions src/api/JSONViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ const JSONViewerHttpApi: JSONViewerApiSchema = {
notificationsStore.handleRequestError(res);
return { status: 'error', result: taskId };
},
getFile: async (path: string): Promise<{ result: string }> => {
const res = await fetch(`json-stream-provider/file?path=${path}`);
if (res.ok) {
return res.json();
}
notificationsStore.handleRequestError(res);
return { result: '' };
},
launchNotebook: async (path: string, parameters = {}) => {
const res = await fetch(`json-stream-provider/execute?path=${path}`, {
method: 'POST',
Expand Down
56 changes: 56 additions & 0 deletions src/components/JSONViewer/DisplayTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';

const shownCapacity = 50;

const DisplayTable = ({ value }: { value: string[][] | undefined }) => {
const [shownSize, setShownSize] = React.useState(shownCapacity);
if (!value) return <div className='display-table-error'>#display-table is undefined</div>;

const header = value[0];
const rows = value.slice(1);

return (
<div className='display-table'>
<table style={{ gridTemplateColumns: `repeat(${header.length}, 1fr) 16px` }}>
<thead>
<tr>
{header.map((key, index) => (
<th key={index}>{key}</th>
))}
<th style={{ width: '16px' }}></th>
</tr>
</thead>
<tbody>
{rows.slice(0, shownSize).map((row, index) => (
<tr key={index}>
{row.slice(0, header.length).map((val, ind) => (
<td key={ind}>{typeof val === 'string' ? `"${val}"` : String(val)}</td>
))}
{row.length < header.length &&
Array(header.length - row.length)
.fill('')
.map((_val, ind) => <td key={ind}></td>)}
<td style={{ width: '16px' }}>
{header.length < row.length && (
<div
className='display-table-info'
title={`Not included extra cells: ${JSON.stringify(row.slice(header.length))}`}
/>
)}
</td>
</tr>
))}
</tbody>
</table>
{shownSize < rows.length && (
<button
onClick={() => setShownSize(shownSize + shownCapacity)}
className='actions-list__load-button'>
Show More
</button>
)}
</div>
);
};

export default DisplayTable;
40 changes: 25 additions & 15 deletions src/components/JSONViewer/FileChoosing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { parseText } from '../../helpers/JSONViewer';

const FileChoosing = ({
type,
multiple,
onSubmit,
close,
}: {
type: 'notebooks' | 'results';
type: 'notebooks' | 'results' | 'all';
multiple: boolean;
onSubmit: (t: TreeNode[], n: string[]) => void;
close: () => void;
}) => {
Expand Down Expand Up @@ -75,11 +77,11 @@ const FileChoosing = ({
const promises: Promise<void>[] = [];
if (selectedFiles.length > 0) {
setIsLoading(true);
if (type === 'notebooks') onSubmit([], selectedFiles);
if (type === 'notebooks' || type === 'all') onSubmit([], selectedFiles);
else {
selectedFiles.forEach(filePath =>
promises.push(
api.jsonViewer.getResults(filePath).then(({ result }) => {
api.jsonViewer.getFile(filePath).then(({ result }) => {
if (filePath.endsWith('.ipynb')) {
notebookData.push(filePath);
return;
Expand Down Expand Up @@ -118,6 +120,10 @@ const FileChoosing = ({

const selectFile = (fileName: string) => {
const fileIndex = selectedFiles.indexOf(fileName);
if (!multiple) {
onSubmit([], [fileName]);
return;
}

if (fileIndex > -1) {
setSelectedFiles([
Expand Down Expand Up @@ -154,18 +160,22 @@ const FileChoosing = ({
value={search}
onChange={e => setSearch(e.target.value)}
/>
<button
disabled={selectedFiles.length === 0 || isLoading}
className='load-JSON-button'
onClick={() => setSelectedFiles([])}>
Reset Selection
</button>
<button
disabled={selectedFiles.length === 0 || isLoading}
className='load-JSON-button'
onClick={getFiles}>
Load {selectedFiles.length} Files
</button>
{multiple && (
<>
<button
disabled={selectedFiles.length === 0 || isLoading}
className='load-JSON-button'
onClick={() => setSelectedFiles([])}>
Reset Selection
</button>
<button
disabled={selectedFiles.length === 0 || isLoading}
className='load-JSON-button'
onClick={getFiles}>
Load {selectedFiles.length} Files
</button>
</>
)}
</div>
{isLoading ? (
<div style={{ marginLeft: 5 }} className='fileChoosing__loading' />
Expand Down
147 changes: 94 additions & 53 deletions src/components/JSONViewer/NotebookParamsCell.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import * as React from 'react';
import { observer } from 'mobx-react-lite';
import { nanoid } from 'nanoid';
import { NotebookParameter, NotebookParameters, TreeNode } from '../../models/JSONSchema';
import {
InputNotebookParameter,
NotebookParameter,
NotebookParameters,
TreeNode,
} from '../../models/JSONSchema';
import api from '../../api';
import '../../styles/jupyter.scss';
import { useJSONViewerStore } from '../../hooks/useJSONViewerStore';
import { parseText } from '../../helpers/JSONViewer';
import {
convertParameterToInput,
convertParameterValue,
getParameterType,
parseText,
validateParameter,
} from '../../helpers/JSONViewer';
import { useNotificationsStore } from '../../hooks';
import ParametersRow from './ParametersRow';

const timeBetweenResults = 1000;
const numberReg = /^-?\d*\.?\d{1,}$/;

const NotebookParamsCell = ({ notebook }: { notebook: string }) => {
const JSONViewerStore = useJSONViewerStore();
const notificationsStore = useNotificationsStore();
const [parameters, setParameters] = React.useState<NotebookParameter[]>([]);
const [paramsValue, setParamsValue] = React.useState<Record<string, string>>({});
const [paramsValue, setParamsValue] = React.useState<InputNotebookParameter[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
const [isRunLoading, setIsRunLoading] = React.useState(false);
const [isExpanded, setIsExpanded] = React.useState(false);
const [timer, setTimer] = React.useState<NodeJS.Timeout | null>();
const [taskId, setTaskId] = React.useState<string | null>();
const keys: string[] = React.useMemo(() => parameters.map(param => param.name), [parameters]);
const [resultCount, setResultCount] = React.useState<string>('1');
const [results, setResults] = React.useState<string[]>([]);
const isValid = React.useMemo(() => paramsValue.every(v => v.isValid), [paramsValue]);

const initParameters = () => {
setParamsValue(parameters.map(convertParameterToInput));
};

React.useEffect(initParameters, [parameters]);

const getParameters = async () => {
setIsLoading(true);
Expand All @@ -41,8 +60,8 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => {
setIsExpanded(!isExpanded);
};

const getResults = async (respTaskId: string, path: string) => {
const { status, result } = await api.jsonViewer.getResults(respTaskId);
const getResults = async (respTaskId: string) => {
const { status, result, path } = await api.jsonViewer.getResults(respTaskId);

switch (status) {
case 'success':
Expand All @@ -68,11 +87,21 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => {
}
}
node.failed = node.complexFields.some(v => v.failed);
const newResults = [node.id, ...results];
const maxResultCount = Number(resultCount);
const convertResultCount = maxResultCount < 1 ? 1 : Math.round(maxResultCount);
if (maxResultCount < 1) {
setResultCount('1');
}

if (node.complexFields.length > 0) {
JSONViewerStore.addNodes([node]);
if (newResults.length > convertResultCount) {
JSONViewerStore.removeNodesById(newResults.slice(convertResultCount));
}
setResults(newResults.slice(0, convertResultCount));
JSONViewerStore.selectTreeNode(node);
}
setParamsValue({});
setIsRunLoading(false);
setIsExpanded(false);
}
Expand All @@ -88,13 +117,22 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => {
}
break;
case 'in progress':
setTimer(setTimeout(() => getResults(respTaskId, path), timeBetweenResults));
setTimer(setTimeout(() => getResults(respTaskId), timeBetweenResults));
break;
default:
break;
}
};

const filterParameters = (inputParameter: InputNotebookParameter, index: number) => {
const parameter = parameters[index];
const parameterType = getParameterType(parameter);
const newValue = convertParameterValue(inputParameter.value, inputParameter.type);
const oldValue = convertParameterValue(parameter.default, parameterType, true);
if (typeof newValue !== typeof oldValue) return true;
return newValue !== oldValue;
};

const runNotebook = async () => {
if (isRunLoading) {
if (timer) {
Expand All @@ -112,32 +150,14 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => {
}
setIsRunLoading(true);
const paramsWithType = Object.fromEntries(
Object.entries(paramsValue)
.filter(val => val[1] !== '')
.map(([name, value]) => {
const ind = keys.indexOf(name);
switch (parameters[ind].inferred_type_name) {
case 'string':
return [name, value];
case 'float':
return [name, parseFloat(value)];
case 'int':
return [name, parseInt(value)];
default:
if (numberReg.test(value)) {
if (Number.isInteger(value)) {
return [name, Number.parseInt(value)];
}
return [name, Number.parseFloat(value)];
}
return [name, value];
}
}),
paramsValue
.filter(filterParameters)
.map(({ name, type, value }) => [name, convertParameterValue(value, type)]),
);
const res = await api.jsonViewer.launchNotebook(notebook, paramsWithType);
if (res.task_id !== '') {
setTaskId(res.task_id);
setTimer(setTimeout(() => getResults(res.task_id, res.path), timeBetweenResults));
setTimer(setTimeout(() => getResults(res.task_id), timeBetweenResults));
} else {
setIsRunLoading(false);
}
Expand Down Expand Up @@ -176,33 +196,38 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => {
)}
</thead>
<tbody>
{parameters.map(parameter => (
<tr key={parameter.name}>
<td>
<label>{parameter.name}</label>
</td>
<td>
<label>{parameter.inferred_type_name}</label>
</td>
<td>
<input
type='text'
placeholder={`default: ${parameter.default}`}
value={paramsValue[parameter.name]}
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
const newState = paramsValue;
newState[parameter.name] = ev.target.value;
setParamsValue(newState);
}}
/>
</td>
</tr>
{parameters.map((parameter, index) => (
<ParametersRow
parameter={parameter}
parameterValue={paramsValue[index]}
setParametersValue={(newValue: string) => {
const newState = paramsValue[index];
newState.value = newValue;
newState.isValid = validateParameter(newState.value, newState.type);
setParamsValue([
...paramsValue.slice(0, index),
newState,
...paramsValue.slice(index + 1),
]);
}}
setParametersType={(newValue: string) => {
const newState = paramsValue[index];
newState.type = newValue;
newState.isValid = validateParameter(newState.value, newState.type);
setParamsValue([
...paramsValue.slice(0, index),
newState,
...paramsValue.slice(index + 1),
]);
}}
key={parameter.name}
/>
))}
</tbody>
</table>
</div>
<div className='buttons'>
<button onClick={runNotebook}>
<button onClick={runNotebook} disabled={!isValid}>
<label>Run</label>
<div className={`notebookCell-icon ${isRunLoading ? 'loading' : 'play'}`} />
</button>
Expand All @@ -212,6 +237,22 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => {
</div>
</div>
)}
{isExpanded && (
<div className='notebookCell-settings'>
<div style={{ display: 'flex', gap: '5px' }}>
<div>Results Amount:</div>
<input
style={{ maxWidth: 400 }}
type='number'
value={resultCount}
pattern='\d+'
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => {
setResultCount(ev.target.value);
}}
/>
</div>
</div>
)}
</div>
);
};
Expand Down
Loading
Loading