Skip to content

Commit

Permalink
[Th2-5212] Add option to change default view type of result group,#di…
Browse files Browse the repository at this point in the history
…splay-table field, option to view last N results of Notebook, file path and timestamp types (#572)

* added endpoint for getting file by path

* created DisplayTeble component

* created ParametersRow component

* added option to change viewType of group

* added remove node to store

* change position of Results Result

* add display-table view type

* display cut cells from display-table

* update table panel's columns width

* update launchNotebook and getResults api

* fix toggle on Tree's leafs

* update display of DispayTable

* add margin bottom for JSON veiw types

* increase amount of rows for DisplayTable

* change display when none node is selected

* change split behaviour on selection

* added filepath and timestamp parameters

* fixed validation of timestamp parameter

* fix typing of string and boolean
  • Loading branch information
molotgor authored Jul 8, 2024
1 parent eeb3df8 commit 304ac0d
Show file tree
Hide file tree
Showing 18 changed files with 749 additions and 116 deletions.
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

0 comments on commit 304ac0d

Please sign in to comment.