From f9465bf4d2a5c783b1bc88378983667b3b1465df Mon Sep 17 00:00:00 2001 From: Vlad S Date: Thu, 9 Feb 2023 14:39:26 +0000 Subject: [PATCH] UI: Selection on graph with edges. Status for jobs/datasets (#2384) * Selection on graph with edges. Status for jobs/datasets Signed-off-by: tito12 --------- Signed-off-by: tito12 Co-authored-by: Willy Lulciuc --- docker/metadata.json | 18 ++++- web/src/components/core/status/MqStatus.tsx | 41 ++++++++++ .../datasets/DatasetColumnLineage.tsx | 5 +- .../components/datasets/DatasetDetailPage.tsx | 22 ++++-- .../components/datasets/DatasetVersions.tsx | 5 +- web/src/components/jobs/JobDetailPage.tsx | 12 ++- web/src/components/jobs/RunStatus.tsx | 9 ++- web/src/components/lineage/Lineage.tsx | 57 +++++++++++--- .../lineage/components/drag-bar/DragBar.tsx | 5 +- .../lineage/components/edge/Edge.tsx | 42 +++++++++- .../lineage/components/node/Node.tsx | 76 ++++--------------- .../namespace-select/NamespaceSelect.tsx | 5 +- web/src/components/search/Search.tsx | 7 +- web/src/helpers/nodes.ts | 70 +++++++++++++++++ web/src/i18n/config.ts | 24 ++++-- web/src/routes/datasets/Datasets.tsx | 20 +++-- web/src/routes/events/Events.tsx | 38 ++-------- web/src/routes/jobs/Jobs.tsx | 17 +++-- web/src/types/api.ts | 14 +++- web/src/types/util/groupBy.ts | 5 +- 20 files changed, 329 insertions(+), 163 deletions(-) create mode 100644 web/src/components/core/status/MqStatus.tsx diff --git a/docker/metadata.json b/docker/metadata.json index 0b6c656cb6..65322f89cb 100644 --- a/docker/metadata.json +++ b/docker/metadata.json @@ -1358,6 +1358,22 @@ "_schemaURL": "https://openlineage.io/spec/facets/1-0-0/DatasourceDatasetFacet.json", "name": "food_delivery_db", "uri": "postgres://food_delivery:food_delivery@postgres:5432/food_delivery" + }, + "dataQualityAssertions": { + "_producer": "https://github.com/MarquezProject/marquez/blob/main/docker/metadata.json", + "_schemaURL": "https://openlineage.io/spec/facets/1-0-0/DataQualityAssertionsDatasetFacet.json", + "assertions": [ + { + "assertion": "not_null", + "success": false, + "column": "driver_id" + }, + { + "assertion": "is_string", + "success": true, + "column": "customer_address" + } + ] } } } @@ -1824,4 +1840,4 @@ }, "producer": "https://github.com/MarquezProject/marquez/blob/main/docker/metadata.json" } -] +] \ No newline at end of file diff --git a/web/src/components/core/status/MqStatus.tsx b/web/src/components/core/status/MqStatus.tsx new file mode 100644 index 0000000000..2c5f639614 --- /dev/null +++ b/web/src/components/core/status/MqStatus.tsx @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { theme } from '../../../helpers/theme' +import Box from '@material-ui/core/Box' +import MqText from '../text/MqText' +import React from 'react' +import createStyles from '@material-ui/core/styles/createStyles' +import withStyles, { WithStyles } from '@material-ui/core/styles/withStyles' + +const styles = () => + createStyles({ + type: { + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1) + }, + status: { + width: theme.spacing(2), + height: theme.spacing(2), + borderRadius: '50%' + } + }) + +interface OwnProps { + color: string | null + label?: string +} + +const MqStatus: React.FC> = ({ label, color, classes }) => { + if (!color) { + return null + } + return ( + + + {label && {label}} + + ) +} + +export default withStyles(styles)(MqStatus) diff --git a/web/src/components/datasets/DatasetColumnLineage.tsx b/web/src/components/datasets/DatasetColumnLineage.tsx index b36b650122..ea7ce9d226 100644 --- a/web/src/components/datasets/DatasetColumnLineage.tsx +++ b/web/src/components/datasets/DatasetColumnLineage.tsx @@ -102,7 +102,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default connect( - mapStateToProps, - mapDispatchToProps -)(DatasetColumnLineage) +export default connect(mapStateToProps, mapDispatchToProps)(DatasetColumnLineage) diff --git a/web/src/components/datasets/DatasetDetailPage.tsx b/web/src/components/datasets/DatasetDetailPage.tsx index e366248f26..fe29690e2d 100644 --- a/web/src/components/datasets/DatasetDetailPage.tsx +++ b/web/src/components/datasets/DatasetDetailPage.tsx @@ -15,6 +15,7 @@ import { LineageDataset } from '../lineage/types' import { alpha } from '@material-ui/core/styles' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' +import { datasetFacetsStatus } from '../../helpers/nodes' import { deleteDataset, dialogToggle, @@ -31,7 +32,9 @@ import DatasetInfo from './DatasetInfo' import DatasetVersions from './DatasetVersions' import Dialog from '../Dialog' import IconButton from '@material-ui/core/IconButton' +import MqStatus from '../core/status/MqStatus' import MqText from '../core/text/MqText' + import React, { ChangeEvent, FunctionComponent, SetStateAction, useEffect } from 'react' const styles = ({ spacing }: ITheme) => { @@ -143,6 +146,7 @@ const DatasetDetailPage: FunctionComponent = props => { const firstVersion = versions[0] const { name, tags, description } = firstVersion + const facetsStatus = datasetFacetsStatus(firstVersion.facets) return ( @@ -202,9 +206,16 @@ const DatasetDetailPage: FunctionComponent = props => { - - {name} - + + {facetsStatus && ( + + + + )} + + {name} + + {description} @@ -241,7 +252,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(DatasetDetailPage)) +export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(DatasetDetailPage)) diff --git a/web/src/components/datasets/DatasetVersions.tsx b/web/src/components/datasets/DatasetVersions.tsx index 9b8c9b3e48..db1b70b29a 100644 --- a/web/src/components/datasets/DatasetVersions.tsx +++ b/web/src/components/datasets/DatasetVersions.tsx @@ -31,9 +31,8 @@ interface DatasetVersionsProps { versions: DatasetVersion[] } -const DatasetVersions: FunctionComponent< - DatasetVersionsProps & IWithStyles -> = props => { +const DatasetVersions: FunctionComponent> = props => { const { versions, classes } = props const [infoView, setInfoView] = React.useState(null) diff --git a/web/src/components/jobs/JobDetailPage.tsx b/web/src/components/jobs/JobDetailPage.tsx index d4788dd62c..188d2b4d2d 100644 --- a/web/src/components/jobs/JobDetailPage.tsx +++ b/web/src/components/jobs/JobDetailPage.tsx @@ -25,15 +25,16 @@ import { resetJobs, resetRuns } from '../../store/actionCreators' +import { jobRunsStatus } from '../../helpers/nodes' import { theme } from '../../helpers/theme' import { useHistory } from 'react-router-dom' import CloseIcon from '@material-ui/icons/Close' import Dialog from '../Dialog' import IconButton from '@material-ui/core/IconButton' import MqEmpty from '../core/empty/MqEmpty' +import MqStatus from '../core/status/MqStatus' import MqText from '../core/text/MqText' import RunInfo from './RunInfo' -import RunStatus from './RunStatus' import Runs from './Runs' const styles = ({ spacing }: ITheme) => { @@ -166,9 +167,9 @@ const JobDetailPage: FunctionComponent = props => { - {job.latestRun && ( + {runs.length && ( - + )} @@ -212,7 +213,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(JobDetailPage)) +export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(JobDetailPage)) diff --git a/web/src/components/jobs/RunStatus.tsx b/web/src/components/jobs/RunStatus.tsx index 1695e16fe9..1b55b79d04 100644 --- a/web/src/components/jobs/RunStatus.tsx +++ b/web/src/components/jobs/RunStatus.tsx @@ -3,8 +3,7 @@ import { Box, Theme, Tooltip, WithStyles, createStyles, withStyles } from '@material-ui/core' import { Run } from '../../types/api' - -import { runColorMap } from '../../helpers/runs' +import { runStateColor } from '../../helpers/nodes' import React, { FunctionComponent } from 'react' @@ -26,7 +25,11 @@ const RunStatus: FunctionComponent> = const { run, classes } = props return ( - + ) } diff --git a/web/src/components/lineage/Lineage.tsx b/web/src/components/lineage/Lineage.tsx index 055f22572a..ed21deb8e1 100644 --- a/web/src/components/lineage/Lineage.tsx +++ b/web/src/components/lineage/Lineage.tsx @@ -85,6 +85,7 @@ class Lineage extends React.Component { const nodeName = this.props.match.params.nodeName const namespace = this.props.match.params.namespace const nodeType = this.props.match.params.nodeType + if (nodeName && namespace && nodeType) { const nodeId = generateNodeId( this.props.match.params.nodeType.toUpperCase() as JobOrDataset, @@ -114,6 +115,7 @@ class Lineage extends React.Component { this.props.match.params.namespace, this.props.match.params.nodeName ) + this.getEdges() } } @@ -129,6 +131,48 @@ class Lineage extends React.Component { }) } + getEdges = () => { + const selectedPaths = this.getSelectedPaths() + + return g?.edges().map(e => { + const isSelected = selectedPaths.some((r: any) => e.v === r[0] && e.w === r[1]) + return Object.assign(g.edge(e), { isSelected: isSelected }) + }) + } + + getSelectedPaths = () => { + const paths = [] as Array<[string, string]> + + const getSuccessors = (node: string) => { + const successors = g?.successors(node) + if (successors?.length) { + for (let i = 0; i < node.length - 1; i++) { + if (successors[i]) { + paths.push([node, (successors[i] as unknown) as string]) + getSuccessors((successors[i] as unknown) as string) + } + } + } + } + + const getPredecessors = (node: string) => { + const predecessors = g?.predecessors(node) + if (predecessors?.length) { + for (let i = 0; i < node.length - 1; i++) { + if (predecessors[i]) { + paths.push([(predecessors[i] as unknown) as string, node]) + getPredecessors((predecessors[i] as unknown) as string) + } + } + } + } + + getSuccessors(this.props.selectedNode) + getPredecessors(this.props.selectedNode) + + return paths + } + buildGraphAll = (graph: LineageNode[]) => { // nodes for (let i = 0; i < graph.length; i++) { @@ -150,7 +194,7 @@ class Lineage extends React.Component { this.setState({ graph: g, - edges: g.edges().map(e => g.edge(e)), + edges: this.getEdges(), nodes: g.nodes().map(v => g.node(v)) }) } @@ -158,6 +202,7 @@ class Lineage extends React.Component { render() { const { classes } = this.props const i18next = require('i18next') + return ( {this.props.selectedNode === null && ( @@ -227,9 +272,6 @@ class Lineage extends React.Component { edge.points[edge.points.length - 1] - )} selectedNode={this.props.selectedNode} /> ))} @@ -262,9 +304,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default withStyles(styles)( - connect( - mapStateToProps, - mapDispatchToProps - )(withRouter(Lineage)) -) +export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(withRouter(Lineage))) diff --git a/web/src/components/lineage/components/drag-bar/DragBar.tsx b/web/src/components/lineage/components/drag-bar/DragBar.tsx index f2e74f68ec..be57e8b8f9 100644 --- a/web/src/components/lineage/components/drag-bar/DragBar.tsx +++ b/web/src/components/lineage/components/drag-bar/DragBar.tsx @@ -107,7 +107,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => }, dispatch ) -export default connect( - null, - mapDispatchToProps -)(withStyles(styles)(DragBar)) +export default connect(null, mapDispatchToProps)(withStyles(styles)(DragBar)) diff --git a/web/src/components/lineage/components/edge/Edge.tsx b/web/src/components/lineage/components/edge/Edge.tsx index 02c909da2b..cb5e3b3114 100644 --- a/web/src/components/lineage/components/edge/Edge.tsx +++ b/web/src/components/lineage/components/edge/Edge.tsx @@ -1,9 +1,11 @@ // Copyright 2018-2023 contributors to the Marquez project // SPDX-License-Identifier: Apache-2.0 +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { GraphEdge } from 'dagre' import { LinePath } from '@visx/shape' import { curveMonotoneX } from '@visx/curve' +import { faCaretRight } from '@fortawesome/free-solid-svg-icons/faCaretRight' import { theme } from '../../../../helpers/theme' import React from 'react' @@ -11,9 +13,36 @@ type EdgeProps = { edgePoints: GraphEdge[] } +type EdgePoint = { + isSelected: boolean + points: { + x: number + y: number + }[] +} + +const RADIUS = 14 +const OUTER_RADIUS = RADIUS + 8 +const ICON_SIZE = 16 + class Edge extends React.Component { + getPoints = (edge: EdgePoint) => edge.points[edge.points.length - 1] + render() { const { edgePoints } = this.props + const edgeEnds = edgePoints.map(edge => { + const isSelected = edgePoints.find( + o => + this.getPoints(o as EdgePoint).x == this.getPoints(edge as EdgePoint).x && + this.getPoints(o as EdgePoint).y == this.getPoints(edge as EdgePoint).y && + o.isSelected === true + ) + return { + ...edge.points[edge.points.length - 1], + ...{ isSelected: typeof isSelected !== 'undefined' } + } + }) + return ( <> {edgePoints.map((edge, i) => ( @@ -23,12 +52,23 @@ class Edge extends React.Component { data={edge.points} x={(d, index) => (index === 0 ? d.x + 20 : d.x - 25)} y={d => d.y} - stroke={theme.palette.secondary.main} + stroke={edge.isSelected ? theme.palette.common.white : theme.palette.secondary.main} strokeWidth={1} opacity={1} shapeRendering='geometricPrecision' /> ))} + {edgeEnds.map((edge, i) => ( + + ))} ) } diff --git a/web/src/components/lineage/components/node/Node.tsx b/web/src/components/lineage/components/node/Node.tsx index 890e3d6982..f76c3b494e 100644 --- a/web/src/components/lineage/components/node/Node.tsx +++ b/web/src/components/lineage/components/node/Node.tsx @@ -9,24 +9,16 @@ import { Node as GraphNode } from 'dagre' import { Link } from 'react-router-dom' import { MqNode } from '../../types' import { NodeText } from './NodeText' -import { Nullable } from '../../../../types/util/Nullable' -import { Run } from '../../../../types/api' import { bindActionCreators } from 'redux' + import { connect } from 'react-redux' import { encodeNode, isDataset, isJob } from '../../../../helpers/nodes' -import { faCaretRight } from '@fortawesome/free-solid-svg-icons/faCaretRight' import { faCog } from '@fortawesome/free-solid-svg-icons/faCog' import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase' import { setSelectedNode } from '../../../../store/actionCreators' import { theme } from '../../../../helpers/theme' -export type Vertex = { - x: number - y: number -} - const RADIUS = 14 -const OUTER_RADIUS = RADIUS + 8 const ICON_SIZE = 16 const BORDER = 4 @@ -36,29 +28,11 @@ interface DispatchProps { interface OwnProps { node: GraphNode - edgeEnds: Vertex[] selectedNode: string } type NodeProps = DispatchProps & OwnProps -function runStateToNodeColor(run: Nullable) { - switch (run && run.state) { - case 'NEW': - return theme.palette.secondary.main - case 'RUNNING': - return theme.palette.info.main - case 'COMPLETED': - return theme.palette.secondary.main - case 'FAILED': - return theme.palette.error.main - case 'ABORTED': - return theme.palette.warning.main - default: - return theme.palette.secondary.main - } -} - class Node extends React.Component { determineLink = (node: GraphNode) => { if (isJob(node)) { @@ -70,7 +44,7 @@ class Node extends React.Component { } render() { - const { node, edgeEnds, selectedNode } = this.props + const { node, selectedNode } = this.props const job = isJob(node) const isSelected = selectedNode === node.label const ariaJobLabel = 'Job' @@ -82,21 +56,14 @@ class Node extends React.Component { > {job ? ( + {/* { console.log(job.latestRun)} */} + {/* {console.log(runStateToNodeColor(job.latestRun))} */} - @@ -109,7 +76,7 @@ class Node extends React.Component { height={ICON_SIZE} x={node.x - ICON_SIZE / 2} y={node.y - ICON_SIZE / 2} - color={runStateToNodeColor(job.latestRun)} + color={isSelected ? theme.palette.common.white : theme.palette.secondary.main} /> ) : ( @@ -118,9 +85,9 @@ class Node extends React.Component { style={{ cursor: 'pointer' }} x={node.x - RADIUS} y={node.y - RADIUS} - fill={theme.palette.common.white} - stroke={isSelected ? theme.palette.primary.main : theme.palette.secondary.main} - strokeWidth={BORDER} + fill={isSelected ? theme.palette.secondary.main : theme.palette.common.white} + stroke={isSelected ? theme.palette.common.white : theme.palette.secondary.main} + strokeWidth={BORDER / 2} width={RADIUS * 2} height={RADIUS * 2} rx={4} @@ -129,9 +96,7 @@ class Node extends React.Component { style={{ cursor: 'pointer' }} x={node.x - (RADIUS - 2)} y={node.y - (RADIUS - 2)} - fill={theme.palette.common.white} - stroke={theme.palette.common.white} - strokeWidth={BORDER} + fill={isSelected ? theme.palette.secondary.main : theme.palette.common.white} width={(RADIUS - 2) * 2} height={(RADIUS - 2) * 2} rx={4} @@ -144,20 +109,10 @@ class Node extends React.Component { height={ICON_SIZE} x={node.x - ICON_SIZE / 2} y={node.y - ICON_SIZE / 2} - color={theme.palette.secondary.main} + color={isSelected ? theme.palette.common.white : theme.palette.secondary.main} /> )} - {edgeEnds.find(edge => edge.x === node.x && edge.y === node.y) && ( - - )} ) @@ -172,7 +127,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default connect( - null, - mapDispatchToProps -)(Node) +export default connect(null, mapDispatchToProps)(Node) diff --git a/web/src/components/namespace-select/NamespaceSelect.tsx b/web/src/components/namespace-select/NamespaceSelect.tsx index cf27735d5a..088430366c 100644 --- a/web/src/components/namespace-select/NamespaceSelect.tsx +++ b/web/src/components/namespace-select/NamespaceSelect.tsx @@ -84,7 +84,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(NamespaceSelect)) +export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(NamespaceSelect)) diff --git a/web/src/components/search/Search.tsx b/web/src/components/search/Search.tsx index 226e9eaaa4..8f4e1819e6 100644 --- a/web/src/components/search/Search.tsx +++ b/web/src/components/search/Search.tsx @@ -362,9 +362,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default withStyles(styles)( - connect( - mapStateToProps, - mapDispatchToProps - )(withRouter(Search)) -) +export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(withRouter(Search))) diff --git a/web/src/helpers/nodes.ts b/web/src/helpers/nodes.ts index 5c2ea2647c..1b4602f4d6 100644 --- a/web/src/helpers/nodes.ts +++ b/web/src/helpers/nodes.ts @@ -1,8 +1,10 @@ // Copyright 2018-2023 contributors to the Marquez project // SPDX-License-Identifier: Apache-2.0 +import { EventType, Facets, Run, RunState } from '../types/api' import { JobOrDataset, LineageDataset, LineageJob, MqNode } from '../components/lineage/types' import { Undefinable } from '../types/util/Nullable' +import { theme } from './theme' export function isJob(node: MqNode): Undefinable { if (node.data.type === 'BATCH') { @@ -56,3 +58,71 @@ type SearchDelimiterMap = typeof searchDelimiterMap export function parseSearchGroup(nodeId: string, field: keyof SearchDelimiterMap) { return nodeId.split(':')[searchDelimiterMap[field]] || '' } + +export function eventTypeColor(state: EventType) { + switch (state) { + case 'START': + return theme.palette.info.main + case 'RUNNING': + return theme.palette.info.main + case 'COMPLETE': + return theme.palette.primary.main + case 'FAIL': + return theme.palette.error.main + case 'ABORT': + return theme.palette.warning.main + default: + return theme.palette.secondary.main + } +} + +export function runStateColor(state: RunState) { + switch (state) { + case 'NEW': + return theme.palette.secondary.main + case 'RUNNING': + return theme.palette.info.main + case 'COMPLETED': + return theme.palette.primary.main + case 'FAILED': + return theme.palette.error.main + case 'ABORTED': + return theme.palette.warning.main + default: + return theme.palette.secondary.main + } +} + +export function jobRunsStatus(runs: Run[], limit = 14) { + runs = runs.slice(-limit) + + const isAllFailed = runs.every(e => e.state === 'FAILED') + const isSomeFailed = runs.some(e => e.state === 'FAILED') + + if (isAllFailed) { + return theme.palette.error.main as string + } else if (isSomeFailed) { + return theme.palette.info.main as string + } else { + return theme.palette.primary.main as string + } +} + +export function datasetFacetsStatus(facets: Facets, limit = 14) { + const assertions = facets?.dataQualityAssertions?.assertions?.slice(-limit) + + if (!assertions?.length) { + return null + } + + const isAllFalse = assertions.every((e: any) => e.success === false) + const isSomeFalse = assertions.some((e: any) => e.success === false) + + if (isAllFalse) { + return theme.palette.error.main as string + } else if (isSomeFalse) { + return theme.palette.info.main as string + } else { + return theme.palette.primary.main as string + } +} diff --git a/web/src/i18n/config.ts b/web/src/i18n/config.ts index 96d4adee68..6a9b48c3a4 100644 --- a/web/src/i18n/config.ts +++ b/web/src/i18n/config.ts @@ -75,7 +75,8 @@ i18next name_col: 'NAME', namespace_col: 'NAMESPACE', source_col: 'SOURCE', - updated_col: 'UPDATED AT' + updated_col: 'UPDATED AT', + status_col: 'STATUS' }, datasets_column_lineage: { empty_title: 'No column lineage', @@ -88,7 +89,8 @@ i18next name_col: 'NAME', namespace_col: 'NAMESPACE', updated_col: 'UPDATED AT', - latest_run_col: 'LATEST RUN DURATION' + latest_run_col: 'LATEST RUN DURATION', + latest_run_state_col: 'LATEST RUN STATE' }, runs_columns: { id: 'ID', @@ -185,7 +187,8 @@ i18next name_col: 'NOM', namespace_col: 'ESPACE DE NOMS', source_col: 'SOURCE', - updated_col: 'MISE À JOUR À' + updated_col: 'MISE À JOUR À', + status_col: 'STATUT' }, datasets_column_lineage: { empty_title: 'Aucune lignée de colonne', @@ -199,7 +202,8 @@ i18next name_col: 'NOM', namespace_col: 'ESPACE DE NOMS', updated_col: 'MISE À JOUR À', - latest_run_col: "DERNIÈRE DURÉE D'EXÉCUTION" + latest_run_col: "DERNIÈRE DURÉE D'EXÉCUTION", + latest_run_state_col: "DERNIER ÉTAT D'EXÉCUTIONE" }, runs_columns: { id: 'ID', @@ -297,7 +301,8 @@ i18next name_col: 'NOMBRE', namespace_col: 'ESPACIO DE NOMBRES', source_col: 'FUENTE', - updated_col: 'ACTUALIZADO EN' + updated_col: 'ACTUALIZADO EN', + status_col: 'ESTADO' }, datasets_column_lineage: { empty_title: 'Sin linaje de columna', @@ -311,7 +316,8 @@ i18next name_col: 'NOMBRE', namespace_col: 'ESPACIO DE NOMBRES', updated_col: 'ACTUALIZADO EN', - latest_run_col: 'DURACIÓN DE LA ÚLTIMA EJECUCIÓN' + latest_run_col: 'DURACIÓN DE LA ÚLTIMA EJECUCIÓN', + latest_run_state_col: 'ESTADO DE LA ÚLTIMA EJECUCIÓN' }, runs_columns: { id: 'ID', @@ -409,7 +415,8 @@ i18next name_col: 'NAZWA', namespace_col: 'PRZESTRZEŃ NAZW', source_col: 'ŹRÓDŁO', - updated_col: 'ZAKTUALIZOWANO' + updated_col: 'ZAKTUALIZOWANO', + status_col: 'STATUS' }, datasets_column_lineage: { empty_title: 'Brak rodowodu kolumny', @@ -423,7 +430,8 @@ i18next name_col: 'NAZWA', namespace_col: 'PRZESTRZEŃ NAZW', updated_col: 'ZAKTUALIZOWANO', - latest_run_col: 'NAJNOWSZY CZAS TRWANIA' + latest_run_col: 'NAJNOWSZY CZAS TRWANIA', + latest_run_state_col: 'NAJNOWSZY STAN URUCHOMIENIA' }, runs_columns: { id: 'ID', diff --git a/web/src/routes/datasets/Datasets.tsx b/web/src/routes/datasets/Datasets.tsx index d93773b527..97ca472508 100644 --- a/web/src/routes/datasets/Datasets.tsx +++ b/web/src/routes/datasets/Datasets.tsx @@ -9,11 +9,12 @@ import { MqScreenLoad } from '../../components/core/screen-load/MqScreenLoad' import { Nullable } from '../../types/util/Nullable' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import { encodeNode } from '../../helpers/nodes' +import { datasetFacetsStatus, encodeNode } from '../../helpers/nodes' import { fetchDatasets, resetDatasets } from '../../store/actionCreators' import { formatUpdatedAt } from '../../helpers' import Box from '@material-ui/core/Box' import MqEmpty from '../../components/core/empty/MqEmpty' +import MqStatus from '../../components/core/status/MqStatus' import MqText from '../../components/core/text/MqText' import React from 'react' import createStyles from '@material-ui/core/styles/createStyles' @@ -88,6 +89,9 @@ class Datasets extends React.Component { {i18next.t('datasets_route.updated_col')} + + {i18next.t('datasets_route.status_col')} + @@ -117,6 +121,15 @@ class Datasets extends React.Component { {formatUpdatedAt(dataset.updatedAt)} + + {datasetFacetsStatus(dataset.facets) ? ( + <> + + + ) : ( + N/A + )} + ) })} @@ -147,7 +160,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(Datasets)) +export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Datasets)) diff --git a/web/src/routes/events/Events.tsx b/web/src/routes/events/Events.tsx index 678dbe9155..d18da28b35 100644 --- a/web/src/routes/events/Events.tsx +++ b/web/src/routes/events/Events.tsx @@ -19,16 +19,17 @@ import { IState } from '../../store/reducers' import { MqScreenLoad } from '../../components/core/screen-load/MqScreenLoad' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' +import { eventTypeColor } from '../../helpers/nodes' import { fetchEvents, resetEvents } from '../../store/actionCreators' import { fileSize, formatUpdatedAt } from '../../helpers' import { formatDateAPIQuery, formatDatePicker } from '../../helpers/time' import { saveAs } from 'file-saver' -import { theme } from '../../helpers/theme' import Box from '@material-ui/core/Box' import IconButton from '@material-ui/core/IconButton' import MqDatePicker from '../../components/core/date-picker/MqDatePicker' import MqEmpty from '../../components/core/empty/MqEmpty' import MqJson from '../../components/core/code/MqJson' +import MqStatus from '../../components/core/status/MqStatus' import MqText from '../../components/core/text/MqText' import React from 'react' import createStyles from '@material-ui/core/styles/createStyles' @@ -42,16 +43,6 @@ const styles = (theme: Theme) => { alignItems: 'center', gap: theme.spacing(2) }, - type: { - display: 'flex', - alignItems: 'center', - gap: theme.spacing(1) - }, - status: { - width: theme.spacing(2), - height: theme.spacing(2), - borderRadius: '50%' - }, table: { marginBottom: theme.spacing(2) }, @@ -91,15 +82,6 @@ type EventsProps = WithStyles & StateProps & DispatchProps const EVENTS_COLUMNS = ['ID', 'STATE', 'NAME', 'NAMESPACE', 'TIME'] -function eventTypeColor(type: string) { - switch (type) { - case 'START': - return theme.palette.info.main - case 'COMPLETE': - return theme.palette.primary.main - } -} - class Events extends React.Component { pageSize: number @@ -284,13 +266,10 @@ class Events extends React.Component { {event.run.runId} - - - {event.eventType} - + {event.job.name} @@ -381,7 +360,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default connect( - mapStateToProps, - mapDispatchToProps -)(withStyles(styles)(Events)) +export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(Events)) diff --git a/web/src/routes/jobs/Jobs.tsx b/web/src/routes/jobs/Jobs.tsx index ab61eb848e..74704d3bab 100644 --- a/web/src/routes/jobs/Jobs.tsx +++ b/web/src/routes/jobs/Jobs.tsx @@ -9,12 +9,13 @@ import { MqScreenLoad } from '../../components/core/screen-load/MqScreenLoad' import { Nullable } from '../../types/util/Nullable' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' -import { encodeNode } from '../../helpers/nodes' +import { encodeNode, runStateColor } from '../../helpers/nodes' import { fetchJobs, resetJobs } from '../../store/actionCreators' import { formatUpdatedAt } from '../../helpers' import { stopWatchDuration } from '../../helpers/time' import Box from '@material-ui/core/Box' import MqEmpty from '../../components/core/empty/MqEmpty' +import MqStatus from '../../components/core/status/MqStatus' import MqText from '../../components/core/text/MqText' import React from 'react' @@ -85,6 +86,9 @@ class Jobs extends React.Component { {i18next.t('jobs_route.latest_run_col')} + + {i18next.t('jobs_route.latest_run_state_col')} + @@ -112,6 +116,12 @@ class Jobs extends React.Component { : 'N/A'} + + + ) })} @@ -142,7 +152,4 @@ const mapDispatchToProps = (dispatch: Redux.Dispatch) => dispatch ) -export default connect( - mapStateToProps, - mapDispatchToProps -)(Jobs) +export default connect(mapStateToProps, mapDispatchToProps)(Jobs) diff --git a/web/src/types/api.ts b/web/src/types/api.ts index d223473c34..04d939af4e 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -28,8 +28,10 @@ export interface Events { events: Event[] } +export type EventType = 'START' | 'RUNNING' | 'ABORT' | 'FAIL' | 'COMPLETE' + export interface Event { - eventType: string + eventType: EventType eventTime: string producer: string schemaURL: string @@ -80,6 +82,16 @@ export interface DatasetVersions { versions: DatasetVersion[] } +export interface Facets { + dataQualityAssertions?: { + assertions?: { + assertion: string + column: string + success: boolean + }[] + } +} + export interface DatasetVersion { id: DatasetVersionId type: DatasetType diff --git a/web/src/types/util/groupBy.ts b/web/src/types/util/groupBy.ts index 4d9ee1aa59..d114c0c562 100644 --- a/web/src/types/util/groupBy.ts +++ b/web/src/types/util/groupBy.ts @@ -6,7 +6,10 @@ export function groupBy(list: T[], key: K) { list.forEach(item => { const itemKey = item[key] if (!map.has(itemKey)) { - map.set(itemKey, list.filter(i => i[key] === item[key])) + map.set( + itemKey, + list.filter(i => i[key] === item[key]) + ) } }) return map