diff --git a/src/api/ApiSchema.ts b/src/api/ApiSchema.ts index da076168..940c4dce 100644 --- a/src/api/ApiSchema.ts +++ b/src/api/ApiSchema.ts @@ -111,8 +111,9 @@ export interface BooksApiSchema { export interface JSONViewerApiSchema { getLinks: (type: string, dir?: string) => Promise<{ directories: string[]; files: string[] }>; getParameters: (path: string) => Promise; - 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; } diff --git a/src/api/JSONViewer.ts b/src/api/JSONViewer.ts index 475e1865..f1025780 100644 --- a/src/api/JSONViewer.ts +++ b/src/api/JSONViewer.ts @@ -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', diff --git a/src/components/JSONViewer/DisplayTable.tsx b/src/components/JSONViewer/DisplayTable.tsx new file mode 100644 index 00000000..72dd4997 --- /dev/null +++ b/src/components/JSONViewer/DisplayTable.tsx @@ -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
#display-table is undefined
; + + const header = value[0]; + const rows = value.slice(1); + + return ( +
+ + + + {header.map((key, index) => ( + + ))} + + + + + {rows.slice(0, shownSize).map((row, index) => ( + + {row.slice(0, header.length).map((val, ind) => ( + + ))} + {row.length < header.length && + Array(header.length - row.length) + .fill('') + .map((_val, ind) => )} + + + ))} + +
{key}
{typeof val === 'string' ? `"${val}"` : String(val)} + {header.length < row.length && ( +
+ )} +
+ {shownSize < rows.length && ( + + )} +
+ ); +}; + +export default DisplayTable; diff --git a/src/components/JSONViewer/FileChoosing.tsx b/src/components/JSONViewer/FileChoosing.tsx index 690a0a6f..1bb5bac4 100644 --- a/src/components/JSONViewer/FileChoosing.tsx +++ b/src/components/JSONViewer/FileChoosing.tsx @@ -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; }) => { @@ -75,11 +77,11 @@ const FileChoosing = ({ const promises: Promise[] = []; 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; @@ -118,6 +120,10 @@ const FileChoosing = ({ const selectFile = (fileName: string) => { const fileIndex = selectedFiles.indexOf(fileName); + if (!multiple) { + onSubmit([], [fileName]); + return; + } if (fileIndex > -1) { setSelectedFiles([ @@ -154,18 +160,22 @@ const FileChoosing = ({ value={search} onChange={e => setSearch(e.target.value)} /> - - + {multiple && ( + <> + + + + )} {isLoading ? (
diff --git a/src/components/JSONViewer/NotebookParamsCell.tsx b/src/components/JSONViewer/NotebookParamsCell.tsx index 60854b86..b4f808fc 100644 --- a/src/components/JSONViewer/NotebookParamsCell.tsx +++ b/src/components/JSONViewer/NotebookParamsCell.tsx @@ -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([]); - const [paramsValue, setParamsValue] = React.useState>({}); + const [paramsValue, setParamsValue] = React.useState([]); const [isLoading, setIsLoading] = React.useState(true); const [isRunLoading, setIsRunLoading] = React.useState(false); const [isExpanded, setIsExpanded] = React.useState(false); const [timer, setTimer] = React.useState(); const [taskId, setTaskId] = React.useState(); - const keys: string[] = React.useMemo(() => parameters.map(param => param.name), [parameters]); + const [resultCount, setResultCount] = React.useState('1'); + const [results, setResults] = React.useState([]); + 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); @@ -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': @@ -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); } @@ -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) { @@ -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); } @@ -176,33 +196,38 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => { )} - {parameters.map(parameter => ( - - - - - - - - - ) => { - const newState = paramsValue; - newState[parameter.name] = ev.target.value; - setParamsValue(newState); - }} - /> - - + {parameters.map((parameter, index) => ( + { + 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} + /> ))}
- @@ -212,6 +237,22 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => {
)} + {isExpanded && ( +
+
+
Results Amount:
+ ) => { + setResultCount(ev.target.value); + }} + /> +
+
+ )} ); }; diff --git a/src/components/JSONViewer/ParametersRow.tsx b/src/components/JSONViewer/ParametersRow.tsx new file mode 100644 index 00000000..d8b56194 --- /dev/null +++ b/src/components/JSONViewer/ParametersRow.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import moment from 'moment'; +import { InputNotebookParameter, NotebookParameter, TreeNode } from '../../models/JSONSchema'; +import FileChoosing from './FileChoosing'; +import { DateTimeInputType, DateTimeMask, TimeInputType } from '../../models/filter/FilterInputs'; +import { DATE_TIME_INPUT_MASK } from '../../util/filterInputs'; +import TimestampParameter from './TimestampParameter'; + +const possibleTypes = ['int', 'float', 'str', 'bool', 'file path', 'timestamp']; + +const ParametersRow = ({ + parameter, + parameterValue, + setParametersValue, + setParametersType, +}: { + parameter: NotebookParameter; + parameterValue: InputNotebookParameter; + setParametersValue: (newValue: string) => void; + setParametersType: (newValue: string) => void; +}) => { + const [browserOpen, setBrowserOpen] = React.useState(false); + const [timestamp, setTimestampNumber] = React.useState(moment.utc().valueOf()); + React.useEffect(() => { + if (parameterValue.type !== 'timestamp') return; + const momentFromDefault = moment.utc(parameterValue.value); + + if (momentFromDefault.isValid()) { + setParametersValue(momentFromDefault.toISOString()); + setTimestampNumber(momentFromDefault.valueOf()); + } else { + setParametersValue(moment.utc().toISOString()); + setTimestampNumber(moment.utc().valueOf()); + } + }, [parameterValue.type]); + + const updateValue = (_t: TreeNode[], files: string[]) => { + setParametersValue(files[0]); + setBrowserOpen(false); + }; + + const setTimestamp = (nextValue: number | null) => { + setTimestampNumber(nextValue); + setParametersValue(moment.utc(nextValue).toISOString()); + }; + + const timestampConfig: DateTimeInputType = { + id: 'startTimestamp', + value: timestamp, + setValue: setTimestamp, + type: TimeInputType.DATE_TIME, + dateMask: DateTimeMask.DATE_TIME_MASK, + placeholder: '', + inputMask: DATE_TIME_INPUT_MASK, + }; + + return ( + + + + + + + + +
+ {parameterValue.type === 'timestamp' ? ( + + ) : ( + <> + {parameterValue.type === 'file path' && ( +
+ + {parameterValue.type === 'file path' && browserOpen && ( + setBrowserOpen(false)} + /> + )} + + + ); +}; + +export default ParametersRow; diff --git a/src/components/JSONViewer/Table.tsx b/src/components/JSONViewer/Table.tsx index 1550bc30..aed72f8b 100644 --- a/src/components/JSONViewer/Table.tsx +++ b/src/components/JSONViewer/Table.tsx @@ -89,7 +89,7 @@ const TableRows = ({ } } if (typeof value === 'object') return

{JSON.stringify(value)}

; - return

{String(value)}

; + return

{typeof value === 'string' ? `"${value}"` : String(value)}

; }; return ( diff --git a/src/components/JSONViewer/TablePanel.tsx b/src/components/JSONViewer/TablePanel.tsx index e21c81f2..dc45c31a 100644 --- a/src/components/JSONViewer/TablePanel.tsx +++ b/src/components/JSONViewer/TablePanel.tsx @@ -5,7 +5,7 @@ import '../../styles/JSONviewer.scss'; import { TreeNode } from '../../models/JSONSchema'; const TablePanel = ({ node }: { node: TreeNode }) => { - const { key, simpleFields, complexFields } = node; + const { id, key, simpleFields, complexFields } = node; const nodeName = useMemo(() => { if (node.displayName) return node.displayName; if (node.key && !(node.isGeneratedKey && !node.isRoot)) return node.key; @@ -14,21 +14,27 @@ const TablePanel = ({ node }: { node: TreeNode }) => { return ( <> - {nodeName !== '' && ( -
-
-
- {nodeName} -
-
+ {id !== '' && ( + <> + {id !== '' && nodeName !== '' && ( +
+
+
+ {nodeName} +
+
+ )} + <> + +
+ + )} - <> -
-
- ); }; diff --git a/src/components/JSONViewer/TimestampParameter.tsx b/src/components/JSONViewer/TimestampParameter.tsx new file mode 100644 index 00000000..54853a91 --- /dev/null +++ b/src/components/JSONViewer/TimestampParameter.tsx @@ -0,0 +1,123 @@ +/** ***************************************************************************** + * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************** */ + +import React from 'react'; +import MaskedInput from 'react-text-mask'; +import moment from 'moment'; +import { DateTimeInputType } from '../../models/filter/FilterInputs'; +import { formatTimestampValue } from '../../helpers/date'; +import { replaceUnfilledDateStringWithMinValues } from '../../helpers/stringUtils'; +import { createStyleSelector } from '../../helpers/styleCreators'; +import FilterDatetimePicker from '../filter/date-time-inputs/FilterDatetimePicker'; + +interface DateTimeInputProps { + inputConfig: DateTimeInputType; +} + +const TimestampParameter = (props: DateTimeInputProps) => { + const { + inputConfig, + inputConfig: { + dateMask, + id, + inputClassName = '', + inputMask, + placeholder, + setValue, + value, + disabled, + }, + } = props; + + const inputRef = React.useRef(null); + + const [showPicker, setShowPicker] = React.useState(false); + const [inputValue, setInputValue] = React.useState(formatTimestampValue(value, dateMask)); + + React.useEffect(() => { + setInputValue(formatTimestampValue(props.inputConfig.value, dateMask)); + }, [props.inputConfig.value]); + + const togglePicker = (isShown: boolean) => setShowPicker(isShown); + + const inputChangeHandler = (e: React.ChangeEvent) => { + const { value: updatedValue } = e.target; + setInputValue(updatedValue); + + if (updatedValue) { + if (!updatedValue.includes('_')) { + setValue(moment.utc(updatedValue, dateMask).valueOf()); + } + return; + } + setValue(null); + }; + + const isValidDate = (maskedValue: string): boolean => { + const dateStr = replaceUnfilledDateStringWithMinValues(maskedValue, dateMask); + const date = moment(dateStr, dateMask); + + return date.isValid(); + }; + + const validPipe = (maskedValue: string): string | false => { + if (isValidDate(maskedValue)) { + return maskedValue; + } + return false; + }; + + const maskedInputClassName = createStyleSelector(inputClassName, value ? 'non-empty' : null); + + return ( + <> + togglePicker(true)} + onChange={inputChangeHandler} + placeholder={placeholder} + keepCharPositions={true} + autoComplete='off' + name={id} + value={inputValue} + /> + {showPicker && ( + togglePicker(false)} + /> + )} + + ); +}; + +export default TimestampParameter; diff --git a/src/components/JSONViewer/TreePanel.tsx b/src/components/JSONViewer/TreePanel.tsx index d098fbab..423863a7 100644 --- a/src/components/JSONViewer/TreePanel.tsx +++ b/src/components/JSONViewer/TreePanel.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { observer } from 'mobx-react-lite'; import '../../styles/JSONviewer.scss'; import { TreeNode, TreeViewType, ViewInstruction } from '../../models/JSONSchema'; @@ -6,6 +6,7 @@ import { createBemBlock } from '../../helpers/styleCreators'; import { useJSONViewerStore } from '../../hooks/useJSONViewerStore'; import JSONView from './JSONView'; import LeafTools from './LeafTools'; +import DisplayTable from './DisplayTable'; const TreePanel = ({ nest, @@ -17,13 +18,32 @@ const TreePanel = ({ prevKey?: string; }) => { const JSONViewerStore = useJSONViewerStore(); - const [viewType, setViewType] = React.useState(TreeViewType.EVENTS_LIST); + const [viewType, setViewType] = React.useState(treeNode.viewType || TreeViewType.EVENTS_LIST); + const [groupViewType, setGroupViewType] = React.useState( + treeNode.viewType || TreeViewType.EVENTS_LIST, + ); const [open, setOpen] = React.useState(false); const nodeName = useMemo(() => { if (treeNode.displayName) return treeNode.displayName; if (treeNode.key && !(treeNode.isGeneratedKey && !treeNode.isRoot)) return treeNode.key; return 'no display name'; }, [treeNode.displayName, treeNode.key, treeNode.isGeneratedKey]); + const [complexFields, setComplexFields] = React.useState(treeNode.complexFields); + const needBounding = useMemo( + () => + (open && viewType === TreeViewType.DISPLAY_TABLE) || + viewType === TreeViewType.JSON || + viewType === TreeViewType.PRETTY, + [open, viewType], + ); + + useEffect(() => { + setComplexFields(complexFields.map(field => ({ ...field, viewType: groupViewType }))); + }, [groupViewType]); + + useEffect(() => { + setOpen(viewType === TreeViewType.DISPLAY_TABLE); + }, [viewType]); const complexFieldsDisplay = () => ( @@ -84,14 +104,16 @@ const TreePanel = ({
+ )} + style={{ + marginBottom: needBounding ? '5px' : undefined, + }}>
-
- {treeNode.complexFields.length > 0 && ( +
+ {((complexFields.length > 0 && viewType === TreeViewType.EVENTS_LIST) || + viewType === TreeViewType.DISPLAY_TABLE) && (
setOpen(!open)} @@ -117,15 +139,25 @@ const TreePanel = ({ {complexFieldsDisplay()} {simpleFieldsDisplay()}
- {!(treeNode.isRoot && treeNode.isGeneratedKey) && ( - - )} +
+ {open && viewType === TreeViewType.DISPLAY_TABLE && ( + + )} {(viewType === TreeViewType.JSON || viewType === TreeViewType.PRETTY) && (
@@ -135,11 +167,12 @@ const TreePanel = ({ )}
{open && - treeNode.complexFields.map(field => ( + viewType === TreeViewType.EVENTS_LIST && + complexFields.map(field => ( ))} diff --git a/src/components/workspace/JSONViewerWorkspace.tsx b/src/components/workspace/JSONViewerWorkspace.tsx index 075fc98c..9a2b75f9 100644 --- a/src/components/workspace/JSONViewerWorkspace.tsx +++ b/src/components/workspace/JSONViewerWorkspace.tsx @@ -53,6 +53,7 @@ const JSONViewerWorkspace = () => { JSONViewerStore.setTreeNodes([]); JSONViewerStore.setNotebooks([]); JSONViewerStore.setTreeNodes(trees); + JSONViewerStore.selectTreeNode(); if (trees.length > 0) JSONViewerStore.selectTreeNode(trees[0]); JSONViewerStore.setNotebooks(notebooks); JSONViewerStore.setIsModalOpen(false, JSONViewerStore.modalType); @@ -95,6 +96,7 @@ const JSONViewerWorkspace = () => { }); JSONViewerStore.setTreeNodes(nodes); JSONViewerStore.setNotebooks([]); + JSONViewerStore.selectTreeNode(); if (nodes.length > 0) JSONViewerStore.selectTreeNode(nodes[0]); }; @@ -157,6 +159,7 @@ const JSONViewerWorkspace = () => { {JSONViewerStore.isModalOpen && ( JSONViewerStore.setIsModalOpen(false, JSONViewerStore.modalType)} /> diff --git a/src/helpers/JSONViewer.ts b/src/helpers/JSONViewer.ts index f3decc5a..35c78854 100644 --- a/src/helpers/JSONViewer.ts +++ b/src/helpers/JSONViewer.ts @@ -1,5 +1,13 @@ import { nanoid } from 'nanoid'; -import { Notebook, SimpleField, TreeNode } from '../models/JSONSchema'; +import moment from 'moment'; +import { + InputNotebookParameter, + Notebook, + NotebookParameter, + SimpleField, + TreeNode, + TreeViewType, +} from '../models/JSONSchema'; export const isNotebook = (obj: Object): obj is Notebook => { const entries = Object.entries(obj); @@ -20,6 +28,7 @@ export const convertJSONtoNode = (obj: object, key = '', isGeneratedKey = false) const complexFields: TreeNode[] = []; let viewInstruction = ''; let displayName: string | undefined; + let displayTable: string[][] | undefined; if (Array.isArray(obj)) { for (let i = 0; i < obj.length; i++) { if (typeof obj[i] === 'object') { @@ -35,7 +44,9 @@ export const convertJSONtoNode = (obj: object, key = '', isGeneratedKey = false) const entries = Object.entries(obj); for (let i = 0; i < entries.length; i++) { const [entryKey, value] = entries[i]; - if (entryKey === '#display-name') { + if (entryKey === '#display-table') { + displayTable = value; + } else if (entryKey === '#display-name') { displayName = String(value); } else if (entryKey === '#view-instruction') { viewInstruction = String(value); @@ -52,11 +63,13 @@ export const convertJSONtoNode = (obj: object, key = '', isGeneratedKey = false) return { id, key, + displayTable, displayName, failed, isArray, isGeneratedKey, viewInstruction, + viewType: TreeViewType.EVENTS_LIST, simpleFields, complexFields, }; @@ -75,3 +88,125 @@ export const parseText = (text: string, name = '', isGeneratedKey = false): Tree } return node.complexFields; }; + +const stringPunct = `'"\``; +const numberReg = /^-?\d*\.?\d{0,}$/; + +export const convertParameterValue = ( + value: string, + type: string, + cutString = false, +): { value: number | string | boolean; type: string } => { + try { + switch (type) { + case 'bool': { + return { + value: value.toLocaleLowerCase() === 'true', + type, + }; + } + case 'str': { + return { + value: cutString ? value.slice(1, value.length - 1) : value, + type, + }; + } + case 'int': { + return { + value: Number.parseInt(value), + type, + }; + } + case 'float': { + return { + value: Number.parseFloat(value), + type, + }; + } + default: { + return { + value, + type, + }; + } + case 'file path': { + return { + value: cutString ? value.slice(1, value.length - 1) : value, + type, + }; + } + case 'timestamp': { + return { + value: cutString ? value.slice(1, value.length - 1) : value, + type, + }; + } + } + } catch { + return { + value, + type, + }; + } +}; + +export const validateParameter = (value: string, type: string): boolean => { + switch (type) { + case 'int': { + return numberReg.test(value) && Number.isInteger(Number(value)); + } + case 'float': { + return numberReg.test(value); + } + case 'str': { + return true; + } + case 'bool': { + return value.toLocaleLowerCase() === 'true' || value.toLocaleLowerCase() === 'false'; + } + case 'file path': { + return true; + } + case 'timestamp': { + return moment.utc(value).isValid(); + } + default: { + return true; + } + } +}; + +export const getParameterType = (parameter: NotebookParameter) => { + const { default: value, inferred_type_name: type, name } = parameter; + + if (type !== 'None') return type; + if (name.endsWith('_timestamp')) { + return 'timestamp'; + } + if (name.endsWith('_file')) { + return 'file path'; + } + if (stringPunct.includes(value[0]) && value[0] === value[value.length - 1]) { + return 'str'; + } + if (value === 'True' || value === 'False') { + return 'bool'; + } + if (numberReg.test(value)) { + if (Number.isInteger(Number(value))) { + return 'int'; + } + return 'float'; + } + return 'str'; +}; + +export const convertParameterToInput = (parameter: NotebookParameter): InputNotebookParameter => { + const type = getParameterType(parameter); + return { + name: parameter.name, + value: String(convertParameterValue(parameter.default, type, true).value), + type, + isValid: validateParameter(parameter.default, type), + }; +}; diff --git a/src/models/JSONSchema.ts b/src/models/JSONSchema.ts index a7047107..c6ac43b1 100644 --- a/src/models/JSONSchema.ts +++ b/src/models/JSONSchema.ts @@ -4,6 +4,7 @@ export enum ViewInstruction { } export enum TreeViewType { + DISPLAY_TABLE = 'Table', EVENTS_LIST = 'Tree', JSON = 'Json', PRETTY = 'Formatted Json', @@ -21,6 +22,7 @@ export interface TreeNode { id: string; key: string; displayName?: string; + displayTable?: string[][]; failed: boolean; viewInstruction: string; complexFields: TreeNode[]; @@ -38,6 +40,13 @@ export interface NotebookParameter { help: string; } +export interface InputNotebookParameter { + name: string; + type: string; + value: string; + isValid: boolean; +} + export interface NotebookParameters { [name: string]: NotebookParameter; } diff --git a/src/stores/JSONViewerStore.ts b/src/stores/JSONViewerStore.ts index ddef560e..be249464 100644 --- a/src/stores/JSONViewerStore.ts +++ b/src/stores/JSONViewerStore.ts @@ -1,5 +1,6 @@ import { action, observable } from 'mobx'; import { TreeNode } from '../models/JSONSchema'; +import { WorkspacePanelsLayout } from '../components/workspace/WorkspaceSplitter'; const nullTreeNode: TreeNode = { id: '', @@ -11,7 +12,7 @@ const nullTreeNode: TreeNode = { }; export class JSONViewerStore { - constructor(private openTableTab: () => void) {} + constructor(private openTabs: (layout: WorkspacePanelsLayout) => void) {} @observable public isModalOpen = false; @@ -42,9 +43,6 @@ export class JSONViewerStore { @action selectTreeNode(tree?: TreeNode) { if (tree) { this.selectedTreeNode = tree; - if (tree.id !== '') { - this.openTableTab(); - } } else { this.selectedTreeNode = nullTreeNode; } @@ -53,4 +51,8 @@ export class JSONViewerStore { @action addNodes(tree: TreeNode[]) { this.treeNodes = this.treeNodes.concat(tree); } + + @action removeNodesById(ids: string[]) { + this.treeNodes = this.treeNodes.filter(node => !ids.includes(node.id)); + } } diff --git a/src/stores/workspace/JSONViewerWorkspaceStore.ts b/src/stores/workspace/JSONViewerWorkspaceStore.ts index 1b166f40..221181e2 100644 --- a/src/stores/workspace/JSONViewerWorkspaceStore.ts +++ b/src/stores/workspace/JSONViewerWorkspaceStore.ts @@ -16,7 +16,7 @@ export default class JSONViewerWorkspaceStore { constructor(private workspacesStore: WorkspacesStore) { this.viewStore = new WorkspaceViewStore({ panelsLayout: [100, 0] }); - this.JSONviewerStore = new JSONViewerStore(() => this.viewStore.setPanelsLayout([50, 50])); + this.JSONviewerStore = new JSONViewerStore(this.viewStore.setPanelsLayout); } @action diff --git a/src/styles/JSONviewer.scss b/src/styles/JSONviewer.scss index 92bbd063..9f6470a0 100644 --- a/src/styles/JSONviewer.scss +++ b/src/styles/JSONviewer.scss @@ -165,7 +165,7 @@ &-wrapper { overflow-x: auto; table { - grid-template-columns: 20% 80%; + grid-template-columns: 30% 70%; } } diff --git a/src/styles/jupyter.scss b/src/styles/jupyter.scss index e5ee4665..d19b6be6 100644 --- a/src/styles/jupyter.scss +++ b/src/styles/jupyter.scss @@ -3,6 +3,16 @@ .notebookCell { border: 3px $failedBorderColor solid; border-radius: 5px; + + input { + font-size: 12px; + border: 1px solid; + border-radius: 5px; + padding: 0; + margin: 0; + color: inherit; + min-height: auto; + } &-header { >label { @@ -62,12 +72,16 @@ display: flex; justify-content: space-between; padding: 5px; + gap: 10px; + border: 0px black solid; + border-bottom-width: 1px; &-table { + width: 100%; table { + width: 100%; border-collapse: collapse; border-style: hidden; - table-layout: fixed; border-radius: 4px; min-width: 50%; @@ -78,10 +92,30 @@ td { font-size: 12px; - input { - font-size: 12px; - border: 1px solid; - border-radius: 5px; + input.failed { + border-color: $failedTextColor; + } + + input.failed:focus { + border-color: $failedTextColor; + outline-color: $failedTextColor; + } + + .input-wrapper { + width: 100%; + display: flex; + position: relative; + gap: 5px; + + .rc-calendar { + width: auto; + } + + .open-browser { + @include icon(url(../../resources/icons/split.svg), 16px, 16px); + cursor: pointer; + border-radius: 5px; + } } } @@ -89,6 +123,12 @@ td { padding: 2px 8px; } + + th:last-child, + td:last-child { + padding: 2px 8px; + width: 100%; + } } } @@ -113,4 +153,43 @@ } } + &-settings { + padding: 5px; + display: flex; + flex-direction: column; + gap: 5; + font-size: 14px; + } +} + +.display-table { + @include default-table; + width: 100%; + &-error { + padding: 2px; + font-size: 14px; + color: $failedTextColor; + } + + &-info { + cursor: pointer; + @include icon(url(../../resources/icons/info.svg), 16px, 16px); + } + + + table { + border-style: solid; + border-width: 0px 0px 1px 0px; + border-color: $alternativeTextColor; + grid-auto-rows: auto; + + th,td { + display: table-cell; + text-align: left; + } + + th { + background-color: $secondaryLightTextColor; + } + } } \ No newline at end of file diff --git a/src/styles/messages.scss b/src/styles/messages.scss index c49b1b0d..b304b074 100644 --- a/src/styles/messages.scss +++ b/src/styles/messages.scss @@ -181,6 +181,16 @@ @include icon(url(../../resources/icons/ascii.svg)); } + &.tree { + @include icon(url(../../resources/icons/tree-view-icon.svg)); + opacity: 0.5; + } + + &.table { + @include icon(url(../../resources/icons/table-view-icon.svg)); + opacity: 0.5; + } + &.download { @include icon(url(../../resources/icons/download.svg), 15px, 15px); }