From 3bac0427213725b6c4bf1e59032c332f565ded95 Mon Sep 17 00:00:00 2001 From: Egor Yashin <61899414+molotgor@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:06:00 +0400 Subject: [PATCH] [Th2-5208] Add ability to cancel notebooks, 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 (#571) * fix parameters type parsing * add ability to cancel launch and proccess failed notebook request * 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 * update launchNotebook and getResults api * change display when none node is selected * change split behaviour on selection * added filepath and timestamp parameters --- package.json | 2 +- src/api/ApiSchema.ts | 6 +- src/api/JSONViewer.ts | 26 +- src/components/JSONViewer/DisplayTable.tsx | 56 +++++ src/components/JSONViewer/FileChoosing.tsx | 40 +-- src/components/JSONViewer/LeafTools.tsx | 112 +++++++++ .../JSONViewer/NotebookParamsCell.tsx | 233 ++++++++++++------ src/components/JSONViewer/ParametersRow.tsx | 117 +++++++++ src/components/JSONViewer/Table.tsx | 153 +++++++----- src/components/JSONViewer/TablePanel.tsx | 44 ++-- .../JSONViewer/TimestampParameter.tsx | 123 +++++++++ src/components/JSONViewer/TreePanel.tsx | 157 ++++++++---- .../workspace/JSONViewerWorkspace.tsx | 29 +-- src/helpers/JSONViewer.ts | 143 ++++++++++- src/models/JSONSchema.ts | 16 +- src/stores/JSONViewerStore.ts | 50 ++-- .../workspace/JSONViewerWorkspaceStore.ts | 2 +- src/styles/JSONviewer.scss | 98 +++++--- src/styles/jupyter.scss | 89 ++++++- src/styles/messages.scss | 10 + webpack/webpack.dev.js | 8 +- 21 files changed, 1191 insertions(+), 323 deletions(-) create mode 100644 src/components/JSONViewer/DisplayTable.tsx create mode 100644 src/components/JSONViewer/LeafTools.tsx create mode 100644 src/components/JSONViewer/ParametersRow.tsx create mode 100644 src/components/JSONViewer/TimestampParameter.tsx diff --git a/package.json b/package.json index 31ae6fce..119cc3f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "th2-rpt-viewer", - "version": "5.2.7", + "version": "5.2.8", "description": "", "main": "index.tsx", "private": true, diff --git a/src/api/ApiSchema.ts b/src/api/ApiSchema.ts index 008769ba..940c4dce 100644 --- a/src/api/ApiSchema.ts +++ b/src/api/ApiSchema.ts @@ -111,8 +111,10 @@ export interface BooksApiSchema { export interface JSONViewerApiSchema { getLinks: (type: string, dir?: string) => Promise<{ directories: string[]; files: string[] }>; getParameters: (path: string) => Promise; - getResults: (path: string) => Promise<{ result: string }>; - launchNotebook: (path: string, parameters?: Object) => Promise<{ path: 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; } export interface SSESchema { diff --git a/src/api/JSONViewer.ts b/src/api/JSONViewer.ts index a5ec5e6b..f1025780 100644 --- a/src/api/JSONViewer.ts +++ b/src/api/JSONViewer.ts @@ -28,13 +28,21 @@ const JSONViewerHttpApi: JSONViewerApiSchema = { notificationsStore.handleRequestError(res); return {}; }, - getResults: async (path: string): Promise<{ result: string }> => { - const res = await fetch(`json-stream-provider/result?path=${path}`); + getResults: async (taskId: string): Promise<{ status: string; result: string }> => { + const res = await fetch(`json-stream-provider/result?id=${taskId}`); if (res.ok) { return res.json(); } notificationsStore.handleRequestError(res); - return { result: path }; + 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}`, { @@ -48,7 +56,17 @@ const JSONViewerHttpApi: JSONViewerApiSchema = { return res.json(); } notificationsStore.handleRequestError(res); - return { path: '' }; + return { path: '', task_id: '' }; + }, + stopNotebook: async (taskId: string) => { + const res = await fetch(`json-stream-provider/stop?id=${taskId}`, { + method: 'POST', + }); + if (res.ok) { + return true; + } + // notificationsStore.handleRequestError(res); + return false; }, }; 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/LeafTools.tsx b/src/components/JSONViewer/LeafTools.tsx new file mode 100644 index 00000000..4769d8a4 --- /dev/null +++ b/src/components/JSONViewer/LeafTools.tsx @@ -0,0 +1,112 @@ +/** **************************************************************************** + * 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, { useRef, useState } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { TreeViewType } from '../../models/JSONSchema'; +import { useOutsideClickListener } from '../../hooks'; +import { createBemElement } from '../../helpers/styleCreators'; +import { MessageViewType } from '../../models/EventMessage'; + +export type LeafToolsConfig = { + activeViewType: TreeViewType | MessageViewType; + toggleViewType: (viewType: any) => void; + viewTypes: TreeViewType[] | MessageViewType[]; +}; + +const LeafTools = ({ activeViewType, toggleViewType, viewTypes }: LeafToolsConfig) => { + const [isViewMenuOpen, setIsViewMenuOpen] = useState(false); + const rootRef = useRef(null); + + useOutsideClickListener( + rootRef, + (e: MouseEvent) => { + if (e.target instanceof Element && rootRef.current && !rootRef.current.contains(e.target)) { + setIsViewMenuOpen(false); + } + }, + isViewMenuOpen, + ); + + return ( +
+
{ + e.stopPropagation(); + setIsViewMenuOpen(isOpen => !isOpen); + }}> +
+
+ +
+ {viewTypes.map(viewType => { + const iconClassName = createBemElement('message-card-tools', 'icon', viewType); + const indicatorClassName = createBemElement( + 'message-card-tools', + 'indicator', + viewType === activeViewType ? 'active' : null, + ); + + return ( +
{ + e.stopPropagation(); + toggleViewType(viewType); + }}> + {viewType} +
+
+
+ ); + })} +
+ +
+ ); +}; + +export default LeafTools; + +interface ToolsPopupProps { + isOpen: boolean; + children: React.ReactNode; +} + +function ToolsPopup({ isOpen, children }: ToolsPopupProps) { + return ( + + {isOpen && ( + + {children} + + )} + + ); +} diff --git a/src/components/JSONViewer/NotebookParamsCell.tsx b/src/components/JSONViewer/NotebookParamsCell.tsx index e8a36a99..b4f808fc 100644 --- a/src/components/JSONViewer/NotebookParamsCell.tsx +++ b/src/components/JSONViewer/NotebookParamsCell.tsx @@ -1,24 +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 maxFetchResults = 5; 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(null); - const keys: string[] = React.useMemo(() => parameters.map(param => param.name), [parameters]); + const [timer, setTimer] = React.useState(); + const [taskId, setTaskId] = React.useState(); + 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); @@ -38,74 +60,104 @@ const NotebookParamsCell = ({ notebook }: { notebook: string }) => { setIsExpanded(!isExpanded); }; - const getResults = async (path: string, launchN = 1) => { - const { result } = await api.jsonViewer.getResults(path); - if (result.includes('{')) { - const node: TreeNode = { - id: nanoid(), - key: `Result of ${notebook}'s run`, - failed: false, - viewInstruction: '', - simpleFields: [{ key: 'filepath', value: path }], - complexFields: [], - isGeneratedKey: true, - isRoot: true, - }; - try { - node.complexFields.push(...parseText(result, '0', true)); - } catch { - const lines = result.split('\n'); - for (let i = 0; i < lines.length; i++) { - if (lines[i] !== '') { - node.complexFields.push(...parseText(lines[i], String(i), true)); + const getResults = async (respTaskId: string) => { + const { status, result, path } = await api.jsonViewer.getResults(respTaskId); + + switch (status) { + case 'success': + if (result.includes('{')) { + const node: TreeNode = { + id: nanoid(), + key: `Result of ${notebook}'s run`, + failed: false, + viewInstruction: '', + simpleFields: [{ key: 'filepath', value: path }], + complexFields: [], + isGeneratedKey: true, + isRoot: true, + }; + try { + node.complexFields.push(...parseText(result, '0', true)); + } catch { + const lines = result.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (lines[i] !== '') { + node.complexFields.push(...parseText(lines[i], String(i), true)); + } + } + } + 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); } + setIsRunLoading(false); + setIsExpanded(false); } - } - node.failed = node.complexFields.some(v => v.failed); - if (node.complexFields.length > 0) { - JSONViewerStore.addNodes([node]); - JSONViewerStore.selectTreeNode(node); - } - setParamsValue({}); - setIsRunLoading(false); - setIsExpanded(false); - return; - } - if (launchN < maxFetchResults) { - setTimeout(() => getResults(path, launchN + 1), 2 * launchN * timeBetweenResults); + break; + case 'failed': + { + const response = new Response(result, { + status: 500, + statusText: `Failed to launch ${notebook}`, + }); + notificationsStore.handleRequestError(response); + setIsRunLoading(false); + } + break; + case 'in progress': + 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) { - setIsRunLoading(false); if (timer) { - timer.unref(); + clearTimeout(timer); setTimer(null); } + if (taskId) { + await api.jsonViewer.stopNotebook(taskId); + setTaskId(null); + } else { + setIsRunLoading(false); + } + setIsRunLoading(false); return; } 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: - return [name, value]; - } - }), + paramsValue + .filter(filterParameters) + .map(({ name, type, value }) => [name, convertParameterValue(value, type)]), ); const res = await api.jsonViewer.launchNotebook(notebook, paramsWithType); - if (res.path !== '') { - setTimeout(() => getResults(res.path), timeBetweenResults); + if (res.task_id !== '') { + setTaskId(res.task_id); + setTimer(setTimeout(() => getResults(res.task_id), timeBetweenResults)); } else { setIsRunLoading(false); } @@ -144,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} + /> ))}
- @@ -180,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 8b8cbb60..aed72f8b 100644 --- a/src/components/JSONViewer/Table.tsx +++ b/src/components/JSONViewer/Table.tsx @@ -1,7 +1,10 @@ -import React from 'react'; -import { SimpleField, TreeNode } from '../../models/JSONSchema'; +import React, { useMemo } from 'react'; +import { SimpleField, TreeNode, TreeViewType } from '../../models/JSONSchema'; import { createBemBlock } from '../../helpers/styleCreators'; -import { isKeyFailed, isValueFailed } from '../../helpers/JSONViewer'; +import DetailedMessageRaw from '../message/message-card/raw/DetailedMessageRaw'; +import { decodeBase64RawContent } from '../../helpers/rawFormatter'; +import SimpleMessageRaw from '../message/message-card/raw/SimpleMessageRaw'; +import LeafTools from './LeafTools'; const Table = ({ simpleFields, @@ -10,12 +13,14 @@ const Table = ({ simpleFields: SimpleField[]; complexFields: TreeNode[]; }) => ( -
-
- +
+
+
- + @@ -29,67 +34,105 @@ const Table = ({ ); +const Base64Cell = ({ value }: { value: string }) => { + const [viewType, setViewType] = React.useState(TreeViewType.ASCII); + const viewTypes = [TreeViewType.ORIGIN, TreeViewType.BINARY, TreeViewType.ASCII]; + + switch (viewType) { + case TreeViewType.ASCII: + return ( +
+ + +
+ ); + case TreeViewType.BINARY: + return ( +
+ + +
+ ); + case TreeViewType.ORIGIN: + return ( +
+
+

{String(value)}

+
+ +
+ ); + default: + return <>; + } +}; + const TableRows = ({ simpleFields, complexFields, }: { simpleFields: SimpleField[]; complexFields: TreeNode[]; -}) => ( - <> - {simpleFields.map(({ key, value }) => ( - - {value === '' ? ( - - ) : ( - <> - + {value === '' ? ( + - - - )} - - ))} - {complexFields.map(field => ( - - ))} - -); + ) : ( + <> + + + + )} + + ))} + {complexFields.map(field => ( + + ))} + + ); +}; const ExpandRow = ({ field }: { field: TreeNode }) => { const [isOpen, setIsOpen] = React.useState(false); + const nodeName = useMemo(() => { + if (field.displayName) return field.displayName; + if (field.key && !(field.isGeneratedKey && !field.isRoot)) return field.key; + return 'no display name'; + }, [field.displayName, field.key, field.isGeneratedKey]); + return ( <> - setIsOpen(!isOpen)}> + setIsOpen(!isOpen)}> @@ -97,9 +140,9 @@ const ExpandRow = ({ field }: { field: TreeNode }) => { {isOpen && (
+ fieldKey + fieldValue
-

{key}

-
+}) => { + const getValue = ({ key, value }: SimpleField) => { + if (key.endsWith('Base64')) { + try { + decodeBase64RawContent(value); + return ; + } catch (error) { + return ( +
+

Failed to decode Base64:

+

{String(value)}

+
+ ); + } + } + if (typeof value === 'object') return

{JSON.stringify(value)}

; + return

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

; + }; + + return ( + <> + {simpleFields.map(({ key, value }, index) => ( +

{key}

-

{typeof value === 'object' ? JSON.stringify(value) : String(value)}

-
+

{key}

+
{getValue({ key, value })}
-
- {field.key} +
+ {nodeName}
-
-
- +
+
+
{ - const { key, viewInstruction, 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; + return 'no display name'; + }, [node.displayName, node.key, node.isGeneratedKey]); return ( <> - {key !== '' && ( -
-
- {key} -
- )} - {viewInstruction === ViewInstruction.table ? ( + {id !== '' && ( <> -
-
- - ) : ( - simpleFields.length > 0 && ( + {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 ca0647a0..423863a7 100644 --- a/src/components/JSONViewer/TreePanel.tsx +++ b/src/components/JSONViewer/TreePanel.tsx @@ -1,10 +1,12 @@ -import React 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'; 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, @@ -16,20 +18,32 @@ const TreePanel = ({ prevKey?: string; }) => { const JSONViewerStore = useJSONViewerStore(); + 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], + ); - if (!treeNode.isRoot && JSONViewerStore.viewType !== TreeViewType.EVENTS_LIST) { - return ( -
-
- -
-
- ); - } + useEffect(() => { + setComplexFields(complexFields.map(field => ({ ...field, viewType: groupViewType }))); + }, [groupViewType]); + + useEffect(() => { + setOpen(viewType === TreeViewType.DISPLAY_TABLE); + }, [viewType]); const complexFieldsDisplay = () => ( @@ -56,17 +70,29 @@ const TreePanel = ({ treeNode.failed ? 'failed' : 'passed', treeNode.id === JSONViewerStore.selectedTreeNode.id ? 'selected' : null, )} - title={treeNode.key} + title={nodeName} onClick={() => { JSONViewerStore.selectTreeNode(treeNode); }}> -
- - {treeNode.key} - {' '} - - {complexFieldsDisplay()} {simpleFieldsDisplay()} - +
+
+ + {nodeName} + {' '} + + {complexFieldsDisplay()} {simpleFieldsDisplay()} + +
+
@@ -75,41 +101,78 @@ const TreePanel = ({ return ( <> -
-
- {treeNode.complexFields.length > 0 && ( -
setOpen(!open)} - /> +
+
+
+ {((complexFields.length > 0 && viewType === TreeViewType.EVENTS_LIST) || + viewType === TreeViewType.DISPLAY_TABLE) && ( +
setOpen(!open)} + /> )} - title={treeNode.key} - onClick={() => { - JSONViewerStore.selectTreeNode(treeNode); - }}>
- - {treeNode.key} - {' '} - - {complexFieldsDisplay()} {simpleFieldsDisplay()} - + className={createBemBlock('valueLeaf')} + title={nodeName} + onClick={() => { + JSONViewerStore.selectTreeNode(treeNode); + }}> +
+
+ + {nodeName} + {' '} + + {complexFieldsDisplay()} {simpleFieldsDisplay()} + +
+ +
+ {open && viewType === TreeViewType.DISPLAY_TABLE && ( + + )} + {(viewType === TreeViewType.JSON || viewType === TreeViewType.PRETTY) && ( +
+
+ +
+
+ )}
{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 16a194cc..9a2b75f9 100644 --- a/src/components/workspace/JSONViewerWorkspace.tsx +++ b/src/components/workspace/JSONViewerWorkspace.tsx @@ -21,7 +21,7 @@ import { computed } from 'mobx'; import { nanoid } from 'nanoid'; import WorkspaceSplitter from './WorkspaceSplitter'; import '../../styles/workspace.scss'; -import { TreeNode, TreeViewType } from '../../models/JSONSchema'; +import { TreeNode } from '../../models/JSONSchema'; import TablePanel from '../JSONViewer/TablePanel'; import useJSONViewerWorkspace from '../../hooks/useJSONViewerWorkspace'; import { useJSONViewerStore } from '../../hooks/useJSONViewerStore'; @@ -53,8 +53,8 @@ const JSONViewerWorkspace = () => { JSONViewerStore.setTreeNodes([]); JSONViewerStore.setNotebooks([]); JSONViewerStore.setTreeNodes(trees); - if (JSONViewerStore.viewType === TreeViewType.EVENTS_LIST && trees.length > 0) - JSONViewerStore.selectTreeNode(trees[0]); + JSONViewerStore.selectTreeNode(); + if (trees.length > 0) JSONViewerStore.selectTreeNode(trees[0]); JSONViewerStore.setNotebooks(notebooks); JSONViewerStore.setIsModalOpen(false, JSONViewerStore.modalType); }; @@ -96,8 +96,8 @@ const JSONViewerWorkspace = () => { }); JSONViewerStore.setTreeNodes(nodes); JSONViewerStore.setNotebooks([]); - if (JSONViewerStore.viewType === TreeViewType.EVENTS_LIST && nodes.length > 0) - JSONViewerStore.selectTreeNode(nodes[0]); + JSONViewerStore.selectTreeNode(); + if (nodes.length > 0) JSONViewerStore.selectTreeNode(nodes[0]); }; const computeTreeKey = React.useCallback( @@ -111,13 +111,6 @@ const JSONViewerWorkspace = () => { return ; }, []); - const setView = (v: string) => { - JSONViewerStore.setView(v); - if (v !== TreeViewType.EVENTS_LIST) { - setPanelsLayout([100, 0]); - } else setPanelsLayout([50, 50]); - }; - const treePanel = React.useMemo( () => computed(() => ({ @@ -148,17 +141,6 @@ const JSONViewerWorkspace = () => { onClick={() => inputRef.current?.click()}> Load Local Result(s) -
- - -
{ {JSONViewerStore.isModalOpen && ( JSONViewerStore.setIsModalOpen(false, JSONViewerStore.modalType)} /> diff --git a/src/helpers/JSONViewer.ts b/src/helpers/JSONViewer.ts index c6a73c92..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); @@ -19,6 +27,8 @@ export const convertJSONtoNode = (obj: object, key = '', isGeneratedKey = false) const simpleFields: SimpleField[] = []; 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') { @@ -34,7 +44,11 @@ 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 === 'view_instruction') { + if (entryKey === '#display-table') { + displayTable = value; + } else if (entryKey === '#display-name') { + displayName = String(value); + } else if (entryKey === '#view-instruction') { viewInstruction = String(value); } else if (typeof value === 'object' && value !== null) { const val = convertJSONtoNode(value, entryKey); @@ -49,10 +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, }; @@ -71,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 4712c890..c6ac43b1 100644 --- a/src/models/JSONSchema.ts +++ b/src/models/JSONSchema.ts @@ -4,9 +4,13 @@ export enum ViewInstruction { } export enum TreeViewType { - EVENTS_LIST = 'Event List', + DISPLAY_TABLE = 'Table', + EVENTS_LIST = 'Tree', JSON = 'Json', PRETTY = 'Formatted Json', + ASCII = 'ASCII', + BINARY = 'binary', + ORIGIN = 'Origin', } export interface SimpleField { @@ -17,6 +21,8 @@ export interface SimpleField { export interface TreeNode { id: string; key: string; + displayName?: string; + displayTable?: string[][]; failed: boolean; viewInstruction: string; complexFields: TreeNode[]; @@ -24,6 +30,7 @@ export interface TreeNode { isArray?: boolean; isGeneratedKey?: boolean; isRoot?: boolean; + viewType?: TreeViewType; } export interface NotebookParameter { @@ -33,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 9a2ec9a6..be249464 100644 --- a/src/stores/JSONViewerStore.ts +++ b/src/stores/JSONViewerStore.ts @@ -1,8 +1,18 @@ import { action, observable } from 'mobx'; -import { TreeNode, TreeViewType } from '../models/JSONSchema'; +import { TreeNode } from '../models/JSONSchema'; +import { WorkspacePanelsLayout } from '../components/workspace/WorkspaceSplitter'; + +const nullTreeNode: TreeNode = { + id: '', + key: '', + failed: false, + viewInstruction: '', + complexFields: [], + simpleFields: [], +}; export class JSONViewerStore { - constructor(private openTableTab: () => void) {} + constructor(private openTabs: (layout: WorkspacePanelsLayout) => void) {} @observable public isModalOpen = false; @@ -10,20 +20,11 @@ export class JSONViewerStore { @observable public modalType: 'notebooks' | 'results' = 'results'; - @observable viewType: string = TreeViewType.EVENTS_LIST; - @observable notebooks: string[] = []; @observable treeNodes: TreeNode[] = []; - @observable selectedTreeNode: TreeNode = { - id: '', - key: '', - failed: false, - viewInstruction: '', - complexFields: [], - simpleFields: [], - }; + @observable selectedTreeNode: TreeNode = nullTreeNode; @action public setIsModalOpen = (v: boolean, type: 'notebooks' | 'results') => { @@ -39,10 +40,11 @@ export class JSONViewerStore { this.notebooks = n.slice(); } - @action selectTreeNode(tree: TreeNode) { - this.selectedTreeNode = tree; - if (tree.id !== '') { - this.openTableTab(); + @action selectTreeNode(tree?: TreeNode) { + if (tree) { + this.selectedTreeNode = tree; + } else { + this.selectedTreeNode = nullTreeNode; } } @@ -50,19 +52,7 @@ export class JSONViewerStore { this.treeNodes = this.treeNodes.concat(tree); } - @action setView(v: string) { - this.viewType = v; - if (v !== TreeViewType.EVENTS_LIST) { - this.selectTreeNode({ - id: '', - key: '', - failed: false, - viewInstruction: '', - complexFields: [], - simpleFields: [], - }); - } else if (this.treeNodes.length > 0) { - this.selectTreeNode(this.treeNodes[0]); - } + @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 4fd1c107..9f6470a0 100644 --- a/src/styles/JSONviewer.scss +++ b/src/styles/JSONviewer.scss @@ -18,6 +18,7 @@ cursor: pointer; gap: 5px; position: relative; + justify-content: space-between; &:hover { background-color: var(--status-secondary-background-color); @@ -29,6 +30,18 @@ &.selected { border-color: var(--status-primary-border-color); } + + &.header { + justify-content: flex-start; + height: 20px; + + .title { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + } color: var(--status-primary-color); font-size: 11px; @@ -126,13 +139,34 @@ padding: 1px 5px 1px 0; } +.leaf { + display: flex; + flex-direction: column; + border-radius: 5px; + &.expanded { + border: 2px solid; + border-color: $attachedMessageCardBorderColor; + .leafWrapper { + border-style: solid; + border-width: 0px 0px 1px 0px; + border-color: $alternativeTextColor; + } + } + &.selected { + border: 2px solid; + border-color: var(--status-primary-border-color); + } +} -.params-table { +.json-table { @include scrollbar; @include default-table; &-wrapper { overflow-x: auto; + table { + grid-template-columns: 30% 70%; + } } td, @@ -140,59 +174,51 @@ text-align: left; } + tr { + &:hover td { + background-color: white; + } + } + display: block; width: 100%; height: auto; overflow: auto; &-row-value { - &.passed { - & td { - background-color: $tableRowPassedBackgroundColor; - } - - &:hover td { - background-color: darken($color: $tableRowPassedBackgroundColor, $amount: 10); - } - } - - &.failed { - & td { - background-color: $tableRowFailedBackgroundColor; - } - - &:hover td { - background-color: darken($color: $tableRowFailedBackgroundColor, $amount: 10); - } - } p { margin: 0; text-decoration: none; font-size: 12px; } + + &:hover td { + background-color: darken(white, 10px) !important; + } + + td .mc-raw__content { + letter-spacing: 0; + } } + + &-Base64Cell { + width: 100%; + display: flex; + justify-content: space-between; + } + &-row-toogler { cursor: pointer; color: #fff; - &.passed { - & td { - background-color: darken($color: $tableRowPassedBackgroundColor, $amount: 50); - } - - &:hover td { - background-color: darken($color: $tableRowPassedBackgroundColor, $amount: 60); - } + + &:hover td { + background-color: darken($tableHeaderBackgroundSecondary, 10px) !important; } - &.failed { - & td { - background-color: darken($color: $tableRowFailedBackgroundColor, $amount: 40); - } - - &:hover td { - background-color: darken($color: $tableRowFailedBackgroundColor, $amount: 50); - } + & td { + background-color: $tableHeaderBackgroundSecondary !important; } + .expand-icon { background-color: white; display: inline-block; 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); } diff --git a/webpack/webpack.dev.js b/webpack/webpack.dev.js index 62816e2e..b56df198 100644 --- a/webpack/webpack.dev.js +++ b/webpack/webpack.dev.js @@ -42,7 +42,13 @@ module.exports = merge(commonConfig, { port: 9001, host: '0.0.0.0', historyApiFallback: true, - proxy: { + proxy: { + '/json-stream-provider': { + target: 'http://localhost:8080/', + changeOrigin: true, + secure: false, + pathRewrite: { '^/json-stream-provider': '' }, + }, '/': { target: 'http://kos-perftest-kuber-master:30000/th2-demo-transport/', changeOrigin: true,