From 2e3b56377657fbcca4e95d0cdf6c78c41c61488a Mon Sep 17 00:00:00 2001 From: GnsP Date: Tue, 26 Nov 2024 12:37:18 +0530 Subject: [PATCH] add error classification ui component --- app/cdap/api/pipeline.js | 2 + app/cdap/components/LogViewer/LogsUrlUtils.ts | 34 ++ app/cdap/components/LogViewer/TopPanel.tsx | 173 +++++---- .../PipelineRunErrorDetails.tsx | 350 ++++++++++++++++++ .../PipelineDetailsTopPanel/index.tsx | 24 +- .../RunLevelInfo/PipelineLogViewer/index.tsx | 64 +++- .../RunLevelInfo/RunLogsStatsChips.tsx | 76 ++++ .../PipelineDetails/RunLevelInfo/index.js | 12 +- .../components/PipelineDetails/store/index.js | 20 + app/cdap/components/ThemeWrapper/colors.ts | 1 + .../components/layouts/SectionWithPanel.tsx | 4 +- app/cdap/styles/colors.scss | 2 + app/cdap/testids/testids.yaml | 6 + app/cdap/text/text-en.yaml | 14 + app/cdap/utils/time.ts | 2 +- app/directives/dag-plus/my-dag-ctrl.js | 9 + app/directives/dag-plus/my-dag.html | 1 + app/directives/dag-plus/my-dag.js | 3 +- app/directives/dag-plus/my-dag.less | 9 + app/hydrator/adapters.less | 7 + .../controllers/detail/canvas-ctrl.js | 4 + app/hydrator/templates/detail/canvas.html | 1 + 22 files changed, 707 insertions(+), 111 deletions(-) create mode 100644 app/cdap/components/LogViewer/LogsUrlUtils.ts create mode 100644 app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineRunErrorDetails.tsx create mode 100644 app/cdap/components/PipelineDetails/RunLevelInfo/RunLogsStatsChips.tsx diff --git a/app/cdap/api/pipeline.js b/app/cdap/api/pipeline.js index ce1bd15d0ea..7fb6244ec79 100644 --- a/app/cdap/api/pipeline.js +++ b/app/cdap/api/pipeline.js @@ -50,6 +50,8 @@ export const MyPipelineApi = { getStatistics: apiCreator(dataSrc, 'GET', 'REQUEST', statsPath), getMetadataEndpoints: apiCreator(dataSrc, 'GET', 'REQUEST', metadataPath), getRunDetails: apiCreator(dataSrc, 'GET', 'REQUEST', `${programPath}/runs/:runid`), + getRunErrorDetails: apiCreator(dataSrc, 'POST', 'REQUEST', `${programPath}/runs/:runid/classify`), + getRuns: apiCreator(dataSrc, 'GET', 'REQUEST', `${programPath}/runs`), getVersionedRuns: apiCreator(dataSrc, 'GET', 'REQUEST', `${versionedProgramPath}/runs`), pollRuns: apiCreator(dataSrc, 'GET', 'POLL', `${programPath}/runs`), diff --git a/app/cdap/components/LogViewer/LogsUrlUtils.ts b/app/cdap/components/LogViewer/LogsUrlUtils.ts new file mode 100644 index 00000000000..41f6104b516 --- /dev/null +++ b/app/cdap/components/LogViewer/LogsUrlUtils.ts @@ -0,0 +1,34 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import IDataFetcher from 'components/LogViewer/DataFetcher'; + +export function getRawLogsBasePath(dataFetcher: IDataFetcher) { + const backendUrl = dataFetcher.getRawLogsUrl(); + const encodedBackendUrl = encodeURIComponent(backendUrl); + + const url = `/downloadLogs?backendPath=${encodedBackendUrl}`; + return url; +} + +export function getRawLogsUrl(dataFetcher: IDataFetcher) { + return `${getRawLogsBasePath(dataFetcher)}&type=raw`; +} + +export function getDownloadLogsUrl(dataFetcher: IDataFetcher) { + const fileName = dataFetcher.getDownloadFileName(); + return `${getRawLogsBasePath(dataFetcher)}&type=download&filename=${fileName}.log`; +} diff --git a/app/cdap/components/LogViewer/TopPanel.tsx b/app/cdap/components/LogViewer/TopPanel.tsx index 3006c8cb688..34698ae8bba 100644 --- a/app/cdap/components/LogViewer/TopPanel.tsx +++ b/app/cdap/components/LogViewer/TopPanel.tsx @@ -25,6 +25,8 @@ import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; import Popover from 'components/shared/Popover'; import IconSVG from 'components/shared/IconSVG'; import LoadingSVG from 'components/shared/LoadingSVG'; +import { getDownloadLogsUrl, getRawLogsUrl } from './LogsUrlUtils'; +import RunLogsStatsChips from 'components/PipelineDetails/RunLevelInfo/RunLogsStatsChips'; export const TOP_PANEL_HEIGHT = '50px'; @@ -33,7 +35,7 @@ const styles = (theme): StyleRules => { root: { backgroundColor: theme.palette.grey[900], display: 'flex', - justifyContent: 'flex-end', + justifyContent: 'space-between', alignItems: 'center', height: TOP_PANEL_HEIGHT, paddingLeft: '20px', @@ -41,7 +43,19 @@ const styles = (theme): StyleRules => { position: 'relative', }, loadingContainer: { - marginRight: 'auto', + margin: '0 auto', + }, + leftContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + height: TOP_PANEL_HEIGHT, + }, + rightContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + height: TOP_PANEL_HEIGHT, }, actionButton: { margin: theme.spacing(1), @@ -144,100 +158,97 @@ const TopPanelView: React.FC = ({ dataFetcher.getIncludeSystemLogs() ); - function getRawLogsBasePath() { - const backendUrl = dataFetcher.getRawLogsUrl(); - const encodedBackendUrl = encodeURIComponent(backendUrl); - - const url = `/downloadLogs?backendPath=${encodedBackendUrl}`; - return url; - } - - function getRawLogsUrl() { - return `${getRawLogsBasePath()}&type=raw`; - } - - function getDownloadLogsUrl() { - const fileName = dataFetcher.getDownloadFileName(); - return `${getRawLogsBasePath()}&type=download&filename=${fileName}.log`; - } - function handleToggleSystemLogs() { const newState = !includeSystemLogs; setLocalIncludeSystemLogs(newState); setSystemLogs(newState); } - return ( -
- + if (loading) { + return ( +
- - - -
+
+ ); + } + + return ( +
+
+ +
+
- { - return ( - - ); - }} - modifiers={{ - preventOverflow: { - enabled: true, - boundariesElement: 'viewport', - }, - }} - className={classes.popover} - placement="bottom" - showOn="Click" + +
+ + { + return ( + + ); + }} + modifiers={{ + preventOverflow: { + enabled: true, + boundariesElement: 'viewport', + }, + }} + className={classes.popover} + placement="bottom" + showOn="Click" + > + + View Raw Logs + + +
+ + + + +
- - - - -
); }; diff --git a/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineRunErrorDetails.tsx b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineRunErrorDetails.tsx new file mode 100644 index 00000000000..b5e3d985672 --- /dev/null +++ b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/PipelineRunErrorDetails.tsx @@ -0,0 +1,350 @@ +/* + * Copyright © 2024 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import React, { useEffect, useState } from 'react'; +import T from 'i18n-react'; +import { useSelector, Provider, useDispatch } from 'react-redux'; +import { Button, Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core'; +import LaunchIcon from '@material-ui/icons/Launch'; +import GetAppIcon from '@material-ui/icons/GetApp'; +import styled from 'styled-components'; +import PipelineMetricsStore from 'services/PipelineMetricsStore'; +import PipelineLogViewer from '../RunLevelInfo/PipelineLogViewer'; +import ThemeWrapper from 'components/ThemeWrapper'; +import { ACTIONS } from '../store'; +import { MyPipelineApi } from 'api/pipeline'; +import { getCurrentNamespace } from 'services/NamespaceStore'; +import { getDataTestid } from '@cdap-ui/testids/TestidsProvider'; +import ProgramDataFetcher from 'components/LogViewer/DataFetcher/ProgramDataFetcher'; +import { GLOBALS } from 'services/global-constants'; +import { getDownloadLogsUrl } from 'components/LogViewer/LogsUrlUtils'; +import { delay } from '@cdap-ui/utils/time'; + +const PREFIX = 'features.PipelineDetails.ErrorDetails'; +const TEST_PREFIX = 'features.pipelineDetails.errorDetails'; +const RETRY_DELAY_MS = 10 * 1000; + +const PipelineRunErrorDetailsWrapper = styled.div` + width: 100%; + background: ${({ theme }) => theme.palette.red[100]}; + position: relative; + color: ${({ theme }) => theme.palette.white[50]}; +`; + +const ShortErrorMessage = styled.div` + width: 100%; + display: flex; + padding: 0 20px; + gap: 40px; + align-items: center; + justify-content: space-between; + + p { + margin: 0; + } +`; + +const ErrorDetailsContainer = styled.div` + position: absolute; + top: 100%; + left: 0; + right: 0; + background: ${({ theme }) => theme.palette.red[600]}; + box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2); + z-index: 1000; + padding: 20px; + color: ${({ theme }) => theme.palette.grey[50]}; + + p { + margin: 0; + } + + .LogViewerContainer-logsContainer-60 { + top: 140px; + } + + a[target='_blank'] > svg { + font-size: 10px; + } + + .alt-color-column { + background: ${({ theme }) => theme.palette.grey[600]}; + } + + pre { + width: 40vw; + border: none; + background: transparent; + min-height: 40px; + max-height: 120px; + overflow: auto; + white-space: pre-wrap; + } +`; + +const ErrorImprovementMessage = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + margin-top: 20px; +`; + +const LogsButtonsContainer = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + gap: 20px; +`; + +const TdClassesOverride = { + root: 'alt-color-column', +}; + +const PipelineErrorCountMessage = ({ classifiedErrorCount }) => { + return ( +

+ {T.translate(`${PREFIX}.errorCountMessage`, { + context: classifiedErrorCount || 0, + })} +

+ ); +}; + +interface IErrorEntry { + errorCategory: string; + errorMessage: string; + errorReason: string; + stageName: string; + supportedDocumentationUrl?: string; +} + +export default function PipelineRunErrorDetails() { + const [detailsExpanded, setDetailsExpanded] = useState(false); + const [logsOpened, setLogsOpened] = useState(false); + + const appId = useSelector((state) => state.name); + const artifactName = useSelector((state) => state.artifact.name); + const currentRun = useSelector((state) => state.currentRun); + const runid = currentRun?.runid; + const loading = useSelector((state) => state.runErrorDetailsLoading[runid] || false); + const errorDetails = useSelector((state) => state.runErrorDetails[runid]); + const canExapndErrorDetails = !!errorDetails?.length; + + const dispatch = useDispatch(); + function setLoading(value: boolean) { + dispatch({ + type: ACTIONS.SET_RUN_ERROR_DETAILS_LOADING, + payload: { + runid, + value, + }, + }); + } + + function setErrorDetails(errors: IErrorEntry[]) { + dispatch({ + type: ACTIONS.SET_RUN_ERROR_DETAILS, + payload: { + runid, + errors, + }, + }); + } + + const dataFetcher = new ProgramDataFetcher({ + namespace: getCurrentNamespace(), + application: appId, + programType: GLOBALS.programType[artifactName], + programName: GLOBALS.programId[artifactName], + runId: runid, + }); + + // TODO [CDAP-21109](https://cdap.atlassian.net/browse/CDAP-21109): add feature flag here + const improvementSurveyEnabled = false; + + function toggleErrorDetails() { + setDetailsExpanded((x) => !x); + } + + useEffect(() => { + document.body.classList.add('with-error-banner'); + + return () => { + document.body.classList.remove('with-error-banner'); + }; + }, []); + + function fetchErrorDetails() { + if (loading) { + return; + } + + setLoading(true); + // POST `/namespaces/:namespace/apps/:appid/:programType/:programName/runs/:runid/classify?isPreview` + MyPipelineApi.getRunErrorDetails({ + namespace: getCurrentNamespace(), + appId, + programType: 'workflows', + programName: 'DataPipelineWorkflow', + runid, + }).subscribe( + (res: IErrorEntry[]) => { + setErrorDetails(res); + setLoading(false); + }, + async (err) => { + // In case of API error, wait 10s before retrying + setErrorDetails(null); + await delay(RETRY_DELAY_MS); + setLoading(false); + } + ); + } + + function viewLogs() { + setDetailsExpanded(false); + setLogsOpened(true); + } + + function toggleLogs() { + setLogsOpened((x) => !x); + } + + useEffect(() => { + if (!errorDetails && !loading && currentRun?.status === 'FAILED') { + fetchErrorDetails(); + } + }, [runid, currentRun?.status, errorDetails, loading]); + + function renderWithLink(strToRender, link) { + if (strToRender.indexOf(link) < 0) { + return
{strToRender}
; + } + + const chunks = strToRender.split(link); + const nodes = [chunks[0]]; + for (let i = 1; i < chunks.length; i++) { + nodes.push( + + {link} + + ); + nodes.push(chunks[i]); + } + + return
{nodes}
; + } + + if (currentRun?.status !== 'FAILED' || loading) { + return null; + } + + return ( + + + + + + {canExapndErrorDetails ? ( + + ) : ( + + )} + + {detailsExpanded && ( + + + + + {T.translate(`${PREFIX}.errorCategoryHeader`)} + + {T.translate(`${PREFIX}.errorReasonHeader`)} + + {T.translate(`${PREFIX}.errorMessageHeader`)} + + + + {errorDetails?.map((err) => ( + + {err.errorCategory} + + {renderWithLink(err.errorReason, err.supportedDocumentationUrl)} + + + {renderWithLink(err.errorMessage, err.supportedDocumentationUrl)} + + + ))} + +
+ + {improvementSurveyEnabled ? ( +

+ {/* TODO (CDAP-21109): Add i18n strings */} + Help us improve the error classification. Raise improvement here + . +

