- );
- }
}
TaskRoleContainerList.contextType = Context;
TaskRoleContainerList.propTypes = {
- className: PropTypes.string,
- style: PropTypes.object,
- taskInfo: PropTypes.object,
- showDebugInfo: PropTypes.bool,
+ taskRoleName: PropTypes.string,
+ tasks: PropTypes.arrayOf(PropTypes.object),
+ showMoreDiagnostics: PropTypes.bool,
+ jobAttemptIndex: PropTypes.number,
};
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-count.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-count.jsx
new file mode 100644
index 0000000000..dbfdc6fe4e
--- /dev/null
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-count.jsx
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import c from 'classnames';
+import { capitalize } from 'lodash';
+import { TooltipHost, Text } from 'office-ui-fabric-react';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import t from '../../../../../components/tachyons.scss';
+
+import { statusColor } from '../../../../../components/theme';
+
+const TaskRoleCount = ({ taskInfo }) => {
+ const count = {
+ running: 0,
+ waiting: 0,
+ succeeded: 0,
+ failed: 0,
+ stopped: 0,
+ unknown: 0,
+ };
+ if (taskInfo && taskInfo.taskStatuses) {
+ for (const item of taskInfo.taskStatuses) {
+ switch (item.taskState) {
+ case 'RUNNING':
+ count.running += 1;
+ break;
+ case 'WAITING':
+ case 'STOPPING':
+ count.waiting += 1;
+ break;
+ case 'SUCCEEDED':
+ count.succeeded += 1;
+ break;
+ case 'FAILED':
+ count.failed += 1;
+ break;
+ case 'STOPPED':
+ count.stopped += 1;
+ break;
+ default:
+ count.unknown += 1;
+ break;
+ }
+ }
+ } else {
+ // task status info not available
+ return;
+ }
+
+ return (
+
+ );
+};
+
+TaskRoleCount.propTypes = {
+ taskInfo: PropTypes.object.isRequired,
+};
+
+export default TaskRoleCount;
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role.jsx
deleted file mode 100644
index 90d13d0682..0000000000
--- a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role.jsx
+++ /dev/null
@@ -1,223 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// All rights reserved.
-//
-// MIT License
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
-// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
-// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import { FontClassNames, ColorClassNames, getTheme } from '@uifabric/styling';
-import c from 'classnames';
-import { capitalize } from 'lodash';
-import {
- Icon,
- IconButton,
- TooltipHost,
- Toggle,
- Stack,
-} from 'office-ui-fabric-react';
-import PropTypes from 'prop-types';
-import React from 'react';
-import yaml from 'js-yaml';
-
-import t from '../../../../../components/tachyons.scss';
-
-import Card from './card';
-import Context from './context';
-import TaskRoleContainerList from './task-role-container-list';
-import { getTaskConfig } from '../util';
-import MonacoCallout from '../../../../../components/monaco-callout';
-import { statusColor } from '../../../../../components/theme';
-
-const TaskRoleCount = ({ taskInfo }) => {
- const count = {
- running: 0,
- waiting: 0,
- succeeded: 0,
- failed: 0,
- stopped: 0,
- unknown: 0,
- };
- if (taskInfo && taskInfo.taskStatuses) {
- for (const item of taskInfo.taskStatuses) {
- switch (item.taskState) {
- case 'RUNNING':
- count.running += 1;
- break;
- case 'WAITING':
- case 'STOPPING':
- count.waiting += 1;
- break;
- case 'SUCCEEDED':
- count.succeeded += 1;
- break;
- case 'FAILED':
- count.failed += 1;
- break;
- case 'STOPPED':
- count.stopped += 1;
- break;
- default:
- count.unknown += 1;
- break;
- }
- }
- } else {
- // task status info not available
- return;
- }
-
- return (
-
- );
- }
-}
-
-TaskRole.contextType = Context;
-
-TaskRole.propTypes = {
- className: PropTypes.string,
- name: PropTypes.string.isRequired,
- taskInfo: PropTypes.object.isRequired,
- isFailed: PropTypes.bool,
-};
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/top.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/top.jsx
index 0a1c5eeb38..867d607c2c 100644
--- a/src/webportal/src/app/job/job-view/fabric/job-detail/components/top.jsx
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/top.jsx
@@ -27,7 +27,7 @@ const Top = () => (
iconProps={{ iconName: 'revToggleKey' }}
href='/job-list.html'
>
- Back to Jobs
+ Go to Jobs List
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/conn.js b/src/webportal/src/app/job/job-view/fabric/job-detail/conn.js
index 55a1c23d91..dc0561807d 100644
--- a/src/webportal/src/app/job/job-view/fabric/job-detail/conn.js
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail/conn.js
@@ -43,55 +43,34 @@ const wrapper = async func => {
}
};
-export async function checkAttemptAPI() {
+export async function fetchJobInfo(attemptIndex) {
return wrapper(async () => {
- try {
- await client.jobHistory.getJobAttemptsHealthz(userName, jobName);
- return true;
- } catch {
- return false;
- }
+ const restServerUri = new URL(config.restServerUri, window.location.href);
+ const url = isNil(attemptIndex)
+ ? `${restServerUri}/api/v2/jobs/${userName}~${jobName}`
+ : `${restServerUri}/api/v2/jobs/${userName}~${jobName}/attempts/${attemptIndex}`;
+ const res = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ const result = await res.json();
+ return result;
});
}
-export async function fetchJobRetries() {
- if (!(await checkAttemptAPI())) {
- return {
- isSucceeded: false,
- errorMessage: 'Attempts API is not working!',
- jobRetries: null,
- };
- }
-
- try {
- const jobAttempts = await client.jobHistory.getJobAttempts(
- userName,
- jobName,
- );
- return {
- isSucceeded: true,
- errorMessage: null,
- jobRetries: jobAttempts.filter(attempt => !attempt.isLatest),
- };
- } catch (err) {
- if (err.status === 404) {
- return {
- isSucceeded: false,
- errorMessage: 'Could not find any attempts of this job!',
- jobRetries: null,
- };
- } else {
- return {
- isSucceeded: false,
- errorMessage: 'Some errors occurred!',
- jobRetries: null,
- };
- }
- }
-}
-
-export async function fetchJobInfo() {
- return wrapper(() => client.job.getJob(userName, jobName));
+export async function fetchTaskStatus(attemptIndex, taskRoleName, taskIndex) {
+ return wrapper(async () => {
+ const restServerUri = new URL(config.restServerUri, window.location.href);
+ const url = `${restServerUri}/api/v2/jobs/${userName}~${jobName}/attempts/${attemptIndex}/taskRoles/${taskRoleName}/taskIndex/${taskIndex}/attempts`;
+ const res = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ const result = await res.json();
+ return result;
+ });
}
export async function fetchRawJobConfig() {
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/util.js b/src/webportal/src/app/job/job-view/fabric/job-detail/util.js
index 9f25e6225c..7c2d0d6d60 100644
--- a/src/webportal/src/app/job/job-view/fabric/job-detail/util.js
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail/util.js
@@ -23,27 +23,14 @@ export function printDateTime(dt) {
dt > DateTime.utc().minus({ week: 1 }) &&
dt < DateTime.utc().minus({ minute: 1 })
) {
- return `${dt.toRelative()}, ${dt.toLocaleString({
- hour: '2-digit',
- minute: '2-digit',
- hourCycle: 'h23',
- })}`;
+ return `${dt.toRelative()}, ${dt.toLocaleString(
+ DateTime.TIME_WITH_SECONDS,
+ )}`;
} else {
- return dt.toLocaleString(DateTime.DATETIME_MED);
+ return dt.toLocaleString(DateTime.DATETIME_MED_WITH_SECONDS);
}
}
-export function parseGpuAttr(attr) {
- const res = [];
- for (let i = 0; attr !== 0; i++, attr >>= 1) {
- if ((attr & 1) === 1) {
- res.push(i);
- }
- }
-
- return res;
-}
-
export function isJobV2(rawJobConfig) {
return (
!isNil(rawJobConfig.protocol_version) ||
@@ -74,8 +61,3 @@ export function getTaskConfig(rawJobConfig, name) {
}
return null;
}
-
-export const HISTORY_DISABLE_MESSAGE =
- 'The job history was not enabled when deploying.';
-export const HISTORY_API_ERROR_MESSAGE =
- 'The job history API is not healthy right now.';
diff --git a/src/webportal/src/app/job/job-view/fabric/job-retry.jsx b/src/webportal/src/app/job/job-view/fabric/job-retry.jsx
deleted file mode 100644
index 8eacb614dd..0000000000
--- a/src/webportal/src/app/job/job-view/fabric/job-retry.jsx
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// All rights reserved.
-//
-// MIT License
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
-// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
-// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import { isNil } from 'lodash';
-import { Fabric, Stack, getTheme } from 'office-ui-fabric-react';
-import React, { useEffect, useState } from 'react';
-import ReactDOM from 'react-dom';
-
-import Top from './job-retry/top';
-import { SpinnerLoading } from '../../../components/loading';
-import { JobRetryCard } from './job-retry/job-retry-card';
-import { fetchJobRetries } from './job-detail/conn';
-const { spacing } = getTheme();
-
-const JobRetryPage = () => {
- const [loading, setLoading] = useState(true);
- const [jobRetries, setJobRetries] = useState(null);
-
- useEffect(() => {
- reload(true);
- }, []);
-
- const reload = async alertFlag => {
- let errorMessage;
- try {
- const result = await fetchJobRetries();
- if (result.isSucceeded) {
- setJobRetries(result.jobRetries);
- } else {
- errorMessage = result.errorMessage;
- }
- } catch (err) {
- errorMessage = `fetch job status failed: ${err.message}`;
- }
- if (alertFlag === true && !isNil(errorMessage)) {
- alert(errorMessage);
- }
- setLoading(false);
- };
-
- return (
-
- {loading && }
- {!loading && (
-
-
-
- {jobRetries.map(jobRetry => {
- return (
-
- );
- })}
-
-
- )}
-
- );
-};
-
-ReactDOM.render(, document.getElementById('content-wrapper'));
diff --git a/src/webportal/src/app/job/job-view/fabric/job-retry/container-list.jsx b/src/webportal/src/app/job/job-view/fabric/job-retry/container-list.jsx
deleted file mode 100644
index fb3a9188f8..0000000000
--- a/src/webportal/src/app/job/job-view/fabric/job-retry/container-list.jsx
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// All rights reserved.
-//
-// MIT License
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
-// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
-// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import { FontClassNames, getTheme } from '@uifabric/styling';
-import c from 'classnames';
-import { capitalize, isNil } from 'lodash';
-import { Link } from 'office-ui-fabric-react';
-import {
- DetailsList,
- SelectionMode,
- DetailsListLayoutMode,
-} from 'office-ui-fabric-react/lib/DetailsList';
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import t from '../../../../components/tachyons.scss';
-
-import StatusBadge from '../../../../components/status-badge';
-
-const { palette } = getTheme();
-
-export const ContainerList = ({ taskStatuses }) => {
- const columns = [
- {
- key: 'number',
- name: 'No.',
- headerClassName: FontClassNames.medium,
- minWidth: 50,
- maxWidth: 50,
- isResizable: true,
- onRender: (item, idx) => {
- return (
- !isNil(idx) &&
- );
-};
-
-ContainerList.propTypes = {
- taskStatuses: PropTypes.arrayOf(PropTypes.object),
-};
diff --git a/src/webportal/src/app/job/job-view/fabric/job-retry/job-retry-card.jsx b/src/webportal/src/app/job/job-view/fabric/job-retry/job-retry-card.jsx
deleted file mode 100644
index 0b0760cae9..0000000000
--- a/src/webportal/src/app/job/job-view/fabric/job-retry/job-retry-card.jsx
+++ /dev/null
@@ -1,313 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// All rights reserved.
-//
-// MIT License
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
-// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
-// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import { FontClassNames, ColorClassNames, getTheme } from '@uifabric/styling';
-import c from 'classnames';
-import { Stack, IconButton, Link } from 'office-ui-fabric-react';
-import PropTypes from 'prop-types';
-import React, { useState } from 'react';
-import { Interval, DateTime } from 'luxon';
-import { capitalize, isNil, get } from 'lodash';
-import styled from 'styled-components';
-import yaml from 'js-yaml';
-
-import { getDurationString } from '../../../../components/util/job';
-import StatusBadge from '../../../../components/status-badge';
-import { ContainerList } from './container-list';
-import { printDateTime } from '../job-detail/util';
-import MonacoPanel from '../../../../components/monaco-panel';
-
-const { spacing, palette } = getTheme();
-
-function getAttemptDurationString(attempt) {
- const start =
- attempt.attemptStartedTime &&
- DateTime.fromMillis(attempt.attemptStartedTime);
- const end = attempt.attemptCompletedTime
- ? DateTime.fromMillis(attempt.attemptCompletedTime)
- : DateTime.utc();
- if (start && end) {
- return getDurationString(
- Interval.fromDateTimes(start, end || DateTime.utc()).toDuration([
- 'days',
- 'hours',
- 'minutes',
- 'seconds',
- ]),
- );
- } else {
- return 'N/A';
- }
-}
-
-const RetryCard = styled.div`
- background: #f8f8f8;
- box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px, rgba(0, 0, 0, 0.05) 0px 0.5px 1px;
-`;
-
-const TaskRoleCard = styled.div`
- padding: ${spacing.l1};
- background: ${palette.white};
- box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px, rgba(0, 0, 0, 0.05) 0px 0.5px 1px;
-`;
-
-const TaskRole = ({ name, taskrole }) => {
- const [isExpanded, setIsExpanded] = useState(false);
- return (
-
-
-
-
-
-
- {Object.keys(jobRetry.taskRoles).map(name => (
-
- ))}
-
-
-
-
- );
-};
-
-JobRetryCard.propTypes = {
- jobRetry: PropTypes.object,
-};
diff --git a/src/webportal/src/app/job/job-view/fabric/job-retry/top.jsx b/src/webportal/src/app/job/job-view/fabric/job-retry/top.jsx
deleted file mode 100644
index 237969d62c..0000000000
--- a/src/webportal/src/app/job/job-view/fabric/job-retry/top.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// All rights reserved.
-//
-// MIT License
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
-// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
-// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
-// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
-// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-import React from 'react';
-import { Stack, ActionButton } from 'office-ui-fabric-react';
-
-const params = new URLSearchParams(window.location.search);
-const username = params.get('username');
-const jobName = params.get('jobName');
-
-const Top = () => (
-
-
-
- Back to Job Detail
-
-
-
-);
-
-export default Top;
diff --git a/src/webportal/src/app/job/job-view/fabric/task-attempt.jsx b/src/webportal/src/app/job/job-view/fabric/task-attempt.jsx
new file mode 100644
index 0000000000..fab668a1c1
--- /dev/null
+++ b/src/webportal/src/app/job/job-view/fabric/task-attempt.jsx
@@ -0,0 +1,176 @@
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import React, { useEffect, useState } from 'react';
+import { Stack, ActionButton, Text } from 'office-ui-fabric-react';
+import ReactDOM from 'react-dom';
+import { isNil, capitalize } from 'lodash';
+import { DateTime, Interval } from 'luxon';
+
+import { SpinnerLoading } from '../../../components/loading';
+import TaskAttemptList from './task-attempt/task-attempt-list';
+import { fetchTaskStatus } from './task-attempt/conn';
+import StatusBadge from '../../../components/status-badge';
+import { getDurationString } from '../../../components/util/job';
+import Card from './job-detail/components/card';
+import HorizontalLine from '../../../components/horizontal-line';
+
+const params = new URLSearchParams(window.location.search);
+const userName = params.get('username');
+const jobName = params.get('jobName');
+const jobAttemptIndex = params.get('jobAttemptIndex');
+const taskRoleName = params.get('taskRoleName');
+const taskIndex = params.get('taskIndex');
+
+const TaskAttemptPage = () => {
+ const [loading, setLoading] = useState(true);
+ const [taskStatus, setTaskStatus] = useState(null);
+
+ const getTimeDuration = (startMs, endMs) => {
+ const start = startMs && DateTime.fromMillis(startMs);
+ const end = endMs && DateTime.fromMillis(endMs);
+ if (start) {
+ return Interval.fromDateTimes(start, end || DateTime.utc()).toDuration([
+ 'days',
+ 'hours',
+ 'minutes',
+ 'seconds',
+ ]);
+ } else {
+ return null;
+ }
+ };
+
+ useEffect(() => {
+ fetchTaskStatus(
+ userName,
+ jobName,
+ jobAttemptIndex,
+ taskRoleName,
+ taskIndex,
+ ).then(data => {
+ setTaskStatus(data);
+ setLoading(false);
+ });
+ }, []);
+
+ return (
+
+ );
+};
+
+ReactDOM.render(
+ ,
+ document.getElementById('content-wrapper'),
+);
diff --git a/src/webportal/src/app/job/job-view/fabric/task-attempt/conn.js b/src/webportal/src/app/job/job-view/fabric/task-attempt/conn.js
new file mode 100644
index 0000000000..72909ca78b
--- /dev/null
+++ b/src/webportal/src/app/job/job-view/fabric/task-attempt/conn.js
@@ -0,0 +1,126 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { clearToken } from '../../../../user/user-logout/user-logout.component';
+import config from '../../../../config/webportal.config';
+const absoluteUrlRegExp = /^[a-z][a-z\d+.-]*:/;
+
+const token = cookies.get('token');
+
+export class NotFoundError extends Error {
+ constructor(msg) {
+ super(msg);
+ this.name = 'NotFoundError';
+ }
+}
+
+const wrapper = async func => {
+ try {
+ return await func();
+ } catch (err) {
+ if (err.data.code === 'UnauthorizedUserError') {
+ alert(err.data.message);
+ clearToken();
+ } else if (err.data.code === 'NoJobConfigError') {
+ throw new NotFoundError(err.data.message);
+ } else {
+ throw new Error(err.data.message);
+ }
+ }
+};
+
+export async function fetchTaskStatus(
+ userName,
+ jobName,
+ attemptIndex,
+ taskRoleName,
+ taskIndex,
+) {
+ return wrapper(async () => {
+ const restServerUri = new URL(config.restServerUri, window.location.href);
+ const url = `${restServerUri}/api/v2/jobs/${userName}~${jobName}/attempts/${attemptIndex}/taskRoles/${taskRoleName}/taskIndex/${taskIndex}/attempts`;
+ const res = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ const result = await res.json();
+ return result;
+ });
+}
+
+export async function getContainerLog(logUrl) {
+ const ret = {
+ fullLogLink: logUrl,
+ text: null,
+ };
+ const res = await fetch(logUrl);
+ var text = await res.text();
+ if (!res.ok) {
+ throw new Error(res.statusText);
+ }
+
+ const contentType = res.headers.get('content-type');
+ if (!contentType) {
+ throw new Error(`Log not available`);
+ }
+
+ // Check log type. The log type is in LOG_TYPE and should be yarn|log-manager.
+ if (config.logType === 'yarn') {
+ try {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(text, 'text/html');
+ const content = doc.getElementsByClassName('content')[0];
+ const pre = content.getElementsByTagName('pre')[0];
+ ret.text = pre.innerText;
+ // fetch full log link
+ if (pre.previousElementSibling) {
+ const link = pre.previousElementSibling.getElementsByTagName('a');
+ if (link.length === 1) {
+ ret.fullLogLink = link[0].getAttribute('href');
+ // relative link
+ if (ret.fullLogLink && !absoluteUrlRegExp.test(ret.fullLogLink)) {
+ let baseUrl = res.url;
+ // check base tag
+ const baseTags = doc.getElementsByTagName('base');
+ // There can be only one element in a document.
+ if (baseTags.length > 0 && baseTags[0].hasAttribute('href')) {
+ baseUrl = baseTags[0].getAttribute('href');
+ // relative base tag url
+ if (!absoluteUrlRegExp.test(baseUrl)) {
+ baseUrl = new URL(baseUrl, res.url);
+ }
+ }
+ const url = new URL(ret.fullLogLink, baseUrl);
+ ret.fullLogLink = url.href;
+ }
+ }
+ }
+ return ret;
+ } catch (e) {
+ throw new Error(`Log not available`);
+ }
+ } else if (config.logType === 'log-manager') {
+ // Try to get roated log if currently log content is less than 15KB
+ if (text.length <= 15 * 1024) {
+ const fullLogUrl = logUrl.replace('/tail/', '/full/');
+ const rotatedLogUrl = logUrl + '.1';
+ const rotatedLogRes = await fetch(rotatedLogUrl);
+ const fullLogRes = await fetch(fullLogUrl);
+ const rotatedText = await rotatedLogRes.text();
+ const fullLog = await fullLogRes.text();
+ if (rotatedLogRes.ok && rotatedText.trim() !== 'No such file!') {
+ text = rotatedText
+ .concat('\n--------log is rotated, may be lost during this--------\n')
+ .concat(fullLog);
+ }
+ // get last 16KB
+ text = text.slice(-16 * 1024);
+ }
+ ret.text = text;
+ ret.fullLogLink = logUrl.replace('/tail/', '/full/');
+ return ret;
+ } else {
+ throw new Error(`Log not available`);
+ }
+}
diff --git a/src/webportal/src/app/job/job-view/fabric/task-attempt/task-attempt-dialog.jsx b/src/webportal/src/app/job/job-view/fabric/task-attempt/task-attempt-dialog.jsx
new file mode 100644
index 0000000000..ea7fa75b6b
--- /dev/null
+++ b/src/webportal/src/app/job/job-view/fabric/task-attempt/task-attempt-dialog.jsx
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { isNil } from 'lodash';
+import {
+ PrimaryButton,
+ DefaultButton,
+ Dialog,
+ DialogFooter,
+} from 'office-ui-fabric-react';
+import TaskAttemptList from './task-attempt-list';
+import { fetchTaskStatus } from '../fabric/job-detail/conn';
+
+const TaskAttemptDialog = props => {
+ const {
+ hideDialog,
+ toggleHideDialog,
+ jobAttemptIndex,
+ taskRoleName,
+ taskIndex,
+ } = props;
+ const [taskAttempts, setTaskAttempts] = useState([]);
+
+ useEffect(() => {
+ if (!isNil(jobAttemptIndex) && !isNil(taskRoleName) && !isNil(taskIndex)) {
+ fetchTaskStatus(jobAttemptIndex, taskRoleName, taskIndex).then(
+ taskStatus => {
+ const attempts = taskStatus.attempts;
+ setTaskAttempts(attempts);
+ },
+ );
+ }
+ }, [jobAttemptIndex, taskRoleName, taskIndex]);
+
+ return (
+
+ );
+};
+
+TaskAttemptDialog.propTypes = {
+ hideDialog: PropTypes.bool,
+ toggleHideDialog: PropTypes.func,
+ jobAttemptIndex: PropTypes.number,
+ taskRoleName: PropTypes.string,
+ taskIndex: PropTypes.number,
+};
+
+export default TaskAttemptDialog;
diff --git a/src/webportal/src/app/job/job-view/fabric/task-attempt/task-attempt-list.jsx b/src/webportal/src/app/job/job-view/fabric/task-attempt/task-attempt-list.jsx
new file mode 100644
index 0000000000..d0a154cd93
--- /dev/null
+++ b/src/webportal/src/app/job/job-view/fabric/task-attempt/task-attempt-list.jsx
@@ -0,0 +1,642 @@
+// Copyright (c) Microsoft Corporation
+// All rights reserved.
+//
+// MIT License
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
+// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { ColorClassNames, FontClassNames, getTheme } from '@uifabric/styling';
+import c from 'classnames';
+import { capitalize, isEmpty, isNil, flatten } from 'lodash';
+import { DateTime, Interval } from 'luxon';
+import { CommandBarButton, PrimaryButton, Stack } from 'office-ui-fabric-react';
+import {
+ DetailsList,
+ SelectionMode,
+ DetailsRow,
+ DetailsListLayoutMode,
+} from 'office-ui-fabric-react/lib/DetailsList';
+import PropTypes from 'prop-types';
+import React from 'react';
+import yaml from 'js-yaml';
+
+import localCss from './task-role-container-list.scss';
+import t from '../../../../components/tachyons.scss';
+
+import { getContainerLog } from './conn';
+import config from '../../../../config/webportal.config';
+import MonacoPanel from '../../../../components/monaco-panel';
+import StatusBadge from '../../../../components/status-badge';
+import CopyButton from '../../../../components/copy-button';
+import { getDurationString } from '../../../../components/util/job';
+
+const theme = getTheme();
+
+const IPTooltipContent = ({ ip }) => {
+ return (
+