From aad4b1f5d15103a0e5d174d7eb4026340007e067 Mon Sep 17 00:00:00 2001 From: Andrey Nenashev Date: Tue, 7 Nov 2023 14:40:02 +0100 Subject: [PATCH] feature(fe): data quality dashboard --- .../DataQuality/DataQuality.styles.ts | 29 ++++- .../components/DataQuality/DataQuality.tsx | 102 ++++++++++++++++-- .../TestResults/TestCategoryResults.styles.ts | 36 +++++++ .../TestResults/TestCategoryResults.tsx | 48 +++++++++ .../shared/elements/DonutChart/DonutChart.tsx | 4 +- odd-platform-ui/src/theme/interfaces.ts | 5 + odd-platform-ui/src/theme/palette.ts | 1 + odd-platform-ui/src/theme/typography.ts | 13 +++ 8 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 odd-platform-ui/src/components/DataQuality/TestResults/TestCategoryResults.styles.ts create mode 100644 odd-platform-ui/src/components/DataQuality/TestResults/TestCategoryResults.tsx diff --git a/odd-platform-ui/src/components/DataQuality/DataQuality.styles.ts b/odd-platform-ui/src/components/DataQuality/DataQuality.styles.ts index bea467acb..70d53039c 100644 --- a/odd-platform-ui/src/components/DataQuality/DataQuality.styles.ts +++ b/odd-platform-ui/src/components/DataQuality/DataQuality.styles.ts @@ -10,23 +10,48 @@ export const Container = styled.section( ` ); -export const SectionWrapper = styled.div( +export const Section = styled.div( ({ theme }) => css` display: flex; + flex-direction: column; + width: fit-content; padding: ${theme.spacing(3)}; border: 1px solid ${theme.palette.border.primary}; border-radius: ${theme.spacing(1)}; + gap: ${theme.spacing(2)}; + ` +); + +interface FlexDirection { + $direction?: 'column' | 'row'; +} + +export const SubSection = styled.div( + ({ theme, $direction = 'row' }) => css` + display: flex; + flex-direction: ${$direction}; + gap: ${theme.spacing(3)}; ` ); export const DashboardLegend = styled.div( ({ theme }) => css` display: flex; - justify-content: space-between; gap: ${theme.spacing(4)}; ` ); +export const ChartWrapper = styled.div( + ({ theme }) => css` + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-top: ${theme.spacing(2)}; + background: ${theme.palette.backgrounds.tertiary}; + border-radius: ${theme.spacing(1)}; + ` +); + interface DashboardLegendItemProps { $status: DataEntityRunStatus; } diff --git a/odd-platform-ui/src/components/DataQuality/DataQuality.tsx b/odd-platform-ui/src/components/DataQuality/DataQuality.tsx index a514b2570..0158d0b0b 100644 --- a/odd-platform-ui/src/components/DataQuality/DataQuality.tsx +++ b/odd-platform-ui/src/components/DataQuality/DataQuality.tsx @@ -7,6 +7,7 @@ import { DonutChart } from 'components/shared/elements'; import { useTheme } from 'styled-components'; import { useTranslation } from 'react-i18next'; import * as S from './DataQuality.styles'; +import TestCategoryResults from './TestResults/TestCategoryResults'; function capitalizeFirstLetter(str: string) { return [...str][0].toUpperCase() + str.slice(1); @@ -43,30 +44,115 @@ const DataQuality: React.FC = () => { })); }, [data?.testResults]); + const tableHealthData = useMemo(() => { + if (!data) return []; + return [ + { + title: 'Healthy', + value: 10, + color: palette.runStatus.SUCCESS.color, + }, + { + title: 'Unhealthy', + value: 5, + color: palette.runStatus.FAILED.color, + }, + { + title: 'Unknown', + value: 2, + color: palette.runStatus.BROKEN.color, + }, + ]; + }, [data?.testResults]); + + const tableMonitoredTables = useMemo(() => { + if (!data) return []; + return [ + { + title: 'Monitored', + value: 10, + color: palette.runStatus.SUCCESS.color, + }, + { + title: 'Not Monitored', + value: 5, + color: palette.runStatus.UNKNOWN.color, + }, + ]; + }, [data?.testResults]); + + const testResults = useMemo(() => { + if (!data) return []; + return data.testResults.sort(({ category: a }, { category: b }) => + a.localeCompare(b) + ); + }, [data?.testResults]); + return ( - + {Object.values(DataEntityRunStatus).map(status => ( - + {capitalizeFirstLetter(status.toLowerCase())} ))} - {isSuccess ? ( + + + + {t('Table Health')} + + + + + + {t('Test Results Breakdown')} + + + + + {isSuccess && + testResults.map(categoryResults => ( + + ))} + + + + + + + {t('Monitored Tables')} + - ) : null} - - Table Monitor Section + + ); }; diff --git a/odd-platform-ui/src/components/DataQuality/TestResults/TestCategoryResults.styles.ts b/odd-platform-ui/src/components/DataQuality/TestResults/TestCategoryResults.styles.ts new file mode 100644 index 000000000..72ebf68db --- /dev/null +++ b/odd-platform-ui/src/components/DataQuality/TestResults/TestCategoryResults.styles.ts @@ -0,0 +1,36 @@ +import type { DataEntityRunStatus } from 'generated-sources'; +import styled, { css } from 'styled-components'; + +export const TestCategoryResultsWrapper = styled.div( + ({ theme }) => css` + display: flex; + justify-content: space-between; + align-items: center; + width: 560px; + background: ${theme.palette.backgrounds.tertiary}; + padding: ${theme.spacing(1)} ${theme.spacing(2)}; + border-radius: ${theme.spacing(1)}; + ` +); + +export const TestCategoryResults = styled.div( + ({ theme }) => css` + display: flex; + align-items: center; + gap: ${theme.spacing(1)}; + ` +); + +interface Status { + $status: DataEntityRunStatus; +} +export const TestCategoryResultsItem = styled.div( + ({ theme, $status }) => css` + text-align: center; + width: ${theme.spacing(5)}; + padding: ${theme.spacing(1 / 2)} ${theme.spacing(1)}; + border-radius: ${theme.spacing(1 / 2)}; + color: ${theme.palette.runStatus[$status].color}; + background: ${theme.palette.backgrounds.default}; + ` +); diff --git a/odd-platform-ui/src/components/DataQuality/TestResults/TestCategoryResults.tsx b/odd-platform-ui/src/components/DataQuality/TestResults/TestCategoryResults.tsx new file mode 100644 index 000000000..ca3cbce96 --- /dev/null +++ b/odd-platform-ui/src/components/DataQuality/TestResults/TestCategoryResults.tsx @@ -0,0 +1,48 @@ +import { Typography } from '@mui/material'; +import React, { useMemo } from 'react'; +import type { DataQualityCategoryResults } from 'generated-sources'; +import { DataEntityRunStatus } from 'generated-sources'; +import * as S from './TestCategoryResults.styles'; + +interface TestCategoryResultsProps { + categoryResults: DataQualityCategoryResults; +} + +const TestCategoryResults = ({ categoryResults }: TestCategoryResultsProps) => { + const { category, results } = categoryResults; + + const total = useMemo( + () => results.reduce((acc, { count }) => acc + count, 0), + [results] + ); + + const sortedResults = useMemo( + () => + Object.values(DataEntityRunStatus) + .map(status => results.find(result => result.status === status)) + .flatMap(f => (f ? [f] : [])), + [results] + ); + + return ( + + + {category} + + + + {total} + + {sortedResults.map(({ status, count }) => ( + + + {count > 0 ? count : '\u2013'} + + + ))} + + + ); +}; + +export default TestCategoryResults; diff --git a/odd-platform-ui/src/components/shared/elements/DonutChart/DonutChart.tsx b/odd-platform-ui/src/components/shared/elements/DonutChart/DonutChart.tsx index aa01a71fc..e2c76477b 100644 --- a/odd-platform-ui/src/components/shared/elements/DonutChart/DonutChart.tsx +++ b/odd-platform-ui/src/components/shared/elements/DonutChart/DonutChart.tsx @@ -1,6 +1,6 @@ -import React, { forwardRef, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { PieChart, Pie, Cell, Text, Label } from 'recharts'; -import type { LabelProps, type PieLabelRenderProps } from 'recharts'; +import type { LabelProps, PieLabelRenderProps } from 'recharts'; import { typography } from 'theme/typography'; import type { PolarViewBox } from 'recharts/types/util/types'; import { palette } from 'theme/palette'; diff --git a/odd-platform-ui/src/theme/interfaces.ts b/odd-platform-ui/src/theme/interfaces.ts index 8ff1abea7..daed31021 100644 --- a/odd-platform-ui/src/theme/interfaces.ts +++ b/odd-platform-ui/src/theme/interfaces.ts @@ -55,6 +55,7 @@ type EntityStatus = Record; interface TextType { primary: string; secondary: string; + secondaryVariant: string; info: string; hint: string; action: string; @@ -169,6 +170,8 @@ declare module '@mui/material/styles/createTypography' { interface TypographyOptions extends Record { errorCode?: TypographyStyleOptions; totalCountTitle?: TypographyStyleOptions; + label?: TypographyStyleOptions; + title?: TypographyStyleOptions; h0?: TypographyStyleOptions; } } @@ -176,6 +179,8 @@ declare module '@mui/material/Typography/Typography' { interface TypographyPropsVariantOverrides extends Record { errorCode: true; totalCountTitle: true; + label: true; + title: true; h0: true; } } diff --git a/odd-platform-ui/src/theme/palette.ts b/odd-platform-ui/src/theme/palette.ts index eb96886f6..733e717b3 100644 --- a/odd-platform-ui/src/theme/palette.ts +++ b/odd-platform-ui/src/theme/palette.ts @@ -76,6 +76,7 @@ export const palette = createPalette({ texts: { primary: colors.black90, secondary: colors.black50, + secondaryVariant: colors.black40, hint: colors.black30, info: colors.black70, action: colors.blue60, diff --git a/odd-platform-ui/src/theme/typography.ts b/odd-platform-ui/src/theme/typography.ts index a2805c0e6..7bf546c87 100644 --- a/odd-platform-ui/src/theme/typography.ts +++ b/odd-platform-ui/src/theme/typography.ts @@ -62,6 +62,13 @@ export const typography = createTypography(palette, { lineHeight: pxToRem(16), fontWeight: 500, }, + title: { + fontSize: pxToRem(14), + lineHeight: pxToRem(20), + color: palette.texts.secondary, + fontWeight: 500, + ...breakpointDownLgBody2, + }, subtitle1: { fontSize: pxToRem(14), lineHeight: pxToRem(20), @@ -105,6 +112,12 @@ export const typography = createTypography(palette, { lineHeight: pxToRem(36), fontWeight: 500, }, + label: { + fontSize: pxToRem(14), + lineHeight: pxToRem(20), + fontWeight: 400, + color: palette.texts.secondaryVariant, + }, ...mapKeysToValue( [ getButtonFontType('main', 'lg'),