From 662f564cbb9d166ccbdb61fa21abb8245d1749e9 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Thu, 17 Oct 2024 15:05:50 +0300 Subject: [PATCH 1/3] chore(Queries): query result component refactoring [YTFRONT-4423] --- .../src/ui/UIFactory/default-ui-factory.tsx | 6 +- packages/ui/src/ui/UIFactory/index.tsx | 12 +-- .../src/ui/pages/query-tracker/Plan/utils.ts | 55 ------------ .../QueryResults/PlanContainer.tsx | 48 +++++++++++ .../QueryResults/QueryChartTab.tsx | 11 +++ .../QueryResults/QueryResultContainer.tsx | 22 +++++ .../QueryResults/helpers/buildOperationUrl.ts | 11 +++ .../helpers/extractOperationIdToCluster.ts | 23 +++++ .../QueryResults/helpers/getOperationUrl.ts | 14 ++++ .../QueryResults/hooks/useQueryResultTabs.tsx | 10 +-- .../query-tracker/QueryResults/index.tsx | 84 ++++--------------- .../QueryResultsVisualization.tsx | 1 - .../QueryResultsVisualization/index.tsx | 5 +- 13 files changed, 160 insertions(+), 142 deletions(-) create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResults/PlanContainer.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResults/QueryChartTab.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResults/QueryResultContainer.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/buildOperationUrl.ts create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/extractOperationIdToCluster.ts create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/getOperationUrl.ts diff --git a/packages/ui/src/ui/UIFactory/default-ui-factory.tsx b/packages/ui/src/ui/UIFactory/default-ui-factory.tsx index 95f32599d..67903ebd1 100644 --- a/packages/ui/src/ui/UIFactory/default-ui-factory.tsx +++ b/packages/ui/src/ui/UIFactory/default-ui-factory.tsx @@ -12,7 +12,7 @@ import {uiSettings} from '../config/ui-settings'; import YT from '../config/yt-config'; import {DefaultSubjectLinkLazy} from '../components/SubjectLink/lazy'; import type {SubjectCardProps} from '../components/SubjectLink/SubjectLink'; -import {CUSTOM_QUERY_REQULT_TAB} from '../pages/query-tracker/QueryResultsVisualization'; +import {QUERY_RESULT_CHART_TAB} from '../pages/query-tracker/QueryResultsVisualization'; import {UIFactory} from './index'; @@ -265,8 +265,8 @@ export const defaultUIFactory: UIFactory = { return undefined; }, - getCustomQueryResultTab() { - return CUSTOM_QUERY_REQULT_TAB; + getQueryResultChartTab() { + return QUERY_RESULT_CHART_TAB; }, getExternalSettings() { diff --git a/packages/ui/src/ui/UIFactory/index.tsx b/packages/ui/src/ui/UIFactory/index.tsx index 881db4268..3ddfdebb1 100644 --- a/packages/ui/src/ui/UIFactory/index.tsx +++ b/packages/ui/src/ui/UIFactory/index.tsx @@ -116,6 +116,11 @@ export type ExtraTab = { position: {before: TabName} | {after: TabName}; }; +export type CustomQueryResultTab = { + title: string; + renderContent: (params: {query: QueryItem}) => React.ReactNode; +}; + export interface UIFactory { getClusterAppearance(cluster?: string): undefined | ClusterAppearance; @@ -383,12 +388,7 @@ export interface UIFactory { renderRolesLink(params: {cluster: string; login: string; className?: string}): React.ReactNode; - getCustomQueryResultTab(): - | undefined - | { - title: string; - renderContent: (params: {query: QueryItem}) => React.ReactNode; - }; + getQueryResultChartTab(): CustomQueryResultTab | undefined; getExternalSettings(params: { cluster: string; diff --git a/packages/ui/src/ui/pages/query-tracker/Plan/utils.ts b/packages/ui/src/ui/pages/query-tracker/Plan/utils.ts index cf0eae39e..708a87648 100644 --- a/packages/ui/src/ui/pages/query-tracker/Plan/utils.ts +++ b/packages/ui/src/ui/pages/query-tracker/Plan/utils.ts @@ -20,8 +20,6 @@ import type {DataSet, Edge, Network, Node} from 'vis-network'; import type {GraphColors} from './GraphColors'; import {DateTime} from 'luxon'; -import {parseTablePath} from './services/tables'; -import {genNavigationUrl} from '../../../utils/navigation/navigation'; export type ParsableDate = string | number | Date | DateTime | {} | undefined | null; @@ -586,59 +584,6 @@ export function getConnectedEdges( return connectedEdges; } -export function usePrepareNode(operationIdToCluster: Map) { - return React.useCallback((node: ProcessedNode) => { - if (node.type === 'in' || node.type === 'out') { - const table = parseTablePath(node.title ?? ''); - if (table) { - node.url = genNavigationUrl({cluster: table.cluster, path: table.path}); - } - } else if (node.progress?.remoteId) { - const id = node.progress?.remoteId.split('/').pop(); - - if (!id) { - node.url = getOperationUrl(node); - - return node; - } - - const cluster = operationIdToCluster.has(id) - ? operationIdToCluster.get(id) - : node.progress?.remoteData?.cluster_name; - - if (cluster) { - node.url = buildOperationUrl(cluster, id); - } - } - - return node; - }, []); -} - -function getOperationUrl(node: ProcessedNode) { - const remoteId = node.progress?.remoteId; - if (!remoteId) { - return undefined; - } - const idParts = remoteId.split('/'); - const cluster = idParts[0]; - const clusterName = cluster.split('.')[0]; - const url = buildOperationUrl(clusterName, idParts[1], idParts[2]); - return url ? url : undefined; -} - -function buildOperationUrl(cluster: string, operation: string, tag?: string) { - let uri = ''; - - if (tag === undefined) { - uri = `/operations/${encodeURIComponent(operation)}`; - } else if (tag === 'filter') { - uri = `/operations?type=all&state=all&filter=${encodeURIComponent(operation)}`; - } - - return `/${cluster.split('.')[0]}${uri}`; -} - export function drawRunningIcon(progress: NodeProgress | undefined, {operation}: GraphColors) { const div = document.createElement('div'); const colors: string[] = []; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResults/PlanContainer.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResults/PlanContainer.tsx new file mode 100644 index 000000000..77cb5c17b --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResults/PlanContainer.tsx @@ -0,0 +1,48 @@ +import React, {FC, useCallback} from 'react'; +import Plan from '../Plan/Plan'; +import {ProcessedNode} from '../Plan/utils'; +import {parseTablePath} from '../Plan/services/tables'; +import {genNavigationUrl} from '../../../utils/navigation/navigation'; +import {buildOperationUrl} from './helpers/buildOperationUrl'; +import {getOperationUrl} from './helpers/getOperationUrl'; + +type Props = { + isActive: boolean; + operationIdToCluster: Map; +}; + +export const PlanContainer: FC = ({isActive, operationIdToCluster}) => { + const handlePrepareNode = useCallback( + (node: ProcessedNode) => { + if (node.type === 'in' || node.type === 'out') { + const table = parseTablePath(node.title ?? ''); + if (table) { + node.url = genNavigationUrl({cluster: table.cluster, path: table.path}); + } + return node; + } + + if (node.progress?.remoteId) { + const id = node.progress?.remoteId.split('/').pop(); + + if (!id) { + node.url = getOperationUrl(node); + return node; + } + + const cluster = operationIdToCluster.has(id) + ? operationIdToCluster.get(id) + : node.progress?.remoteData?.cluster_name; + + if (cluster) { + node.url = buildOperationUrl(cluster, id); + } + } + + return node; + }, + [operationIdToCluster], + ); + + return ; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResults/QueryChartTab.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResults/QueryChartTab.tsx new file mode 100644 index 000000000..6663ce8f4 --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResults/QueryChartTab.tsx @@ -0,0 +1,11 @@ +import {FC} from 'react'; +import UIFactory from '../../../UIFactory'; +import {QueryItem} from '../module/api'; + +type Props = { + query: QueryItem; +}; + +export const QueryChartTab: FC = (props) => { + return UIFactory.getQueryResultChartTab()?.renderContent(props) || null; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResults/QueryResultContainer.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResults/QueryResultContainer.tsx new file mode 100644 index 000000000..cdd74d6fe --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResults/QueryResultContainer.tsx @@ -0,0 +1,22 @@ +import React, {FC, useEffect} from 'react'; +import {QueryItem} from '../module/api'; +import {useDispatch} from 'react-redux'; +import {loadQueryResult} from '../module/query_result/actions'; +import {QueryResultsView} from '../QueryResultsView'; + +type Props = { + query: QueryItem; + activeResultParams?: {queryId: string; resultIndex: number}; +}; + +export const QueryResultContainer: FC = ({query, activeResultParams}) => { + const dispatch = useDispatch(); + + useEffect(() => { + if (activeResultParams) { + dispatch(loadQueryResult(activeResultParams.queryId, activeResultParams.resultIndex)); + } + }, [activeResultParams, dispatch]); + + return ; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/buildOperationUrl.ts b/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/buildOperationUrl.ts new file mode 100644 index 000000000..8ea3eaced --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/buildOperationUrl.ts @@ -0,0 +1,11 @@ +export const buildOperationUrl = (cluster: string, operation: string, tag?: string) => { + let uri = ''; + + if (tag === undefined) { + uri = `/operations/${encodeURIComponent(operation)}`; + } else if (tag === 'filter') { + uri = `/operations?type=all&state=all&filter=${encodeURIComponent(operation)}`; + } + + return `/${cluster.split('.')[0]}${uri}`; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/extractOperationIdToCluster.ts b/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/extractOperationIdToCluster.ts new file mode 100644 index 000000000..c7995334a --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/extractOperationIdToCluster.ts @@ -0,0 +1,23 @@ +import {YQLSstatistics} from '../../module/api'; + +export const extractOperationIdToCluster = ( + statistics: YQLSstatistics | undefined, +): Map => { + const clusterNames: Map = new Map(); + + if (!statistics) return clusterNames; + + const traverse = (obj: YQLSstatistics) => { + for (const key in obj) { + if (key === '_cluster_name') { + clusterNames.set(obj._id, obj._cluster_name); + } else if (typeof obj[key] === 'object' && obj[key] !== null) { + traverse(obj[key]); + } + } + }; + + traverse(statistics); + + return clusterNames; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/getOperationUrl.ts b/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/getOperationUrl.ts new file mode 100644 index 000000000..9567b5a2f --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResults/helpers/getOperationUrl.ts @@ -0,0 +1,14 @@ +import {ProcessedNode} from '../../Plan/utils'; +import {buildOperationUrl} from './buildOperationUrl'; + +export const getOperationUrl = (node: ProcessedNode) => { + const remoteId = node.progress?.remoteId; + if (!remoteId) { + return undefined; + } + const idParts = remoteId.split('/'); + const cluster = idParts[0]; + const clusterName = cluster.split('.')[0]; + const url = buildOperationUrl(clusterName, idParts[1], idParts[2]); + return url ? url : undefined; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResults/hooks/useQueryResultTabs.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResults/hooks/useQueryResultTabs.tsx index 40eeb5482..e2a824f38 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResults/hooks/useQueryResultTabs.tsx +++ b/packages/ui/src/ui/pages/query-tracker/QueryResults/hooks/useQueryResultTabs.tsx @@ -17,7 +17,7 @@ export enum QueryResultTab { RESULT = 'result', STATISTIC = 'statistic', PROGRESS = 'progress', - CUSTOM_TAB = 'custom-tab', + CHART_TAB = 'chart-tab', } const isResultTab = (tabId: string) => tabId.startsWith('result/'); @@ -90,12 +90,12 @@ export const useQueryResultTabs = ( if (query.state === QueryStatus.FAILED) { items.unshift({id: QueryResultTab.ERROR, title: 'Error'}); } else if (query.state === QueryStatus.COMPLETED) { - const customQueryResultTab = UIFactory.getCustomQueryResultTab(); + const queryResultChartTab = UIFactory.getQueryResultChartTab(); - if (customQueryResultTab && query.result_count) { + if (queryResultChartTab && query.result_count) { items.unshift({ - id: QueryResultTab.CUSTOM_TAB, - title: customQueryResultTab.title, + id: QueryResultTab.CHART_TAB, + title: queryResultChartTab.title, }); } diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResults/index.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResults/index.tsx index 5bc6a9c70..f29cbb13b 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResults/index.tsx +++ b/packages/ui/src/ui/pages/query-tracker/QueryResults/index.tsx @@ -1,84 +1,37 @@ -import React, {useEffect} from 'react'; +import React, {ReactNode} from 'react'; import block from 'bem-cn-lite'; -import {QueryItem, YQLSstatistics} from '../module/api'; +import {QueryItem} from '../module/api'; import {Tabs} from '@gravity-ui/uikit'; -import {useDispatch} from 'react-redux'; -import {QueryResultsView} from '../QueryResultsView'; import {QueryMetaInfo} from './QueryMetaRow'; import QueryMetaTable from '../QueryMetaTable'; -import {loadQueryResult} from '../module/query_result/actions'; import {QueryResultActions} from './QueryResultActions'; import {QueryResultTab, useQueryResultTabs} from './hooks/useQueryResultTabs'; import {YQLStatisticsTable} from '../QueryResultsView/YQLStatistics'; import NotRenderUntilFirstVisible from '../NotRenderUntilFirstVisible/NotRenderUntilFirstVisible'; import {PlanProvider} from '../Plan/PlanContext'; -import Plan from '../Plan/Plan'; -import {usePrepareNode} from '../Plan/utils'; import PlanActions from '../Plan/PlanActions'; +import {QueryResultContainer} from './QueryResultContainer'; +import {QueryChartTab} from './QueryChartTab'; +import {PlanContainer} from './PlanContainer'; +import {extractOperationIdToCluster} from './helpers/extractOperationIdToCluster'; import './index.scss'; import {ErrorTree} from './ErrorTree'; import {QueryProgress} from './QueryResultActions/QueryProgress'; -import UIFactory from '../../../UIFactory'; - const b = block('query-results'); -function QueryResultContainer({ - query, - activeResultParams, -}: { +type Props = { query: QueryItem; - activeResultParams?: {queryId: string; resultIndex: number}; -}) { - const dispatch = useDispatch(); - useEffect(() => { - if (activeResultParams) { - dispatch(loadQueryResult(activeResultParams.queryId, activeResultParams.resultIndex)); - } - }, [activeResultParams, dispatch]); - return ; -} - -function CustomQueryTabContainer({query}: {query: QueryItem}) { - const customQueryResultTab = UIFactory.getCustomQueryResultTab(); - - if (!customQueryResultTab) { - return null; - } - - return customQueryResultTab.renderContent({query}); -} - -function extractOperationIdToCluster(obj: YQLSstatistics | undefined): Map { - const clusterNames: Map = new Map(); - - if (!obj) return clusterNames; - - const traverse = (o: YQLSstatistics) => { - for (const key in o) { - if (key === '_cluster_name') { - clusterNames.set(o._id, o._cluster_name); - } else if (typeof o[key] === 'object' && o[key] !== null) { - traverse(o[key]); - } - } - }; - - traverse(obj); - - return clusterNames; -} + className: string; + toolbar: ReactNode; + minimized: boolean; +}; -export const QueryResults = React.memo(function QueryResults({ +export const QueryResults = React.memo(function QueryResults({ query, className, toolbar, minimized = false, -}: { - query: QueryItem; - className: string; - toolbar: React.ReactChild; - minimized: boolean; }) { const [tabs, setTab, {activeTabId, category, activeResultParams}] = useQueryResultTabs(query); const operationIdToCluster = React.useMemo( @@ -130,10 +83,10 @@ export const QueryResults = React.memo(function QueryResults({ /> - + {category === QueryResultTab.ERROR && } {category === QueryResultTab.META && } @@ -154,12 +107,3 @@ export const QueryResults = React.memo(function QueryResults({ ); }); - -interface PlanContainerProps { - isActive: boolean; - operationIdToCluster: Map; -} - -function PlanContainer({isActive, operationIdToCluster}: PlanContainerProps) { - return ; -} diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/QueryResultsVisualization/QueryResultsVisualization.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/QueryResultsVisualization/QueryResultsVisualization.tsx index dae898695..7d3654f14 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/QueryResultsVisualization/QueryResultsVisualization.tsx +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/QueryResultsVisualization/QueryResultsVisualization.tsx @@ -28,7 +28,6 @@ const b = block('query-result-visualization'); type QueryResultsVisualizationProps = { query: QueryItem; - index: number; }; export function QueryResultsVisualization({query}: QueryResultsVisualizationProps) { diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/index.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/index.tsx index d96fcf8cd..4c6051ddc 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/index.tsx +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import withLazyLoading from '../../../hocs/withLazyLoading'; +import type {QueryItem} from '../module/api'; const QueryResultsVisualizationLazy = withLazyLoading( React.lazy(async () => { @@ -13,7 +14,7 @@ const QueryResultsVisualizationLazy = withLazyLoading( }), ); -export const CUSTOM_QUERY_REQULT_TAB = { +export const QUERY_RESULT_CHART_TAB = { title: 'Chart', - renderContent: (props: any) => , + renderContent: (params: {query: QueryItem}) => , }; From 2d1932ddedbca2ef2379f7b69962195b637f8708 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Fri, 25 Oct 2024 16:42:24 +0300 Subject: [PATCH 2/3] chore(Queries): move query chart store [YTFRONT-4423] --- .../store => module/queryChart}/actions.ts | 0 .../store/reducer.tsx => module/queryChart/queryChartSlice.ts} | 0 .../store => module/queryChart}/selectors.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename packages/ui/src/ui/pages/query-tracker/{QueryResultsVisualization/store => module/queryChart}/actions.ts (100%) rename packages/ui/src/ui/pages/query-tracker/{QueryResultsVisualization/store/reducer.tsx => module/queryChart/queryChartSlice.ts} (100%) rename packages/ui/src/ui/pages/query-tracker/{QueryResultsVisualization/store => module/queryChart}/selectors.ts (100%) diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/store/actions.ts b/packages/ui/src/ui/pages/query-tracker/module/queryChart/actions.ts similarity index 100% rename from packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/store/actions.ts rename to packages/ui/src/ui/pages/query-tracker/module/queryChart/actions.ts diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/store/reducer.tsx b/packages/ui/src/ui/pages/query-tracker/module/queryChart/queryChartSlice.ts similarity index 100% rename from packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/store/reducer.tsx rename to packages/ui/src/ui/pages/query-tracker/module/queryChart/queryChartSlice.ts diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/store/selectors.ts b/packages/ui/src/ui/pages/query-tracker/module/queryChart/selectors.ts similarity index 100% rename from packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/store/selectors.ts rename to packages/ui/src/ui/pages/query-tracker/module/queryChart/selectors.ts From 32e1f94e7324bdce71a02139c0333922713a9ea8 Mon Sep 17 00:00:00 2001 From: simbiozizv Date: Tue, 22 Oct 2024 14:01:03 +0300 Subject: [PATCH 3/3] chore(Queries): query result chart refactoring [YTFRONT-4423] --- packages/ui/src/ui/UIFactory/index.tsx | 4 +- .../components/Chart.tsx | 38 +++ .../ChartErrorBoundary.scss | 9 - .../ChartErrorBoundary/ChartErrorBoundary.tsx | 87 ------ .../components/ChartFields/ChartField.scss | 15 + .../components/ChartFields/ChartField.tsx | 76 +++++ .../ChartFields/ChartFieldName.scss | 35 +++ .../components/ChartFields/ChartFieldName.tsx | 35 +++ .../components/ChartFields/ChartFields.scss | 3 + .../components/ChartFields/ChartFields.tsx | 28 ++ .../components/ChartFields/index.ts | 1 + .../components/ChartSettings.tsx | 122 ++++++++ .../ChartValidation.scss | 0 .../ChartValidation.tsx | 24 +- .../EmptyPlaceholdersMessage.scss | 0 .../EmptyPlaceholdersMessage.tsx | 0 .../QueryResultsVisualization.scss | 0 .../components/QueryResultsVisualization.tsx | 78 ++++++ .../components/SavingIndicator.scss | 14 + .../SavingIndicator.tsx | 8 +- .../components/VisualizationSelector.tsx | 35 +++ .../containers/Chart/Chart.tsx | 44 --- .../ChartSettings/ChartSettings.tsx | 163 ----------- .../Placeholder/Placeholder.scss | 49 ---- .../Placeholder/Placeholder.tsx | 96 ------- .../PlaceholdersContainer.scss | 7 - .../PlaceholdersContainer.tsx | 27 -- .../VisualizationSelector.tsx | 38 --- .../QueryResultsVisualization.tsx | 116 -------- .../SavingIndicator/SavingIndicator.scss | 9 - .../VisualizationSelect.tsx | 41 --- .../VisualizationSelector.tsx | 44 --- .../helpers/placeholdersToMap.ts | 5 + .../QueryResultsVisualization/index.tsx | 2 +- .../preparers/bar.ts | 52 ++-- .../preparers/getPointData.ts | 4 +- .../preparers/scatter.ts | 24 +- .../preparers/types.ts | 9 +- .../QueryResultsVisualization/types.ts | 14 +- .../validation/colorsValidation.ts | 43 +++ .../validation/lineAndBarValidation.ts | 33 +++ .../validation/scatterValidation.ts | 42 +++ .../src/ui/pages/query-tracker/module/api.ts | 5 + .../ui/pages/query-tracker/module/index.ts | 2 + .../query-tracker/module/query/actions.ts | 2 + .../query-tracker/module/query/selectors.ts | 4 +- .../module/queryChart/actions.ts | 147 +++------- .../module/queryChart/queryChartSlice.ts | 265 ++++++------------ .../module/queryChart/selectors.ts | 193 ++----------- .../module/query_result/actions.ts | 6 +- .../module/query_result/reducer.ts | 2 +- .../module/query_result/selectors.ts | 2 +- .../module/query_result/types.ts | 10 +- .../ui/src/ui/store/reducers/index.main.ts | 2 - 54 files changed, 833 insertions(+), 1281 deletions(-) create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/Chart.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartErrorBoundary/ChartErrorBoundary.scss delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartErrorBoundary/ChartErrorBoundary.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartField.scss create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartField.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartFieldName.scss create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartFieldName.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartFields.scss create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartFields.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/index.ts create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartSettings.tsx rename packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/{containers/ChartValidation => components}/ChartValidation.scss (100%) rename packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/{containers/ChartValidation => components}/ChartValidation.tsx (74%) rename packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/{EmptyPlaceholdersMessage => }/EmptyPlaceholdersMessage.scss (100%) rename packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/{EmptyPlaceholdersMessage => }/EmptyPlaceholdersMessage.tsx (100%) rename packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/{containers/QueryResultsVisualization => components}/QueryResultsVisualization.scss (100%) create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/QueryResultsVisualization.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/SavingIndicator.scss rename packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/{containers/SavingIndicator => components}/SavingIndicator.tsx (68%) create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/VisualizationSelector.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/Chart/Chart.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/ChartSettings/ChartSettings.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/Placeholder/Placeholder.scss delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/Placeholder/Placeholder.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/PlaceholdersContainer.scss delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/PlaceholdersContainer.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/VisualizationSelector/VisualizationSelector.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/QueryResultsVisualization/QueryResultsVisualization.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/SavingIndicator/SavingIndicator.scss delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/VisualizationSelect/VisualizationSelect.tsx delete mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/VisualizationSelector/VisualizationSelector.tsx create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/helpers/placeholdersToMap.ts create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/colorsValidation.ts create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/lineAndBarValidation.ts create mode 100644 packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/scatterValidation.ts diff --git a/packages/ui/src/ui/UIFactory/index.tsx b/packages/ui/src/ui/UIFactory/index.tsx index 3ddfdebb1..a759f1f6f 100644 --- a/packages/ui/src/ui/UIFactory/index.tsx +++ b/packages/ui/src/ui/UIFactory/index.tsx @@ -116,7 +116,7 @@ export type ExtraTab = { position: {before: TabName} | {after: TabName}; }; -export type CustomQueryResultTab = { +export type QueryResultChartTab = { title: string; renderContent: (params: {query: QueryItem}) => React.ReactNode; }; @@ -388,7 +388,7 @@ export interface UIFactory { renderRolesLink(params: {cluster: string; login: string; className?: string}): React.ReactNode; - getQueryResultChartTab(): CustomQueryResultTab | undefined; + getQueryResultChartTab(): QueryResultChartTab | undefined; getExternalSettings(params: { cluster: string; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/Chart.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/Chart.tsx new file mode 100644 index 000000000..cd976052f --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/Chart.tsx @@ -0,0 +1,38 @@ +import React, {useMemo} from 'react'; +import ChartKit from '../../../../components/YagrChartKit/YagrChartKit'; +import {settings} from '@gravity-ui/chartkit'; +import type {ChartKitRef} from '@gravity-ui/chartkit'; +import {prepareWidgetData} from '../preparers/prepareWidgetData'; +import {useSelector} from 'react-redux'; +import { + selectIsPlaceholdersFieldsFilled, + selectQueryResultVisualization, +} from '../../module/queryChart/selectors'; +import type {QueryResult} from '../preparers/types'; +import {EmptyPlaceholdersMessage} from './EmptyPlaceholdersMessage'; +import {D3Plugin} from '@gravity-ui/chartkit/d3'; + +settings.set({plugins: [...settings.get('plugins'), D3Plugin]}); + +type LineBasicProps = { + result: QueryResult; +}; + +export const BaseChart = React.forwardRef( + function BaseChartComponent({result}, ref) { + const visualization = useSelector(selectQueryResultVisualization); + const fieldsIsFilled = useSelector(selectIsPlaceholdersFieldsFilled); + + const widgetData = useMemo(() => { + return prepareWidgetData({result, visualization}); + }, [result, visualization]); + + if (!fieldsIsFilled) { + return ; + } + + return ; + }, +); + +export const Chart = React.memo(BaseChart); diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartErrorBoundary/ChartErrorBoundary.scss b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartErrorBoundary/ChartErrorBoundary.scss deleted file mode 100644 index ab21111bb..000000000 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartErrorBoundary/ChartErrorBoundary.scss +++ /dev/null @@ -1,9 +0,0 @@ -.chart-error-boundary { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - - text-align: center; -} diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartErrorBoundary/ChartErrorBoundary.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartErrorBoundary/ChartErrorBoundary.tsx deleted file mode 100644 index b6a5fa965..000000000 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartErrorBoundary/ChartErrorBoundary.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import * as React from 'react'; -import {useEffect, useState} from 'react'; -import {ChartKitError} from '@gravity-ui/chartkit'; -import {Button} from '@gravity-ui/uikit'; - -import './ChartErrorBoundary.scss'; - -const renderError = ({ - error, - refreshClick, -}: { - error: ChartKitError | Error; - refreshClick(): void; -}) => { - return ( -
-
-

- {error?.message} -
-

- -
-
- ); -}; - -interface ChartErrorBoundaryProps { - deps: Record; - children: (args: {handleError({error}: {error: ChartKitError}): void}) => React.ReactElement; -} - -export const ChartErrorHandler = ({children, deps}: ChartErrorBoundaryProps) => { - const [error, setError] = useState(); - - const handleError = ({error}: {error: ChartKitError}) => { - setError(error); - }; - - const handleRefreshClick = () => { - setError(undefined); - }; - - useEffect(() => { - setError(undefined); - }, [deps]); - - if (error) { - return renderError({error, refreshClick: handleRefreshClick}); - } - - return children({handleError}); -}; - -export class ChartErrorBoundary extends React.Component< - React.PropsWithChildren>, - {error?: Error | ChartKitError} -> { - static getDerivedStateFromError(error: Error) { - return {error}; - } - - constructor(props: React.PropsWithChildren>) { - super(props); - - this.state = {error: undefined}; - } - - componentDidCatch() {} - - handleRefreshClick = () => { - this.setState({error: undefined}); - }; - - render() { - if (this.state.error) { - return renderError({ - error: this.state.error, - refreshClick: this.handleRefreshClick, - }); - } - - return this.props.children; - } -} diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartField.scss b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartField.scss new file mode 100644 index 000000000..baa19a355 --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartField.scss @@ -0,0 +1,15 @@ +.yt-chart-field { + width: 200px; + padding: 12px 0; + border-top: 1px solid var(--g-color-line-generic); + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__name { + font-weight: bold; + } +} diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartField.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartField.tsx new file mode 100644 index 000000000..c50fb2dde --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/components/ChartFields/ChartField.tsx @@ -0,0 +1,76 @@ +import React, {useCallback} from 'react'; +import block from 'bem-cn-lite'; +import './ChartField.scss'; +import {Placeholder} from '../../types'; +import {Button, Icon, Select} from '@gravity-ui/uikit'; +import {Plus as PlusIcon} from '@gravity-ui/icons'; +import {useDispatch} from 'react-redux'; +import {removeField, setField} from '../../../module/queryChart/queryChartSlice'; +import {ChartFieldName} from './ChartFieldName'; + +const b = block('yt-chart-field'); + +type PlaceholderComponentProps = { + placeholder: Placeholder; + availableFields: string[]; +}; + +export const ChartField = ({placeholder, availableFields}: PlaceholderComponentProps) => { + const {id, field} = placeholder; + const dispatch = useDispatch(); + + const handleRemoveField = useCallback( + (fieldName: string, placeholderId: string) => { + dispatch( + removeField({ + fieldName, + placeholderId, + }), + ); + }, + [dispatch], + ); + + const onSelectUpdate = React.useCallback( + (value: string[]) => { + dispatch( + setField({ + placeholderId: placeholder.id, + fieldName: value[0], + }), + ); + }, + [dispatch, placeholder.id], + ); + + return ( +
+
+
{id}
+ ; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/Chart/Chart.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/Chart/Chart.tsx deleted file mode 100644 index 02f3ad178..000000000 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/Chart/Chart.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, {useMemo} from 'react'; -import ChartKit from '../../../../../components/YagrChartKit/YagrChartKit'; -import {settings} from '@gravity-ui/chartkit'; -import type {ChartKitRef} from '@gravity-ui/chartkit'; -import {prepareWidgetData} from '../../preparers/prepareWidgetData'; -import {useSelector} from 'react-redux'; -import { - selectIsPlaceholdersMissSomeFields, - selectQueryResultVisualization, -} from '../../store/selectors'; -import type {QueryResult} from '../../preparers/types'; -import {ChartErrorHandler} from '../../components/ChartErrorBoundary/ChartErrorBoundary'; -import {EmptyPlaceholdersMessage} from '../../components/EmptyPlaceholdersMessage/EmptyPlaceholdersMessage'; -import {D3Plugin} from '@gravity-ui/chartkit/d3'; - -settings.set({plugins: [...settings.get('plugins'), D3Plugin]}); - -type LineBasicProps = { - result: QueryResult; -}; - -export const BaseChart = React.forwardRef( - function BaseChartComponent({result}, ref) { - const visualization = useSelector(selectQueryResultVisualization); - const isPlaceholdersMissSomeFields = useSelector(selectIsPlaceholdersMissSomeFields); - const widgetData = useMemo(() => { - return prepareWidgetData({result, visualization}); - }, [result, visualization]); - - if (isPlaceholdersMissSomeFields) { - return ; - } - - return ( - - {({handleError}) => ( - - )} - - ); - }, -); - -export const Chart = React.memo(BaseChart); diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/ChartSettings/ChartSettings.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/ChartSettings/ChartSettings.tsx deleted file mode 100644 index 1e405d79f..000000000 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/ChartSettings/ChartSettings.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, {useCallback, useState} from 'react'; -import {Gear} from '@gravity-ui/icons'; -import {Icon} from '@gravity-ui/uikit'; -import Button from '../../../../../components/Button/Button'; -import {DialogField, DialogTabField, FormApi, YTDFDialog} from '../../../../../components/Dialog'; -import type {ChartSettings} from '../../types'; -import {useSelector} from 'react-redux'; -import {selectQueryResultChartSettings} from '../../store/selectors'; -import {useThunkDispatch} from '../../../../../store/thunkDispatch'; - -type FormValues = ChartSettings; - -function pixelIntervalValidator(value: string) { - return !value || /^\d+$/.test(value) ? undefined : 'Should be a number'; -} - -const xAxisTab: DialogTabField> = { - type: 'tab', - name: 'xAxis', - title: 'X axis', - fields: [ - { - name: 'legend', - caption: 'Legend', - type: 'radio', - extras: { - options: [ - { - value: 'on', - label: 'On', - }, - { - value: 'off', - label: 'Off', - }, - ], - }, - }, - { - name: 'labels', - caption: 'Labels', - type: 'radio', - extras: { - options: [ - { - value: 'on', - label: 'On', - }, - { - value: 'off', - label: 'Off', - }, - ], - }, - }, - { - caption: 'Title', - name: 'title', - type: 'text', - }, - { - caption: 'Grid step, px', - name: 'pixelInterval', - type: 'text', - validator: pixelIntervalValidator, - }, - ], -}; - -const yAxisTab: DialogTabField> = { - type: 'tab', - name: 'yAxis', - title: 'Y axis', - fields: [ - { - name: 'labels', - caption: 'Labels', - type: 'radio', - extras: { - options: [ - { - value: 'on', - label: 'On', - }, - { - value: 'off', - label: 'Off', - }, - ], - }, - }, - { - caption: 'Title', - name: 'title', - type: 'text', - }, - { - name: 'grid', - caption: 'Grid', - type: 'radio', - extras: { - options: [ - { - value: 'on', - label: 'On', - }, - { - value: 'off', - label: 'Off', - }, - ], - }, - }, - { - caption: 'Grid step, px', - name: 'pixelInterval', - type: 'text', - validator: pixelIntervalValidator, - }, - ], -}; - -const fields: DialogTabField>[] = [xAxisTab, yAxisTab]; - -export function ChartSettingsComponent() { - const [visible, setVisilbility] = useState(false); - const chartSettings = useSelector(selectQueryResultChartSettings); - const dispatch = useThunkDispatch(); - - const handleOnOpen = useCallback(() => { - setVisilbility(true); - }, [setVisilbility]); - - const handleOnClose = useCallback(() => { - setVisilbility(false); - }, [setVisilbility]); - - const handleOnAdd = useCallback((form: FormApi>) => { - const data = form.getState().values; - - dispatch({ - type: 'set-chart-settings', - data, - }); - - return Promise.resolve(); - }, []); - - return ( - - - - visible={visible} - initialValues={chartSettings} - onClose={handleOnClose} - onAdd={handleOnAdd} - fields={fields} - /> - - ); -} diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/Placeholder/Placeholder.scss b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/Placeholder/Placeholder.scss deleted file mode 100644 index ef28f55a2..000000000 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/Placeholder/Placeholder.scss +++ /dev/null @@ -1,49 +0,0 @@ -.placeholder { - width: 200px; - padding: 12px 0; - border-top: 1px solid var(--g-color-line-generic); - - &__header { - display: flex; - justify-content: space-between; - align-items: center; - } - - &__name { - font-weight: bold; - } - - &__field { - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - height: 32px; - margin-top: 4px; - - background-color: var(--g-color-base-misc-light); - - &:hover { - background-color: var(--g-color-base-simple-hover); - } - } - - &__field-spacer { - display: flex; - margin-left: 12px; - } - - &__field-title { - overflow: hidden; - text-overflow: ellipsis; - margin-left: 8px; - } - - &__field:hover &__field-actions { - visibility: visible; - } - - &__field-actions { - visibility: hidden; - } -} diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/Placeholder/Placeholder.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/Placeholder/Placeholder.tsx deleted file mode 100644 index d03020734..000000000 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/PlaceholdersContainer/Placeholder/Placeholder.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import block from 'bem-cn-lite'; -import './Placeholder.scss'; -import {Field, Placeholder} from '../../../types'; -import {Button, Icon, Select} from '@gravity-ui/uikit'; -import {Plus as PlusIcon, Xmark as XmarkIcon} from '@gravity-ui/icons'; -import {useThunkDispatch} from '../../../../../../store/thunkDispatch'; - -const b = block('placeholder'); - -type PlaceholderComponentProps = { - placeholder: Placeholder; - availableFields: Field[]; -}; - -export const PlaceholderComponent = ({placeholder, availableFields}: PlaceholderComponentProps) => { - const {id, fields} = placeholder; - const dispatch = useThunkDispatch(); - - const addField = React.useCallback( - ({field, placeholder}: {field: Field; placeholder: Placeholder}) => { - dispatch({ - type: 'set-fields', - data: { - fields: [field], - placeholderId: placeholder.id, - }, - }); - }, - [dispatch], - ); - - const removeField = React.useCallback( - ({field, placeholder}: {field: Field; placeholder: Placeholder}) => { - dispatch({ - type: 'remove-field', - data: { - field, - placeholderId: placeholder.id, - }, - }); - }, - [dispatch], - ); - - const onSelectUpdate = React.useCallback( - (value: string[]) => { - addField({ - placeholder, - field: availableFields.find((field) => field.name === value[0])!, - }); - }, - [addField], - ); - - return ( -
-
-
{id}
- ; -} diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/VisualizationSelector/VisualizationSelector.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/VisualizationSelector/VisualizationSelector.tsx deleted file mode 100644 index 312a540ae..000000000 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/containers/VisualizationSelector/VisualizationSelector.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, {useCallback, useMemo} from 'react'; -import {Select} from '@gravity-ui/uikit'; -import type {VisualizationId} from '../../types'; -import {useSelector} from 'react-redux'; -import {selectQueryResultVisualizationId} from '../../store/selectors'; -import {useThunkDispatch} from '../../../../../store/thunkDispatch'; - -const options = [ - { - value: 'line', - content: 'Line chart', - }, - { - value: 'bar-x', - content: 'Bar chart', - }, - { - value: 'scatter', - content: 'Scatter chart', - }, -]; - -export function VisualizationSelector() { - const visualizationId = useSelector(selectQueryResultVisualizationId); - const dispatch = useThunkDispatch(); - - const value = useMemo(() => { - return [visualizationId]; - }, [visualizationId]); - - const onUpdate = useCallback( - ([visualization]: string[]) => { - dispatch({ - type: 'set-visualization', - data: visualization as VisualizationId, - }); - }, - [dispatch], - ); - - return ( - width={125} value={value} options={options} onUpdate={onUpdate} /> - ); -} diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/helpers/placeholdersToMap.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/helpers/placeholdersToMap.ts new file mode 100644 index 000000000..e41486fab --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/helpers/placeholdersToMap.ts @@ -0,0 +1,5 @@ +import {Placeholder} from '../types'; + +export const placeholdersToMap = (placeholders: Placeholder[]) => { + return new Map(placeholders.map((placeholder) => [placeholder.id, placeholder.field])); +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/index.tsx b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/index.tsx index 4c6051ddc..3a19f6223 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/index.tsx +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/index.tsx @@ -7,7 +7,7 @@ const QueryResultsVisualizationLazy = withLazyLoading( return { default: ( await import( - /* webpackChunkName: "query-results" */ './containers/QueryResultsVisualization/QueryResultsVisualization' + /* webpackChunkName: "query-results" */ './components/QueryResultsVisualization' ) ).QueryResultsVisualization, }; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/bar.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/bar.ts index 9ad3fa415..ba011f6e3 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/bar.ts +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/bar.ts @@ -2,7 +2,7 @@ import uniq_ from 'lodash/uniq'; import map_ from 'lodash/map'; import type {ChartKitWidgetData} from '@gravity-ui/chartkit'; import type {PrepareLineArgs, QueryResult} from './types'; -import {Field, VisualizationId} from '../types'; +import {VisualizationId} from '../types'; import {splitDataByColor} from './splitDataByColor'; import {getPointValue} from './getPointData'; import {getVisualizationPlaceholders} from './utils'; @@ -12,9 +12,9 @@ import {getVisualizationPlaceholders} from './utils'; interface PrepareColoredSeriesDataArgs { rows: QueryResult; - xField: Field; - yField: Field; - colorField: Field; + xField: string; + yField: string; + colorField: string; visualizationId: VisualizationId; } @@ -25,15 +25,15 @@ function prepareColoredSeriesData({ colorField, visualizationId, }: PrepareColoredSeriesDataArgs): ChartKitWidgetData { - const xFieldPath = `${xField.name}.$rawValue`; + const xFieldPath = `${xField}.$rawValue`; return { series: { data: splitDataByColor({ rows, - xFieldName: xField.name, - yFieldName: yField.name, - colorFieldName: colorField?.name, + xFieldName: xField, + yFieldName: yField, + colorFieldName: colorField, }).map((item) => { return { type: visualizationId, @@ -52,16 +52,14 @@ function prepareColoredSeriesData({ interface PrepareSeriesDataArgs { rows: QueryResult; - xField: Field; - yField: Field; + xField: string; + yField: string; visualizationId: VisualizationId; - yFields: Field[]; } function prepareSeriesData({ xField, yField, - yFields, rows, visualizationId, }: PrepareSeriesDataArgs): ChartKitWidgetData { @@ -72,14 +70,12 @@ function prepareSeriesData({ const dataMatrix: Record = {}; rows.forEach((row) => { - const xRowItem = row[xField.name]; + const xRowItem = row[xField]; const xValue = xRowItem.$rawValue; xValues.push(xValue); - yFields.forEach((field) => { - dataMatrix[xValue] = getPointValue(row[field.name]); - }); + dataMatrix[xValue] = getPointValue(row[yField]); }); xValues = Array.from(new Set(xValues)); @@ -88,15 +84,13 @@ function prepareSeriesData({ result.graphs = []; - yFields.forEach(() => { - const graph = { - data: xValues.map((xValue) => { - return dataMatrix[String(xValue)]; - }), - }; + const graph = { + data: xValues.map((xValue) => { + return dataMatrix[String(xValue)]; + }), + }; - result.graphs?.push(graph); - }); + result.graphs?.push(graph); return { series: { @@ -109,7 +103,7 @@ function prepareSeriesData({ y: Number(item), }; }), - name: yField.name, + name: yField, }, ], }, @@ -127,10 +121,9 @@ export function prepareBar(args: PrepareLineArgs): ChartKitWidgetData { const {xPlaceholder, yPlaceholder, colorPlaceholder} = getVisualizationPlaceholders(visualization); - const [xField] = xPlaceholder?.fields || []; - const yFields = yPlaceholder?.fields || []; - const [yField] = yFields; - const [colorField] = colorPlaceholder?.fields || []; + const colorField = colorPlaceholder?.field; + const xField = xPlaceholder?.field; + const yField = yPlaceholder?.field; if (!xField || !yField) { return { @@ -154,7 +147,6 @@ export function prepareBar(args: PrepareLineArgs): ChartKitWidgetData { rows, yField, xField, - yFields, visualizationId, }); } diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/getPointData.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/getPointData.ts index c7dabc8b3..f39948ff7 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/getPointData.ts +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/getPointData.ts @@ -1,6 +1,6 @@ -import {FieldObject} from './types'; +import {Result} from '../../module/query_result/types'; -export const getPointValue = (value: FieldObject) => { +export const getPointValue = (value: Result) => { switch (value.$type) { case 'yql.interval': { return Number(value.$value); diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/scatter.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/scatter.ts index cfcee37d9..06b04dc07 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/scatter.ts +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/scatter.ts @@ -8,10 +8,10 @@ export function prepareScatter(args: PrepareLineArgs): ChartKitWidgetData { const rows = args.result; const {xPlaceholder, yPlaceholder, colorPlaceholder} = getVisualizationPlaceholders(visualization); - const [xField] = xPlaceholder?.fields || []; - const yFields = yPlaceholder?.fields || []; - const [yField] = yFields; - const [colorField] = colorPlaceholder?.fields || []; + const [colorField] = [colorPlaceholder?.field] || []; + + const xField = xPlaceholder?.field; + const yField = yPlaceholder?.field; if (!xField || !yField) { return { @@ -26,9 +26,9 @@ export function prepareScatter(args: PrepareLineArgs): ChartKitWidgetData { series: { data: splitDataByColor({ rows, - yFieldName: yField.name, - xFieldName: xField.name, - colorFieldName: colorField?.name, + yFieldName: yField, + xFieldName: xField, + colorFieldName: colorField, }).map((item) => ({ data: item.data.map(({x, y}) => ({ x: Number(x), @@ -41,22 +41,20 @@ export function prepareScatter(args: PrepareLineArgs): ChartKitWidgetData { }; } - const widgetData: ChartKitWidgetData = { + return { series: { data: [ { type: 'scatter', data: rows.map((row) => { return { - x: Number(row[xField.name].$rawValue), - y: Number(row[yField.name].$rawValue), + x: Number(row[xField].$rawValue), + y: Number(row[yField].$rawValue), }; }), - name: `${xField.name} x ${yField.name}`, + name: `${xField} x ${yField}`, }, ], }, }; - - return widgetData; } diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/types.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/types.ts index a3ffb710a..4186abbf9 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/types.ts +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/preparers/types.ts @@ -1,14 +1,9 @@ import type {Visualization} from '../types'; +import {Result} from '../../module/query_result/types'; export type PrepareLineArgs = { result: QueryResult; visualization: Visualization; }; -export type FieldObject = { - $type: string; - $value: unknown; - $rawValue: string; -}; - -export type QueryResult = Record[]; +export type QueryResult = Record[]; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/types.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/types.ts index 59a078ab5..c62c9d151 100644 --- a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/types.ts +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/types.ts @@ -1,12 +1,8 @@ -export type Field = { - name: string; -}; - export type PlaceholderId = 'x' | 'y' | 'colors'; export type Placeholder = { id: PlaceholderId; - fields: Field[]; + field: string; }; type RadioSetting = 'on' | 'off'; @@ -36,11 +32,3 @@ export type Visualization = { placeholders: Placeholder[]; chartSettings: ChartSettings; }; - -export type ExtendedQueryItem = { - id: string; - result_count: number; - annotations?: { - ui_chart_config: Visualization[]; - }; -}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/colorsValidation.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/colorsValidation.ts new file mode 100644 index 000000000..aa4f7165f --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/colorsValidation.ts @@ -0,0 +1,43 @@ +import type {QueryResult} from '../preparers/types'; +import type {Placeholder} from '../types'; +import objectHash from 'object-hash'; + +export const colorsValidation = (queryResult: QueryResult, placeholders: Placeholder[]) => { + const fields = placeholders.reduce((acc: string[], {field}) => { + if (field) { + acc.push(field); + } + + return acc; + }, []); + + const notAllFieldsSelected = fields.length < placeholders.length; + + if (notAllFieldsSelected) { + return { + x: {invalid: false}, + }; + } + + const hashes: Record = {}; + + const isDataDuplicated = queryResult.some((item) => { + const newObject = fields.reduce((acc: Record, field: string) => { + acc[field] = item[field].$rawValue; + return acc; + }, {}); + + // eslint-disable-next-line new-cap + const lineHash = objectHash.MD5(newObject); + + const isLineDuplicated = hashes[lineHash]; + + hashes[lineHash] = true; + + return isLineDuplicated; + }); + + return { + x: {invalid: isDataDuplicated}, + }; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/lineAndBarValidation.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/lineAndBarValidation.ts new file mode 100644 index 000000000..90a4d4b10 --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/lineAndBarValidation.ts @@ -0,0 +1,33 @@ +import type {QueryResult} from '../preparers/types'; +import type {Placeholder} from '../types'; +import {placeholdersToMap} from '../helpers/placeholdersToMap'; + +export const lineAndBarValidation = (queryResult: QueryResult, placeholders: Placeholder[]) => { + const fields = placeholdersToMap(placeholders); + + const xFieldName = fields.get('x'); + const yFieldName = fields.get('y'); + const notAllFieldsSelected = !xFieldName || !yFieldName; + + if (notAllFieldsSelected) { + return { + x: {invalid: false}, + }; + } + + const xCoords: Record = {}; + + const isXCoordsDuplicated = queryResult.some((item) => { + const xValue = item[xFieldName].$rawValue; + + const isXValueDuplicated = xCoords[xValue]; + + xCoords[xValue] = true; + + return isXValueDuplicated; + }); + + return { + x: {invalid: isXCoordsDuplicated}, + } as unknown as Record; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/scatterValidation.ts b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/scatterValidation.ts new file mode 100644 index 000000000..5a7e9ec8d --- /dev/null +++ b/packages/ui/src/ui/pages/query-tracker/QueryResultsVisualization/validation/scatterValidation.ts @@ -0,0 +1,42 @@ +import type {QueryResult} from '../preparers/types'; +import type {Placeholder} from '../types'; +import objectHash from 'object-hash'; +import {placeholdersToMap} from '../helpers/placeholdersToMap'; + +export const scatterValidation = (queryResult: QueryResult, placeholders: Placeholder[]) => { + const fields = placeholdersToMap(placeholders); + + const xFieldName = fields.get('x'); + const yFieldName = fields.get('y'); + const notAllFieldsSelected = !xFieldName || !yFieldName; + + if (notAllFieldsSelected) { + return { + x: {invalid: false}, + y: {invalid: false}, + }; + } + + const hashes: Record = {}; + + const isPointsDuplicated = queryResult.some((item) => { + const xValue = item[xFieldName].$rawValue; + const yValue = item[yFieldName].$rawValue; + // eslint-disable-next-line new-cap + const hash = objectHash.MD5({ + xValue, + yValue, + }); + + const isPointDuplicated = hashes[hash]; + + hashes[hash] = true; + + return isPointDuplicated; + }); + + return { + x: {invalid: isPointsDuplicated}, + y: {invalid: isPointsDuplicated}, + }; +}; diff --git a/packages/ui/src/ui/pages/query-tracker/module/api.ts b/packages/ui/src/ui/pages/query-tracker/module/api.ts index 1380ee85d..3d8df27f1 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/api.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/api.ts @@ -18,6 +18,7 @@ import {getLastSelectedACONamespaces, selectIsMultipleAco} from './query_aco/sel import {setSettingByKey} from '../../../store/actions/settings'; import unipika from '../../../common/thor/unipika'; import {CancelTokenSource} from 'axios'; +import {Visualization} from '../QueryResultsVisualization/types'; function getQTApiSetup(): {proxy?: string} { const QT_CLUSTER = getQueryTrackerCluster(); @@ -131,6 +132,10 @@ export interface QueryItem extends DraftQuery { spyt_progress?: number; }; error?: QueryError; + annotations?: { + title?: string; + chartConfig?: Visualization; + }; } export enum QueryStatus { diff --git a/packages/ui/src/ui/pages/query-tracker/module/index.ts b/packages/ui/src/ui/pages/query-tracker/module/index.ts index 96a5a8de5..73fe230ad 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/index.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/index.ts @@ -6,6 +6,7 @@ import {reducer as queryAcoReducer} from './query_aco/reducer'; import {queryFilesFormReducer} from './queryFilesForm/queryFilesFormSlice'; import {vcsReducer} from './vcs/vcsSlice'; import {queryNavigationReducer} from './queryNavigation/queryNavigationSlice'; +import {queryChartReducer} from './queryChart/queryChartSlice'; export const queryTracker = combineReducers({ list: listReducer, @@ -15,4 +16,5 @@ export const queryTracker = combineReducers({ queryFilesModal: queryFilesFormReducer, vcs: vcsReducer, queryNavigation: queryNavigationReducer, + queryChart: queryChartReducer, }); diff --git a/packages/ui/src/ui/pages/query-tracker/module/query/actions.ts b/packages/ui/src/ui/pages/query-tracker/module/query/actions.ts index 66b6f239e..aa7144391 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/query/actions.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/query/actions.ts @@ -49,6 +49,7 @@ import { UPDATE_ACO_QUERY, UPDATE_QUERY, } from '../query-tracker-contants'; +import {loadVisualization} from '../queryChart/actions'; export const setCurrentClusterToQuery = (): ThunkAction => (dispatch, getState) => { @@ -140,6 +141,7 @@ export function loadQuery( initialQuery: queryItem, }, }); + dispatch(loadVisualization()); } catch (e: unknown) { dispatch(createEmptyQuery()); } finally { diff --git a/packages/ui/src/ui/pages/query-tracker/module/query/selectors.ts b/packages/ui/src/ui/pages/query-tracker/module/query/selectors.ts index d53cc0960..e23c25811 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/query/selectors.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/query/selectors.ts @@ -35,8 +35,10 @@ export const isQueryLoading = (state: RootState) => getState(state).state === 'l export const getCliqueMap = (state: RootState) => getState(state).cliqueMap; export const getCliqueLoading = (state: RootState) => getState(state).cliqueLoading; +export const getQueryItem = (state: RootState) => getState(state).queryItem; + export const isQueryExecuted = (state: RootState): boolean => { - const queryItem = getState(state).queryItem; + const queryItem = getQueryItem(state); // TODO: Use real query's state return Boolean(queryItem?.id) && queryItem?.state !== QueryStatus.DRAFT; }; diff --git a/packages/ui/src/ui/pages/query-tracker/module/queryChart/actions.ts b/packages/ui/src/ui/pages/query-tracker/module/queryChart/actions.ts index 036e4a2a0..219aa3175 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/queryChart/actions.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/queryChart/actions.ts @@ -1,130 +1,53 @@ import {YTApiId, ytApiV4Id} from '../../../../rum/rum-wrap-api'; -import {Visualization} from '../types'; - +import {Visualization} from '../../QueryResultsVisualization/types'; import debounce_ from 'lodash/debounce'; -import type {QueryResultVisualizationAction} from './reducer'; -import {QueryResultMetaScheme, getQueryResultMeta, readQueryResults} from '../../module/api'; -import {getPrimitiveTypesMap} from '../../../../store/selectors/global/supported-features'; -import thorYPath from '../../../../common/thor/ypath'; -import {getType} from '../../../../components/SchemaDataType/dataTypes'; -import {Type, parseV3Type} from '../../../../components/SchemaDataType/dateTypesV3'; -import {getQueryResultGlobalSettings} from '../../module/query_result/selectors'; -import {prepareFormattedValue} from '../../module/query_result/utils/format'; -import {selectQueryResult} from './selectors'; -import {RootState} from '../../../../store/reducers/index'; -import {AppThunkDispatch} from '../../../../store/thunkDispatch'; +import {initialVisualization, setSaved, setVisualization} from './queryChartSlice'; +import {RootState} from '../../../../store/reducers'; import {wrapApiPromiseByToaster} from '../../../../utils/utils'; +import {ThunkAction} from 'redux-thunk'; +import {Action, Dispatch} from 'redux'; +import {getQueryItem} from '../query/selectors'; + +const DELAY = 5 * 1000; + +type AsyncAction = ThunkAction; type SaveQueryChartConfig = { - visualizations: Visualization[]; + visualization: Visualization; queryId: string; }; -const DELAY = 5 * 1000; - -const debouncedSaveQueryChartConfig = debounce_( - (dispatch: AppThunkDispatch, payload: SaveQueryChartConfig) => { - const promise = ytApiV4Id - .alterQuery(YTApiId.alterQuery, { - parameters: { - query_id: payload.queryId, - annotations: { - ui_chart_config: payload.visualizations, - }, +const saveChartConfig = (dispatch: Dispatch, chartConfig: SaveQueryChartConfig) => { + dispatch(setSaved(false)); + wrapApiPromiseByToaster( + ytApiV4Id.alterQuery(YTApiId.alterQuery, { + parameters: { + query_id: chartConfig.queryId, + annotations: { + chartConfig: chartConfig.visualization, }, - }) - .then(() => { - dispatch({ - type: 'set-visualization-saved', - data: { - saved: true, - }, - }); - }); - - wrapApiPromiseByToaster(promise, { + }, + }), + { toasterName: 'saveQueryChartConfig', skipSuccessToast: true, errorContent: 'Failed to save query chart config', - }); - }, - DELAY, -); + }, + ).then(() => { + dispatch(setSaved(true)); + }); +}; -export function saveQueryChartConfig(payload: SaveQueryChartConfig) { - return (dispatch: AppThunkDispatch) => { - dispatch({ - type: 'set-visualization-saved', - data: { - saved: false, - }, - }); +const debouncedSaveQueryChartConfig = debounce_(saveChartConfig, DELAY); +export const saveQueryChartConfig = + (payload: SaveQueryChartConfig): AsyncAction => + (dispatch) => { debouncedSaveQueryChartConfig(dispatch, payload); }; -} - -export function newbiusLoadQueryResults({ - queryId, - resultIndex, -}: { - queryId: string; - resultIndex: number; -}) { - return async (dispatch: AppThunkDispatch, getState: () => RootState) => { - const queryResultAlreadyExist = selectQueryResult(getState()); - if (queryResultAlreadyExist) { - return; - } +export const loadVisualization = (): AsyncAction => (dispatch, getState) => { + const queryItem = getQueryItem(getState()); - const meta = await dispatch(getQueryResultMeta(queryId, resultIndex)); - - if (meta?.error) throw meta.error; - - const typeMap = getPrimitiveTypesMap(getState()); - const scheme: QueryResultMetaScheme[] = thorYPath.getValue(meta?.schema) || []; - const columns = - scheme.map(({name, type_v3: typeV3}) => { - return { - name, - type: getType(parseV3Type(typeV3 as Type, typeMap)), - displayName: name, - }; - }) || []; - - const settings = getQueryResultGlobalSettings(); - - const result = await dispatch( - readQueryResults( - queryId, - resultIndex, - { - start: 0, - end: Infinity, - }, - columns.map(({name}) => name), - {cellsSize: settings.cellSize}, - ), - ); - - const {rows, yql_type_registry: types} = result; - - const queryResult = rows.map((v) => { - return Object.entries(v).reduce( - (acc, [k, [value, typeIndex]]) => { - acc[k] = prepareFormattedValue(value, types[Number(typeIndex)]); - return acc; - }, - {} as Record, - ); - }); - - dispatch({ - type: 'set-query-result', - data: { - queryResult, - }, - }); - }; -} + dispatch(setVisualization(queryItem?.annotations?.chartConfig || initialVisualization)); +}; diff --git a/packages/ui/src/ui/pages/query-tracker/module/queryChart/queryChartSlice.ts b/packages/ui/src/ui/pages/query-tracker/module/queryChart/queryChartSlice.ts index b501c6ac2..313d2b131 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/queryChart/queryChartSlice.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/queryChart/queryChartSlice.ts @@ -1,213 +1,116 @@ -import type { - ChartSettings, - ExtendedQueryItem, - Field, - Visualization, - VisualizationId, -} from '../types'; -import type {ActionD} from '../../../../types'; -import type {QueryResult} from '../preparers/types'; +import type {ChartSettings, Visualization} from '../../QueryResultsVisualization/types'; +import {PayloadAction, createSlice} from '@reduxjs/toolkit'; -export interface QueryResultsVisualizationState { +export type QueryResultsVisualizationState = { saved: boolean; - query: ExtendedQueryItem | undefined; - queryResults: Record; - resultIndex: number; - visualizations: Visualization[]; -} + visualization: Visualization; +}; -function getInitialVisualization(): Visualization { - return { - id: 'line', - placeholders: [ - { - id: 'x', - fields: [], - }, - { - id: 'y', - fields: [], - }, - { - id: 'colors', - fields: [], - }, - ], - chartSettings: { - xAxis: { - legend: 'on', - labels: 'on', - title: '', - grid: 'on', - pixelInterval: '', - }, - yAxis: { - labels: 'on', - title: '', - grid: 'on', - pixelInterval: '', - }, +export const initialVisualization: Visualization = { + id: 'line', + placeholders: [ + { + id: 'x', + field: '', + }, + { + id: 'y', + field: '', + }, + { + id: 'colors', + field: '', + }, + ], + chartSettings: { + xAxis: { + legend: 'on', + labels: 'on', + title: '', + grid: 'on', + pixelInterval: '', }, - }; -} + yAxis: { + labels: 'on', + title: '', + grid: 'on', + pixelInterval: '', + }, + }, +}; export const initialState: QueryResultsVisualizationState = { saved: true, - query: undefined, - queryResults: {}, - resultIndex: 0, - visualizations: [getInitialVisualization()], + visualization: initialVisualization, }; -export type QueryResultVisualizationAction = - | ActionD<'set-fields', {placeholderId: string; fields: Field[]}> - | ActionD<'remove-field', {placeholderId: string; field: Field}> - | ActionD<'set-chart-settings', ChartSettings> - | ActionD<'set-visualization', VisualizationId> - | ActionD<'set-query', {query: ExtendedQueryItem}> - | ActionD<'set-result-index', number> - | ActionD<'set-visualization-saved', {saved: boolean}> - | ActionD<'set-query-result', {queryResult: QueryResult}>; - -export function queryResultsVisualization( - state: QueryResultsVisualizationState = initialState, - action: QueryResultVisualizationAction, -): QueryResultsVisualizationState { - switch (action.type) { - case 'set-visualization-saved': { - const {saved} = action.data; - - return { - ...state, - saved, - }; - } - case 'set-query': { - const {query} = action.data; - - const queryVisualizations = query.annotations?.ui_chart_config || []; - - const visualizations = []; - - for (let i = 0; i < query.result_count; i++) { - visualizations.push(queryVisualizations[i] || getInitialVisualization()); - } - - return { - ...state, - query, - queryResults: {}, - visualizations, - }; - } - case 'set-query-result': { - const {queryResult} = action.data; - +const queryChartSlice = createSlice({ + initialState, + name: 'queryChart', + reducers: { + setSaved: (state, {payload}: PayloadAction) => { + state.saved = payload; + }, + setField: (state, {payload}: PayloadAction<{placeholderId: string; fieldName: string}>) => { return { ...state, - queryResults: { - ...state.queryResults, - [state.resultIndex]: queryResult, - }, - }; - } - case 'set-fields': { - const visualizations = state.visualizations.map((visualization, index) => { - if (index !== state.resultIndex) { - return visualization; - } - - return { - ...visualization, - placeholders: visualization.placeholders.map((placeholder) => { - if (placeholder.id === action.data.placeholderId) { + visualization: { + ...state.visualization, + placeholders: state.visualization.placeholders.map((placeholder) => { + if (placeholder.id === payload.placeholderId) { return { ...placeholder, - fields: action.data.fields, + field: payload.fieldName, }; } return placeholder; }), - }; - }); - - return { - ...state, - visualizations, - }; - } - case 'remove-field': { - const visualizations = state.visualizations.map((visualization, index) => { - if (index !== state.resultIndex) { - return visualization; - } - - const placeholders = visualization.placeholders.map((placeholder) => { - if (placeholder.id === action.data.placeholderId) { - return { - ...placeholder, - fields: placeholder.fields.filter( - (field) => field.name !== action.data.field.name, - ), - }; - } - - return placeholder; - }); - - return { - ...visualization, - placeholders, - }; - }); - - return { - ...state, - visualizations, + }, }; - } - case 'set-chart-settings': { - const visualizations = state.visualizations.map((visualization, index) => { - if (index !== state.resultIndex) { - return visualization; + }, + removeField: ( + state, + {payload}: PayloadAction<{placeholderId: string; fieldName: string}>, + ) => { + const placeholders = state.visualization.placeholders.map((placeholder) => { + if (placeholder.id === payload.placeholderId) { + return { + ...placeholder, + field: '', + }; } - return { - ...visualization, - chartSettings: action.data, - }; + return placeholder; }); return { ...state, - visualizations, + visualization: { + ...state.visualization, + placeholders, + }, }; - } - case 'set-visualization': { - const visualizations = state.visualizations.map((visualization, index) => { - if (index !== state.resultIndex) { - return visualization; - } - - return { - ...visualization, - id: action.data, - }; - }); - + }, + setChartSettings: (state, {payload}: PayloadAction) => { return { ...state, - visualizations, + visualization: { + ...state.visualization, + chartSettings: payload, + }, }; - } - case 'set-result-index': { + }, + setVisualization: (state, {payload}: PayloadAction) => { return { ...state, - resultIndex: action.data, + visualization: payload, }; - } - default: - return state; - } -} + }, + }, +}); + +export const {setSaved, setChartSettings, setField, removeField, setVisualization} = + queryChartSlice.actions; + +export const queryChartReducer = queryChartSlice.reducer; diff --git a/packages/ui/src/ui/pages/query-tracker/module/queryChart/selectors.ts b/packages/ui/src/ui/pages/query-tracker/module/queryChart/selectors.ts index 30e664139..b42406e2e 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/queryChart/selectors.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/queryChart/selectors.ts @@ -1,15 +1,14 @@ import {createSelector} from 'reselect'; -import get_ from 'lodash/get'; -import {RootState} from '../../../../store/reducers/index'; -import type {QueryResult} from '../preparers/types'; -import type {Placeholder, PlaceholderId} from '../types'; -import objectHash from 'object-hash'; - -export const selectQueryResultAllVisualizations = (state: RootState) => - state.queryResultsVisualization.visualizations; +import {RootState} from '../../../../store/reducers'; +import {getQueryResultsState} from '../query_result/selectors'; +import {getQueryDraft} from '../query/selectors'; +import {QueryResultReadyState} from '../query_result/types'; +import {colorsValidation} from '../../QueryResultsVisualization/validation/colorsValidation'; +import {lineAndBarValidation} from '../../QueryResultsVisualization/validation/lineAndBarValidation'; +import {scatterValidation} from '../../QueryResultsVisualization/validation/scatterValidation'; export const selectQueryResultVisualization = (state: RootState) => - state.queryResultsVisualization.visualizations[state.queryResultsVisualization.resultIndex]; + state.queryTracker.queryChart.visualization; export const selectQueryResultVisualizationPlaceholders = (state: RootState) => selectQueryResultVisualization(state).placeholders; @@ -20,26 +19,27 @@ export const selectQueryResultVisualizationId = (state: RootState) => export const selectQueryResultChartSettings = (state: RootState) => selectQueryResultVisualization(state).chartSettings; -export const selectQueryResultIndex = (state: RootState) => - state.queryResultsVisualization.resultIndex; - export const selectQueryResultChartSaved = (state: RootState) => - state.queryResultsVisualization.saved; + state.queryTracker.queryChart.saved; + +export const selectQueryResult = createSelector( + [getQueryDraft, getQueryResultsState], + (draft, results) => { + const {id} = draft; -export const selectQueryId = (state: RootState) => state.queryResultsVisualization.query?.id; + if (!id || !(id in results)) return []; -export const selectQueryResult = (state: RootState) => - state.queryResultsVisualization.queryResults[state.queryResultsVisualization.resultIndex]; + return (results[id][0] as QueryResultReadyState).results; + }, +); export const selectAvailableFields = (state: RootState) => { const result = selectQueryResult(state); - const row = (result && result[0]) || {}; + const row = (result && result[0]) || []; - const columns: {name: string}[] = Object.keys(row).map((name) => { - return { - name, - }; + const columns: string[] = Object.keys(row).map((name) => { + return name; }); return columns; @@ -53,7 +53,7 @@ export const resultsPlaceholdersValidation = createSelector( ], (queryResult, placeholders, visualizationId): Record => { const hasColors = Boolean( - placeholders.find((placeholder) => placeholder.id === 'colors')?.fields.length, + placeholders.find((placeholder) => placeholder.id === 'colors')?.field, ); if (hasColors) { @@ -67,152 +67,11 @@ export const resultsPlaceholdersValidation = createSelector( }, ); -function scatterValidation(queryResult: QueryResult, placeholders: Placeholder[]) { - const fields = placeholders.reduce( - (ret: {x: null | string; y: null | string}, field) => { - const fieldName = get_(field, 'fields.0.name'); - const fieldType = get_(field, 'id') as 'x' | 'y'; - - if (fieldName && fieldType) { - ret[fieldType] = fieldName; - } - - return ret; - }, - {x: null, y: null}, - ); - - const xFieldName = fields.x; - const yFieldName = fields.y; - const notAllFieldsSelected = !xFieldName || !yFieldName; - - if (notAllFieldsSelected) { - return { - x: {invalid: false}, - y: {invalid: false}, - }; - } - - const hashes: Record = {}; - - const isPointsDuplicated = queryResult.some((item) => { - const xValue = item[xFieldName].$rawValue; - const yValue = item[yFieldName].$rawValue; - // eslint-disable-next-line new-cap - const hash = objectHash.MD5({ - xValue, - yValue, - }); - - const isPointDuplicated = hashes[hash]; - - hashes[hash] = true; - - return isPointDuplicated; - }); - - return { - x: {invalid: isPointsDuplicated}, - y: {invalid: isPointsDuplicated}, - }; -} - -function lineAndBarValidation(queryResult: QueryResult, placeholders: Placeholder[]) { - const fields = placeholders.reduce( - (ret: {x: null | string; y: null | string}, field) => { - const fieldName = get_(field, 'fields.0.name'); - const fieldType = get_(field, 'id') as 'x' | 'y'; - - if (fieldName && fieldType) { - ret[fieldType] = fieldName; - } - - return ret; - }, - {x: null, y: null}, - ); - - const xFieldName = fields.x; - const yFieldName = fields.y; - const notAllFieldsSelected = !xFieldName || !yFieldName; - - if (notAllFieldsSelected) { - return { - x: {invalid: false}, - }; - } - - const xCoords: Record = {}; - - const isXCoordsDuplicated = queryResult.some((item) => { - const xValue = item[xFieldName].$rawValue; - - const isXValueDuplicated = xCoords[xValue]; - - xCoords[xValue] = true; - - return isXValueDuplicated; - }); - - return { - x: {invalid: isXCoordsDuplicated}, - } as unknown as Record; -} - -function colorsValidation(queryResult: QueryResult, placeholders: Placeholder[]) { - const fields = placeholders.reduce((acc: string[], field) => { - const fieldName = get_(field, 'fields.0.name'); - - if (fieldName) { - acc.push(fieldName); - } - - return acc; - }, []); - - const notAllFieldsSelected = fields.length < placeholders.length; - - if (notAllFieldsSelected) { - return { - x: {invalid: false}, - }; - } - - const hashes: Record = {}; - - const isDataDuplicated = queryResult.some((item) => { - const newObject = fields.reduce((acc: Record, field: string) => { - acc[field] = item[field].$rawValue; - return acc; - }, {}); - - // eslint-disable-next-line new-cap - const lineHash = objectHash.MD5(newObject); - - const isLineDuplicated = hashes[lineHash]; - - hashes[lineHash] = true; - - return isLineDuplicated; - }); - - return { - x: {invalid: isDataDuplicated}, - }; -} - -export const selectIsPlaceholdersMissSomeFields = createSelector( +export const selectIsPlaceholdersFieldsFilled = createSelector( [selectQueryResultVisualizationPlaceholders], (placeholders) => { - const placeholdersMap = placeholders.reduce( - (acc, placeholder) => { - acc[placeholder.id] = placeholder.fields.length > 0; - - return acc; - }, - {} as Record, - ); - - return !placeholdersMap.x || !placeholdersMap.y; + return placeholders.every((item) => { + return (item.id !== 'x' && item.id !== 'y') || item.field !== ''; + }); }, ); diff --git a/packages/ui/src/ui/pages/query-tracker/module/query_result/actions.ts b/packages/ui/src/ui/pages/query-tracker/module/query_result/actions.ts index 82c2228e8..33bbbd45e 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/query_result/actions.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/query_result/actions.ts @@ -9,7 +9,7 @@ import { } from '../api'; import {getType} from '../../../../components/SchemaDataType/dataTypes'; import {getQueryResultGlobalSettings, getQueryResultSettings, hasQueryResult} from './selectors'; -import {QueryResultErrorState, QueryResultReadyState, QueryResultState} from './types'; +import {QueryResultErrorState, QueryResultReadyState, QueryResultState, Result} from './types'; import {prepareFormattedValue} from './utils/format'; import {wrapApiPromiseByToaster} from '../../../../utils/utils'; import {getPrimitiveTypesMap} from '../../../../store/selectors/global/supported-features'; @@ -103,7 +103,7 @@ export function loadQueryResult( acc[k] = prepareFormattedValue(value, types[Number(typeIndex)]); return acc; }, - {} as Record, + {} as Record, ); }); dispatch({ @@ -174,7 +174,7 @@ export function updateQueryResult( acc[k] = prepareFormattedValue(value, types[Number(typeIndex)]); return acc; }, - {} as Record, + {} as Record, ); }); diff --git a/packages/ui/src/ui/pages/query-tracker/module/query_result/reducer.ts b/packages/ui/src/ui/pages/query-tracker/module/query_result/reducer.ts index 2c5509c6a..17bf4745f 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/query_result/reducer.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/query_result/reducer.ts @@ -20,7 +20,7 @@ import { SET_QUERY_RESULTS_SETTINGS, } from '../query-tracker-contants'; -export type QueryResultsState = Record>; +export type QueryResultsState = Record; const initialState: QueryResultsState = {}; diff --git a/packages/ui/src/ui/pages/query-tracker/module/query_result/selectors.ts b/packages/ui/src/ui/pages/query-tracker/module/query_result/selectors.ts index a54f55e76..0bf7dc23b 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/query_result/selectors.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/query_result/selectors.ts @@ -4,7 +4,7 @@ import {NAMESPACES, SettingName} from '../../../../../shared/constants/settings' import {getPath} from '../../../../../shared/utils/settings'; import {QueryResult, QueryResultReadyState, QueryResultState} from './types'; -const getQueryResultsState = (state: RootState) => state.queryTracker.results; +export const getQueryResultsState = (state: RootState) => state.queryTracker.results; export const getQueryResultGlobalSettings = (): QueryResultReadyState['settings'] => { const settings = getSettingsDataFromInitialConfig().data; diff --git a/packages/ui/src/ui/pages/query-tracker/module/query_result/types.ts b/packages/ui/src/ui/pages/query-tracker/module/query_result/types.ts index 06ed932a8..6a628a997 100644 --- a/packages/ui/src/ui/pages/query-tracker/module/query_result/types.ts +++ b/packages/ui/src/ui/pages/query-tracker/module/query_result/types.ts @@ -23,10 +23,18 @@ export enum QueryResultsViewMode { Scheme = 'scheme', } +export type Result = { + $formattedValue: string; + $fullFormattedValue: string; + $rawValue: string; + $type: string; + $value: unknown; +}; + export type QueryResultReadyState = { state: QueryResultState.Ready; resultReady: true; - results: Record[]; + results: Record[]; columns: QueryResultColumn[]; page: number; settings: { diff --git a/packages/ui/src/ui/store/reducers/index.main.ts b/packages/ui/src/ui/store/reducers/index.main.ts index fd954544d..da064c39e 100644 --- a/packages/ui/src/ui/store/reducers/index.main.ts +++ b/packages/ui/src/ui/store/reducers/index.main.ts @@ -42,7 +42,6 @@ import {hasOdinPage} from '../../config'; import {chyt} from './chyt'; import {RawVersion} from '../../store/selectors/thor/support'; import {mainLocations} from '../../store/location.main'; -import {queryResultsVisualization} from '../../pages/query-tracker/QueryResultsVisualization/store/reducer'; import {flow} from '../../store/reducers/flow'; const appReducers = { @@ -82,7 +81,6 @@ const appReducers = { chyt, manageTokens, - queryResultsVisualization, flow, };