+ ) : ( +

+ )} + + + + + + + )} + + + {logsOpened && } + + ); +} diff --git a/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/index.tsx b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/index.tsx index 2c26b694384..78bad6bfdba 100644 --- a/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/index.tsx +++ b/app/cdap/components/PipelineDetails/PipelineDetailsTopPanel/index.tsx @@ -15,6 +15,7 @@ */ import React, { useEffect } from 'react'; +import styled from 'styled-components'; import { Provider, connect } from 'react-redux'; import PipelineDetailsMetadata from 'components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsMetadata'; import PipelineDetailsButtons from 'components/PipelineDetails/PipelineDetailsTopPanel/PipelineDetailsButtons'; @@ -27,6 +28,7 @@ import PlusButton from 'components/shared/PlusButton'; import { fetchAndUpdateRuntimeArgs } from 'components/PipelineConfigurations/Store/ActionCreator'; import { FeatureProvider } from 'services/react/providers/featureFlagProvider'; import { setEditDraftId } from '../store/ActionCreator'; +import PipelineRunErrorDetails from './PipelineRunErrorDetails'; require('./PipelineDetailsTopPanel.scss'); @@ -51,6 +53,12 @@ const mapStateToButtonsProps = (state) => { const ConnectedPipelineDetailsButtons = connect(mapStateToButtonsProps)(PipelineDetailsButtons); +const PipelineDetailsWrapper = styled.div` + width: 100%; + height: 100%; + position: relative; +`; + export const PipelineDetailsTopPanel = () => { useEffect(() => { const pipelineDetailStore = PipelineDetailStore.getState(); @@ -73,15 +81,19 @@ export const PipelineDetailsTopPanel = () => { window.localStorage.removeItem('editDraftId'); } }, []); + return ( -

- - - - -
+ + +
+ + + + +
+
); diff --git a/app/cdap/components/PipelineDetails/RunLevelInfo/PipelineLogViewer/index.tsx b/app/cdap/components/PipelineDetails/RunLevelInfo/PipelineLogViewer/index.tsx index 36f656e47ba..c5616538a1b 100644 --- a/app/cdap/components/PipelineDetails/RunLevelInfo/PipelineLogViewer/index.tsx +++ b/app/cdap/components/PipelineDetails/RunLevelInfo/PipelineLogViewer/index.tsx @@ -15,7 +15,11 @@ */ import * as React from 'react'; -import withStyles, { WithStyles, StyleRules } from '@material-ui/core/styles/withStyles'; +import withStyles, { + WithStyles, + StyleRules, + CreateCSSProperties, +} from '@material-ui/core/styles/withStyles'; import { connect } from 'react-redux'; import { getCurrentNamespace } from 'services/NamespaceStore'; import { GLOBALS } from 'services/global-constants'; @@ -26,23 +30,39 @@ import { PIPELINE_LOGS_FILTER } from 'services/global-constants'; const PIPELINE_TOP_PANEL_OFFSET = '160px'; const FOOTER_HEIGHT = '54px'; +const ERROR_BANNER_OFFSET = '20px'; // to align the top of the logs viewer when the error banner is present +const LOGS_PORTAL_OFFSET = '50px'; // to remove the unnecessary page scroll + const styles = (theme): StyleRules => { + const portalContainerBase = { + position: 'absolute', + top: 0, + left: 0, + height: '100vh', + width: '100vw', + zIndex: 1301, + }; + + const logsContainerBase = { + position: 'absolute', + top: PIPELINE_TOP_PANEL_OFFSET, + height: `calc(100% - ${PIPELINE_TOP_PANEL_OFFSET} - ${FOOTER_HEIGHT})`, + width: '100%', + backgroundColor: theme.palette.white[50], + }; + return { - portalContainer: { - position: 'absolute', - top: 0, - left: 0, - height: '100vh', - width: '100vw', - zIndex: 1301, - }, - logsContainer: { - position: 'absolute', - top: PIPELINE_TOP_PANEL_OFFSET, - height: `calc(100% - ${PIPELINE_TOP_PANEL_OFFSET} - ${FOOTER_HEIGHT})`, - width: '100%', - backgroundColor: theme.palette.white[50], - }, + portalContainer: portalContainerBase as CreateCSSProperties<{}>, + logsContainer: logsContainerBase as CreateCSSProperties<{}>, + portalContainerWithErrorBanner: { + ...portalContainerBase, + height: `calc(100vh - ${LOGS_PORTAL_OFFSET})`, + } as CreateCSSProperties<{}>, + logsContainerWithErrorBanner: { + ...logsContainerBase, + height: `calc(100% - ${PIPELINE_TOP_PANEL_OFFSET} - ${FOOTER_HEIGHT} - ${ERROR_BANNER_OFFSET})`, + top: `calc(${PIPELINE_TOP_PANEL_OFFSET} - ${ERROR_BANNER_OFFSET})`, + } as CreateCSSProperties<{}>, }; }; @@ -53,6 +73,7 @@ interface ILogViewerProps extends WithStyles { appId: string; artifactName: string; toggleLogViewer: () => void; + withErrorBanner?: boolean; } const LogViewerContainer: React.FC = ({ @@ -61,6 +82,7 @@ const LogViewerContainer: React.FC = ({ appId, artifactName, toggleLogViewer, + withErrorBanner = false, }) => { const backgroundElem = React.useRef(null); const [dataFetcher] = React.useState( @@ -85,8 +107,14 @@ const LogViewerContainer: React.FC = ({ } return ( -
-
+
+
diff --git a/app/cdap/components/PipelineDetails/RunLevelInfo/RunLogsStatsChips.tsx b/app/cdap/components/PipelineDetails/RunLevelInfo/RunLogsStatsChips.tsx new file mode 100644 index 00000000000..a17d32a321e --- /dev/null +++ b/app/cdap/components/PipelineDetails/RunLevelInfo/RunLogsStatsChips.tsx @@ -0,0 +1,76 @@ +/* + * Copyright © 2025 Cask Data, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { Provider, useSelector } from 'react-redux'; +import PipelineMetricsStore from 'services/PipelineMetricsStore'; +import T from 'i18n-react'; +import { Chip } from '@material-ui/core'; + +const PREFIX = 'features.PipelineDetails.RunLevel'; + +interface IRunLogsStatsProps { + currentRun: { + starting?: boolean; + }; +} + +const ChipsContainer = styled.div` + display: flex; + align-items: center; + gap: 10px; +`; + +function RunLogsStatsChipsComp({ currentRun }: IRunLogsStatsProps) { + const logsMetrics = useSelector((state) => state?.logsMetrics || {}); + const numErrors = currentRun?.starting ? logsMetrics['system.app.log.error'] || 0 : null; + const numWarnings = currentRun?.starting ? logsMetrics['system.app.log.warn'] || 0 : null; + + return ( + + {!!numWarnings && ( + + )} + {!!numErrors && ( + + )} + + ); +} + +export default function RunLogsStatsChips() { + const currentRun = useSelector((state) => state.currentRun); + if (!currentRun) { + return
; + } + + return ( + + + + ); +} diff --git a/app/cdap/components/PipelineDetails/RunLevelInfo/index.js b/app/cdap/components/PipelineDetails/RunLevelInfo/index.js index 1a604a18a8f..55147e48c8f 100644 --- a/app/cdap/components/PipelineDetails/RunLevelInfo/index.js +++ b/app/cdap/components/PipelineDetails/RunLevelInfo/index.js @@ -43,6 +43,10 @@ const ConnectedRunNumWarnings = connect(mapStateToProps)(RunNumWarnings); const ConnectedRunNumErrors = connect(mapStateToProps)(RunNumErrors); export default function RunLevelInfo() { + // error and warning counts are disabled when error classification is available, + // i.e. post v6.11.0 + const shouldShowErrorAndWarningCounts = false; + return (
@@ -54,8 +58,12 @@ export default function RunLevelInfo() { - - + {shouldShowErrorAndWarningCounts && ( + + + + + )}
diff --git a/app/cdap/components/PipelineDetails/store/index.js b/app/cdap/components/PipelineDetails/store/index.js index 49118344713..b983a024b7d 100644 --- a/app/cdap/components/PipelineDetails/store/index.js +++ b/app/cdap/components/PipelineDetails/store/index.js @@ -36,6 +36,8 @@ const ACTIONS = { SET_USER_RUNTIME_ARGUMENTS: 'SET_USER_RUNTIME_ARGUMENTS', SET_MACROS_AND_USER_RUNTIME_ARGUMENTS: 'SET_MACROS_AND_USER_RUNTIME_ARGUMENTS', SET_RUNTIME_ARGUMENTS_FOR_DISPLAY: 'SET_RUNTIME_ARGUMENTS_FOR_DISPLAY', + SET_RUN_ERROR_DETAILS: 'SET_RUN_ERROR_DETAILS', + SET_RUN_ERROR_DETAILS_LOADING: 'SET_RUN_ERROR_DETAILS_LOADING', // Loading and error states Actions SET_RUN_BUTTON_LOADING: 'SET_RUN_BUTTON_LOADING', @@ -84,6 +86,8 @@ const DEFAULT_PIPELINE_DETAILS = { // `runtimeArgsForDisplay` combines `macrosMap` and `userRuntimeArgumentsMap` objects // to create an object that can be used as a prop to the KeyValuePairs component runtimeArgsForDisplay: {}, + runErrorDetails: {}, + runErrorDetailsLoading: {}, // loading and error states runButtonLoading: false, @@ -229,6 +233,22 @@ const pipelineDetails = (state = DEFAULT_PIPELINE_DETAILS, action = defaultActio ...state, runtimeArgsForDisplay: action.payload.args, }; + case ACTIONS.SET_RUN_ERROR_DETAILS: + return { + ...state, + runErrorDetails: { + ...state.runErrorDetails, + [action.payload.runid]: action.payload.errors, + }, + }; + case ACTIONS.SET_RUN_ERROR_DETAILS_LOADING: + return { + ...state, + runErrorDetailsLoading: { + ...state.runErrorDetailsLoading, + [action.payload.runid]: action.payload.value, + }, + }; case ACTIONS.SET_RUN_BUTTON_LOADING: return { ...state, diff --git a/app/cdap/components/ThemeWrapper/colors.ts b/app/cdap/components/ThemeWrapper/colors.ts index 04eb27b3989..a5e6cdf3211 100644 --- a/app/cdap/components/ThemeWrapper/colors.ts +++ b/app/cdap/components/ThemeWrapper/colors.ts @@ -40,6 +40,7 @@ export const red = { 50: colors.red01, 100: colors.red02, 200: colors.red03, + 600: colors.red06, }; export const bluegrey = { diff --git a/app/cdap/components/layouts/SectionWithPanel.tsx b/app/cdap/components/layouts/SectionWithPanel.tsx index 6054135559f..4d62561284c 100644 --- a/app/cdap/components/layouts/SectionWithPanel.tsx +++ b/app/cdap/components/layouts/SectionWithPanel.tsx @@ -16,7 +16,7 @@ import React, { PropsWithChildren, createContext, useContext, useRef, useState } from 'react'; import styled from 'styled-components'; -import { sleep } from '../../utils/time'; +import { delay } from '../../utils/time'; export type PanelOpeningDirection = 'left' | 'top' | 'right' | 'bottom'; @@ -342,7 +342,7 @@ export default function SectionWithPanel({ dividerRef.current.style[panelOpensFrom] = `${newSize - DIVIDER_SIZE}px`; if (durationInMs) { - await sleep(durationInMs); + await delay(durationInMs); panelWrapperRef.current.style.transition = oldPanelWrapperTransition; dividerRef.current.style.transition = oldDividerTransition; } diff --git a/app/cdap/styles/colors.scss b/app/cdap/styles/colors.scss index e27fd17a1b0..a75a2972a8d 100644 --- a/app/cdap/styles/colors.scss +++ b/app/cdap/styles/colors.scss @@ -57,6 +57,7 @@ $grey-11: #dfe2e9; $red-01: #a40403; $red-02: #d40001; $red-03: #d15668; +$red-06: #ffefef; $yellow-01: #ffba01; $yellow-02: #ffd500; $yellow-02-lighter: transparentize($color: $yellow-02, $amount: 0.7); @@ -119,6 +120,7 @@ $green-06: #4ab63c; red01: $red-01; red02: $red-02; red03: $red-03; + red06: $red-06; yellow01: $yellow-01; yellow02: $yellow-02; diff --git a/app/cdap/testids/testids.yaml b/app/cdap/testids/testids.yaml index 29ef6e64920..0541e5a6fcb 100644 --- a/app/cdap/testids/testids.yaml +++ b/app/cdap/testids/testids.yaml @@ -334,5 +334,11 @@ features: wrangler: pageLink: ~ pipelineDetails: + errorDetails: + closeButton: ~ + downloadLogsButton: ~ + errorCountMessage: ~ + viewDetailsButton: ~ + viewLogsButton: ~ runLevel: currentRunIndex: ~ diff --git a/app/cdap/text/text-en.yaml b/app/cdap/text/text-en.yaml index a24abe84e75..eb47a2c4fb9 100644 --- a/app/cdap/text/text-en.yaml +++ b/app/cdap/text/text-en.yaml @@ -2463,6 +2463,18 @@ features: PipelineDetails: duration: Duration + ErrorDetails: + errorCountMessage: + 0: Pipeline execution failed. + 1: Pipeline execution failed with {context} error. + _: Pipeline execution failed with {context} errors. + closeButton: Close + viewDetailsButton: View details + viewLogsButton: View logs + downloadLogsButton: Download raw logs + errorCategoryHeader: Error category + errorMessageHeader: Error Message + errorReasonHeader: Error reason PipelineRuntimeArgsDropdownBtn: RuntimeArgsTabContent: ProvidedPopover: @@ -2507,6 +2519,8 @@ features: status: Status tooltipRunLimit: Pipeline runs for only the last {runLimit} days are displayed warnings: Warnings + errorsCountChip: 'Errors: {numErrors}' + warningsCountChip: 'Warnings: {numWarnings}' startTime: Start time TopPanel: actions: Actions diff --git a/app/cdap/utils/time.ts b/app/cdap/utils/time.ts index 80b7c8165a7..faa0cc9628d 100644 --- a/app/cdap/utils/time.ts +++ b/app/cdap/utils/time.ts @@ -14,6 +14,6 @@ * the License. */ -export function sleep(durationInMs: number): Promise { +export function delay(durationInMs: number): Promise { return new Promise((resolve) => setTimeout(resolve, durationInMs)); } diff --git a/app/directives/dag-plus/my-dag-ctrl.js b/app/directives/dag-plus/my-dag-ctrl.js index 2ebb85583e2..9cd39936bbf 100644 --- a/app/directives/dag-plus/my-dag-ctrl.js +++ b/app/directives/dag-plus/my-dag-ctrl.js @@ -36,6 +36,7 @@ angular.module(PKG.name + '.commons') vm.isDisabled = $scope.isDisabled; vm.disableNodeClick = $scope.disableNodeClick; + vm.errorStages = $scope.errorStages || []; var metricsPopovers = {}; var selectedConnections = []; @@ -1806,6 +1807,10 @@ angular.module(PKG.name + '.commons') vm.pipelineComments = comments; }; + vm.isErrorStage = (node) => { + return vm.errorStages.indexOf(node.name) !== -1; + } + $scope.$on('$destroy', cleanupOnDestroy); vm.initPipelineComments(); @@ -1822,4 +1827,8 @@ angular.module(PKG.name + '.commons') vm.initPipelineComments(); } }, true); + + $scope.$watch('errorStages', function() { + vm.errorStages = $scope.errorStages; + }, true); }); diff --git a/app/directives/dag-plus/my-dag.html b/app/directives/dag-plus/my-dag.html index fb3cce7088a..9adfe79f20b 100644 --- a/app/directives/dag-plus/my-dag.html +++ b/app/directives/dag-plus/my-dag.html @@ -156,6 +156,7 @@ 'selected': DAGPlusPlusCtrl.isNodeSelected(node.id) || DAGPlusPlusCtrl.activePluginToComment == node.id, }" > +
err.stageName); } }); diff --git a/app/hydrator/templates/detail/canvas.html b/app/hydrator/templates/detail/canvas.html index 4a8a1e7cc15..5da89eab900 100644 --- a/app/hydrator/templates/detail/canvas.html +++ b/app/hydrator/templates/detail/canvas.html @@ -24,5 +24,6 @@ metrics-popover-template="/assets/features/hydrator/templates/partial/metrics-popover.html" disable-metrics-click="CanvasCtrl.totalRuns > 0 ? false : true" run-id="CanvasCtrl.runId" + error-stages="CanvasCtrl.errorStages" >