Skip to content

Commit 72b47bf

Browse files
harshithmullapudicgardenstimroesTim Roes
authored
feat: added a new feature to copy all the job info (#10130)
* feat: added a new feature to copy all the job info * feat: added more context to attempt details like (committed/emitted records, failure reason) * Update airbyte-webapp/src/components/JobItem/JobItem.tsx Co-authored-by: Charles <[email protected]> * fix: text and logic * Update airbyte-webapp/src/components/JobItem/JobItem.tsx Co-authored-by: Tim Roes <[email protected]> * Update airbyte-webapp/src/components/JobItem/components/AttemptDetails.tsx Co-authored-by: Tim Roes <[email protected]> * Update airbyte-webapp/src/components/JobItem/components/AttemptDetails.tsx Co-authored-by: Tim Roes <[email protected]> * Update airbyte-webapp/src/components/JobItem/components/DebugInfoButton.tsx Co-authored-by: Tim Roes <[email protected]> * Update airbyte-webapp/src/components/JobItem/components/MainInfo.tsx Co-authored-by: Tim Roes <[email protected]> * Update airbyte-webapp/src/components/JobItem/components/DebugInfoDetailsModal.tsx Co-authored-by: Tim Roes <[email protected]> * fix: changes requested * fix: changes requested * Fix copy Co-authored-by: Charles <[email protected]> Co-authored-by: Tim Roes <[email protected]> Co-authored-by: Tim Roes <[email protected]>
1 parent 8749c7a commit 72b47bf

File tree

17 files changed

+432
-38
lines changed

17 files changed

+432
-38
lines changed

airbyte-webapp/src/components/JobItem/JobItem.tsx

+21-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import styled from "styled-components";
33

44
import { Spinner } from "components";
55

6-
import { JobInfo, JobListItem, Logs } from "core/domain/job/Job";
6+
import { JobInfo, JobListItem, Logs, Attempt } from "core/domain/job/Job";
77
import Status from "core/statuses";
88

99
import JobLogs from "./components/JobLogs";
@@ -45,6 +45,17 @@ const JobCurrentLogs: React.FC<{
4545
return <LogsDetails {...props} path={path} />;
4646
};
4747

48+
const isPartialSuccessCheck = (attempts: Attempt[]) => {
49+
if (attempts[attempts.length - 1].status === Status.FAILED) {
50+
return attempts.some(
51+
(attempt) =>
52+
attempt.failureSummary && attempt.failureSummary.partialSuccess
53+
);
54+
} else {
55+
return false;
56+
}
57+
};
58+
4859
type IProps = {
4960
shortInfo?: boolean;
5061
} & ({ job: JobListItem } | { jobInfo: JobInfo });
@@ -55,13 +66,17 @@ const JobItem: React.FC<IProps> = ({ shortInfo, ...props }) => {
5566

5667
const jobMeta = isJobEntity(props) ? props.job.job : props.jobInfo;
5768
const isFailed = jobMeta.status === Status.FAILED;
69+
const isPartialSuccess = isJobEntity(props)
70+
? isPartialSuccessCheck(props.job.attempts)
71+
: undefined;
5872

5973
return (
6074
<Item isFailed={isFailed}>
6175
<MainInfo
6276
shortInfo={shortInfo}
6377
isOpen={isOpen}
6478
isFailed={isFailed}
79+
isPartialSuccess={isPartialSuccess}
6580
onExpand={onExpand}
6681
job={jobMeta}
6782
attempts={isJobEntity(props) ? props.job.attempts : undefined}
@@ -77,7 +92,11 @@ const JobItem: React.FC<IProps> = ({ shortInfo, ...props }) => {
7792
>
7893
{isOpen ? (
7994
isJobEntity(props) ? (
80-
<JobLogs id={jobMeta.id} jobIsFailed={isFailed} />
95+
<JobLogs
96+
id={jobMeta.id}
97+
jobIsFailed={isFailed}
98+
isPartialSuccess={isPartialSuccess}
99+
/>
81100
) : (
82101
<JobCurrentLogs
83102
id={jobMeta.id}

airbyte-webapp/src/components/JobItem/components/AttemptDetails.tsx

+79-24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { FormattedMessage } from "react-intl";
2+
import { FormattedMessage, useIntl } from "react-intl";
33
import styled from "styled-components";
44
import dayjs from "dayjs";
55

@@ -17,12 +17,21 @@ const Details = styled.div`
1717
line-height: 15px;
1818
color: ${({ theme }) => theme.greyColor40};
1919
`;
20+
const FailureReasonDetails = styled.div`
21+
padding-bottom: 10px;
22+
`;
23+
24+
const getFailureFromAttempt = (attempt: Attempt) => {
25+
return attempt.failureSummary && attempt.failureSummary.failures[0];
26+
};
2027

2128
const AttemptDetails: React.FC<IProps> = ({
2229
attempt,
2330
className,
2431
configType,
2532
}) => {
33+
const { formatMessage } = useIntl();
34+
2635
if (attempt.status !== Status.SUCCEEDED && attempt.status !== Status.FAILED) {
2736
return (
2837
<Details className={className}>
@@ -55,42 +64,88 @@ const AttemptDetails: React.FC<IProps> = ({
5564
);
5665
};
5766

67+
const getFailureOrigin = (attempt: Attempt) => {
68+
const failure = getFailureFromAttempt(attempt);
69+
const failureOrigin =
70+
failure?.failureOrigin ?? formatMessage({ id: "errorView.unknown" });
71+
72+
return `${formatMessage({
73+
id: "sources.failureOrigin",
74+
})}: ${failureOrigin}`;
75+
};
76+
77+
const getFailureMessage = (attempt: Attempt) => {
78+
const failure = getFailureFromAttempt(attempt);
79+
const failureMessage =
80+
failure?.externalMessage ?? formatMessage({ id: "errorView.unknown" });
81+
82+
return `${formatMessage({
83+
id: "sources.message",
84+
})}: ${failureMessage}`;
85+
};
86+
5887
const date1 = dayjs(attempt.createdAt * 1000);
5988
const date2 = dayjs(attempt.updatedAt * 1000);
6089
const hours = Math.abs(date2.diff(date1, "hour"));
6190
const minutes = Math.abs(date2.diff(date1, "minute")) - hours * 60;
6291
const seconds =
6392
Math.abs(date2.diff(date1, "second")) - minutes * 60 - hours * 3600;
93+
const isFailed = attempt.status === Status.FAILED;
6494

6595
return (
6696
<Details className={className}>
67-
<span>{formatBytes(attempt.bytesSynced)} | </span>
68-
<span>
69-
<FormattedMessage
70-
id="sources.countRecords"
71-
values={{ count: attempt.recordsSynced }}
72-
/>{" "}
73-
|{" "}
74-
</span>
75-
<span>
76-
{hours ? (
77-
<FormattedMessage id="sources.hour" values={{ hour: hours }} />
78-
) : null}
79-
{hours || minutes ? (
80-
<FormattedMessage id="sources.minute" values={{ minute: minutes }} />
81-
) : null}
82-
<FormattedMessage id="sources.second" values={{ second: seconds }} />
83-
</span>
84-
{configType ? (
97+
<div>
98+
<span>{formatBytes(attempt.bytesSynced)} | </span>
8599
<span>
86-
{" "}
100+
<FormattedMessage
101+
id="sources.countEmittedRecords"
102+
values={{ count: attempt.totalStats?.recordsEmitted || 0 }}
103+
/>{" "}
87104
|{" "}
105+
</span>
106+
<span>
88107
<FormattedMessage
89-
id={`sources.${configType}`}
90-
defaultMessage={configType}
91-
/>
108+
id="sources.countCommittedRecords"
109+
values={{ count: attempt.totalStats?.recordsCommitted || 0 }}
110+
/>{" "}
111+
|{" "}
92112
</span>
93-
) : null}
113+
<span>
114+
{hours ? (
115+
<FormattedMessage id="sources.hour" values={{ hour: hours }} />
116+
) : null}
117+
{hours || minutes ? (
118+
<FormattedMessage
119+
id="sources.minute"
120+
values={{ minute: minutes }}
121+
/>
122+
) : null}
123+
<FormattedMessage id="sources.second" values={{ second: seconds }} />
124+
</span>
125+
{configType ? (
126+
<span>
127+
{" "}
128+
|{" "}
129+
<FormattedMessage
130+
id={`sources.${configType}`}
131+
defaultMessage={configType}
132+
/>
133+
</span>
134+
) : null}
135+
</div>
136+
{isFailed && (
137+
<FailureReasonDetails>
138+
{formatMessage(
139+
{
140+
id: "ui.keyValuePairV3",
141+
},
142+
{
143+
key: getFailureOrigin(attempt),
144+
value: getFailureMessage(attempt),
145+
}
146+
)}
147+
</FailureReasonDetails>
148+
)}
94149
</Details>
95150
);
96151
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useState } from "react";
2+
import { useIntl } from "react-intl";
3+
import styled from "styled-components";
4+
5+
import DebugInfoDetailsModal from "./DebugInfoDetailsModal";
6+
7+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
8+
import { faFileAlt } from "@fortawesome/free-solid-svg-icons";
9+
10+
import { Button } from "components";
11+
12+
import { JobDebugInfoMeta } from "core/domain/job";
13+
14+
const DebugInfo = styled(Button)`
15+
position: absolute;
16+
top: 9px;
17+
right: 64px;
18+
`;
19+
20+
type IProps = {
21+
jobDebugInfo: JobDebugInfoMeta;
22+
};
23+
24+
const DebugInfoButton: React.FC<IProps> = ({ jobDebugInfo }) => {
25+
const { formatMessage } = useIntl();
26+
const [isModalOpen, setIsModalOpen] = useState(false);
27+
28+
return (
29+
<>
30+
<DebugInfo
31+
onClick={() => setIsModalOpen(true)}
32+
secondary
33+
title={formatMessage({
34+
id: "sources.debugInfoDetails",
35+
})}
36+
>
37+
<FontAwesomeIcon icon={faFileAlt} />
38+
</DebugInfo>
39+
{isModalOpen && (
40+
<DebugInfoDetailsModal
41+
jobDebugInfo={jobDebugInfo}
42+
onClose={() => setIsModalOpen(false)}
43+
/>
44+
)}
45+
</>
46+
);
47+
};
48+
49+
export default DebugInfoButton;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
import { useIntl, FormattedMessage } from "react-intl";
4+
5+
import Modal from "components/Modal";
6+
import { Button } from "components";
7+
8+
import { JobDebugInfoMeta } from "core/domain/job";
9+
10+
export type IProps = {
11+
onClose: () => void;
12+
jobDebugInfo: JobDebugInfoMeta;
13+
};
14+
15+
const Content = styled.div`
16+
padding: 18px 37px 28px;
17+
font-size: 14px;
18+
line-height: 28px;
19+
max-width: 585px;
20+
`;
21+
const ButtonContent = styled.div`
22+
padding-top: 27px;
23+
text-align: right;
24+
`;
25+
const Section = styled.div`
26+
text-align: right;
27+
display: flex;
28+
`;
29+
const ButtonWithMargin = styled(Button)`
30+
margin-right: 9px;
31+
`;
32+
33+
const DebugInfoDetailsModal: React.FC<IProps> = ({ onClose, jobDebugInfo }) => {
34+
const { formatMessage } = useIntl();
35+
36+
const getAirbyteVersion = () => {
37+
return formatMessage(
38+
{
39+
id: "ui.keyValuePair",
40+
},
41+
{
42+
key: formatMessage({ id: "sources.airbyteVersion" }),
43+
value: jobDebugInfo.airbyteVersion,
44+
}
45+
);
46+
};
47+
48+
const getSourceDetails = () => {
49+
const sourceDetails = formatMessage(
50+
{
51+
id: "ui.keyValuePairV2",
52+
},
53+
{
54+
key: jobDebugInfo.sourceDefinition.name,
55+
value: jobDebugInfo.sourceDefinition.dockerImageTag,
56+
}
57+
);
58+
59+
return formatMessage(
60+
{
61+
id: "ui.keyValuePair",
62+
},
63+
{
64+
key: formatMessage({ id: "connector.source" }),
65+
value: sourceDetails,
66+
}
67+
);
68+
};
69+
70+
const getDestinationDetails = () => {
71+
const destinationDetails = formatMessage(
72+
{
73+
id: "ui.keyValuePairV2",
74+
},
75+
{
76+
key: jobDebugInfo.destinationDefinition.name,
77+
value: jobDebugInfo.destinationDefinition.dockerImageTag,
78+
}
79+
);
80+
81+
return formatMessage(
82+
{
83+
id: "ui.keyValuePair",
84+
},
85+
{
86+
key: formatMessage({ id: "connector.destination" }),
87+
value: destinationDetails,
88+
}
89+
);
90+
};
91+
92+
const onCopyClick = () => {
93+
const airbyteVersionDetails = getAirbyteVersion();
94+
const sourceDetails = getSourceDetails();
95+
const destinationDetails = getDestinationDetails();
96+
97+
navigator.clipboard.writeText(
98+
[airbyteVersionDetails, sourceDetails, destinationDetails].join("\n")
99+
);
100+
};
101+
102+
return (
103+
<Modal
104+
onClose={onClose}
105+
title={formatMessage({
106+
id: "sources.debugInfoModalTitle",
107+
})}
108+
>
109+
<Content>
110+
<Section>{getAirbyteVersion()}</Section>
111+
<Section>{getSourceDetails()}</Section>
112+
<Section>{getDestinationDetails()}</Section>
113+
<ButtonContent>
114+
<ButtonWithMargin onClick={onClose} secondary>
115+
<FormattedMessage id="form.cancel" />
116+
</ButtonWithMargin>
117+
<Button onClick={onCopyClick}>
118+
<FormattedMessage id="sources.copyText" />
119+
</Button>
120+
</ButtonContent>
121+
</Content>
122+
</Modal>
123+
);
124+
};
125+
126+
export default DebugInfoDetailsModal;

0 commit comments

Comments
 (0)