diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail.jsx
index 82d827368b..a7c4fb22ec 100644
--- a/src/webportal/src/app/job/job-view/fabric/job-detail.jsx
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail.jsx
@@ -1,19 +1,5 @@
-// 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.
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
import { capitalize, isEmpty, isNil, get, cloneDeep } from 'lodash';
import { DateTime, Interval } from 'luxon';
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/context.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/context.jsx
index 280f49131f..d0ce17573e 100644
--- a/src/webportal/src/app/job/job-view/fabric/job-detail/components/context.jsx
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/context.jsx
@@ -22,6 +22,8 @@ const Context = React.createContext({
rawJobConfig: null,
sshInfo: null,
isViewingSelf: null,
+ filter: null,
+ setFilter: null,
});
export default Context;
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-list.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-list.jsx
index a67ca21881..5f302ed1ab 100644
--- a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-list.jsx
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-list.jsx
@@ -1,19 +1,5 @@
-// 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.
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
import { ThemeProvider } from '@uifabric/foundation';
import {
@@ -52,6 +38,8 @@ import t from '../../../../../components/tachyons.scss';
import Context from './context';
import Timer from './timer';
+import TaskRoleFilter from './task-role-filter';
+import TaskRoleContainerTop from './task-role-container-top';
import { getContainerLog, getContainerLogList } from '../conn';
import config from '../../../../../config/webportal.config';
import MonacoPanel from '../../../../../components/monaco-panel';
@@ -174,6 +162,8 @@ export default class TaskRoleContainerList extends React.Component {
items: props.tasks,
ordering: { field: null, descending: false },
hideAllLogsDialog: true,
+ filter: new TaskRoleFilter(),
+ taskRoleName: props.taskRoleName,
};
this.showSshInfo = this.showSshInfo.bind(this);
@@ -492,16 +482,24 @@ export default class TaskRoleContainerList extends React.Component {
tailLogUrls,
hideAllLogsDialog,
items,
+ filter,
+ taskRoleName,
} = this.state;
const { showMoreDiagnostics } = this.props;
return (
+ this.setState({ filter: newFilter })}
+ />
+ );
+}
+
+KeywordSearchBox.propTypes = {
+ filter: PropTypes.object.isRequired,
+ setFilter: PropTypes.func.isRequired,
+};
+
+export default function TaskRoleContainerTop({
+ taskStatuses,
+ filter,
+ setFilter,
+ taskRoleName,
+}) {
+ const exitTypes = new Set();
+ const exitCodes = new Set();
+ const nodeNames = new Set();
+
+ for (const item of taskStatuses) {
+ if (item.containerExitSpec && item.containerExitSpec.type) {
+ exitTypes.add(item.containerExitSpec.type);
+ }
+ if (item.containerExitCode) {
+ exitCodes.add(item.containerExitCode.toString());
+ }
+ if (item.containerNodeName) {
+ nodeNames.add(item.containerNodeName);
+ }
+ }
+
+ const statuses = {
+ Waiting: true,
+ Succeeded: true,
+ Running: true,
+ Stopped: true,
+ Failed: true,
+ };
+
+ const { spacing } = getTheme();
+ const csvExporter = new TaskRoleCsvExporter();
+ const expCsv = () => csvExporter.apply(taskRoleName + '.csv', taskStatuses);
+
+ return (
+
+
+
+
+
+
+
+ {
+ const { keyword, exitType, exitCode, nodeName } = filter;
+ setFilter(
+ new TaskRoleFilter(
+ keyword,
+ new Set(statuses),
+ exitType,
+ exitCode,
+ nodeName,
+ ),
+ );
+ }}
+ clearButton
+ />
+ {
+ const { keyword, statuses, exitCode, nodeName } = filter;
+ setFilter(
+ new TaskRoleFilter(
+ keyword,
+ statuses,
+ new Set(exitTypes),
+ exitCode,
+ nodeName,
+ ),
+ );
+ }}
+ searchBox
+ clearButton
+ />
+ {
+ const { keyword, statuses, exitType, nodeName } = filter;
+ setFilter(
+ new TaskRoleFilter(
+ keyword,
+ statuses,
+ exitType,
+ new Set(exitCodes),
+ nodeName,
+ ),
+ );
+ }}
+ searchBox
+ clearButton
+ />
+ {
+ const { keyword, statuses, exitType, exitCode } = filter;
+ setFilter(
+ new TaskRoleFilter(
+ keyword,
+ statuses,
+ exitType,
+ exitCode,
+ new Set(nodeNames),
+ ),
+ );
+ }}
+ searchBox
+ clearButton
+ />
+ setFilter(new TaskRoleFilter())}
+ />
+
+
+
+ );
+}
+
+TaskRoleContainerTop.propTypes = {
+ taskStatuses: PropTypes.array.isRequired,
+ filter: PropTypes.object.isRequired,
+ setFilter: PropTypes.func.isRequired,
+ taskRoleName: PropTypes.string.isRequired,
+};
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-csv-exporter.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-csv-exporter.jsx
new file mode 100644
index 0000000000..51c8c13346
--- /dev/null
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-csv-exporter.jsx
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { capitalize, isNil } from 'lodash';
+import { DateTime, Interval } from 'luxon';
+import { getDurationString } from '../../../../../components/util/job';
+
+function exportToCsv(filename, rows) {
+ var csvFile = rows.join('\n');
+ var blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' });
+ if (navigator.msSaveBlob) {
+ // IE 10+
+ navigator.msSaveBlob(blob, filename);
+ } else {
+ var link = document.createElement('a');
+ if (link.download !== undefined) {
+ // Browsers that support HTML5 download attribute
+ var url = URL.createObjectURL(blob);
+ link.setAttribute('href', url);
+ link.setAttribute('download', filename);
+ link.style.visibility = 'hidden';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
+ }
+}
+
+function getPortsString(ports) {
+ if (isNil(ports)) {
+ return null;
+ }
+
+ return Object.entries(ports)
+ .map(([key, val]) => `${key}: ${val}`)
+ .join(' ');
+}
+
+function 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';
+ }
+}
+
+export default class TaskRoleCsvExporter {
+ apply(taskRoleName, items) {
+ const columns = [
+ 'Task Index',
+ 'Task State',
+ 'Task Retries',
+ 'IP',
+ 'Ports',
+ 'Exit Type',
+ 'Exit Code',
+ 'Running Start Time',
+ 'Running Duration',
+ 'Node Name',
+ 'Container ID',
+ ];
+ const rows = [columns.join(',')];
+
+ for (const item of items) {
+ const cols = [
+ item.taskIndex,
+ capitalize(item.taskState),
+ item.retries,
+ !isNil(item.containerIp) ? item.containerIp : 'N/A',
+ getPortsString(item.containerPorts),
+ !isNil(item.containerExitSpec) && !isNil(item.containerExitSpec.type)
+ ? item.containerExitSpec.type
+ : 'null',
+ isNil(item.containerExitSpec)
+ ? item.containerExitCode
+ : `${item.containerExitCode} (${item.containerExitSpec.phrase})`,
+ isNil(item.launchedTime)
+ ? 'null'
+ : `"${DateTime.fromMillis(item.launchedTime).toLocaleString(
+ DateTime.DATETIME_MED_WITH_SECONDS,
+ )}"`,
+ getDurationString(
+ getTimeDuration(item.launchedTime, item.completedTime),
+ ),
+ item.containerNodeName,
+ item.containerId,
+ ];
+
+ rows.push(cols.join(','));
+ }
+
+ exportToCsv(taskRoleName, rows);
+ }
+}
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-filter.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-filter.jsx
new file mode 100644
index 0000000000..d0afbcb773
--- /dev/null
+++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-filter.jsx
@@ -0,0 +1,98 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { capitalize, isEmpty } from 'lodash';
+
+class TaskRoleFilter {
+ /**
+ * @param {string} keyword
+ * @param {Set?} statuses
+ * @param {Set?} exitType
+ * @param {Set?} exitCode
+ * @param {Set?} nodeName
+ */
+ constructor(
+ keyword = '',
+ statuses = new Set(),
+ exitType = new Set(),
+ exitCode = new Set(),
+ nodeName = new Set(),
+ ) {
+ this.keyword = keyword;
+ this.statuses = statuses;
+ this.exitType = exitType;
+ this.exitCode = exitCode;
+ this.nodeName = nodeName;
+ }
+
+ /**
+ * @param {any[]} taskRoles
+ */
+ apply(taskRoles) {
+ const { keyword, statuses, exitType, exitCode, nodeName } = this;
+
+ const filters = [];
+ if (keyword !== '') {
+ filters.push(
+ ({
+ containerExitSpec,
+ containerNodeName,
+ taskState,
+ containerIp,
+ containerId,
+ }) =>
+ (taskState &&
+ taskState.toLowerCase().indexOf(keyword.toLowerCase()) > -1) ||
+ (containerExitSpec &&
+ containerExitSpec.type &&
+ containerExitSpec.type
+ .toLowerCase()
+ .indexOf(keyword.toLowerCase()) > -1) ||
+ (containerExitSpec &&
+ containerExitSpec.code !== undefined &&
+ containerExitSpec.code
+ .toString()
+ .toLowerCase()
+ .indexOf(keyword.toLowerCase()) > -1) ||
+ (containerNodeName &&
+ containerNodeName.toLowerCase().indexOf(keyword.toLowerCase()) >
+ -1) ||
+ (containerIp &&
+ containerIp.toLowerCase().indexOf(keyword.toLowerCase()) > -1) ||
+ (containerId &&
+ containerId.toLowerCase().indexOf(keyword.toLowerCase()) > -1),
+ );
+ }
+ if (!isEmpty(exitType)) {
+ filters.push(({ containerExitSpec }) => {
+ return (
+ containerExitSpec &&
+ containerExitSpec.type &&
+ exitType.has(containerExitSpec.type)
+ );
+ });
+ }
+ if (!isEmpty(exitCode)) {
+ filters.push(({ containerExitSpec }) => {
+ return (
+ containerExitSpec &&
+ containerExitSpec.code &&
+ exitCode.has(containerExitSpec.code)
+ );
+ });
+ }
+ if (!isEmpty(nodeName)) {
+ filters.push(({ containerNodeName }) => nodeName.has(containerNodeName));
+ }
+ if (!isEmpty(statuses)) {
+ filters.push(({ taskState }) => statuses.has(capitalize(taskState)));
+ }
+ if (filters.length === 0) return taskRoles;
+
+ return taskRoles.filter(taskRole =>
+ filters.every(filter => filter(taskRole)),
+ );
+ }
+}
+
+export default TaskRoleFilter;