From c0623627f64109d065ef465abcd0018377ecf7f9 Mon Sep 17 00:00:00 2001 From: Yi Yi Date: Tue, 15 Dec 2020 05:14:46 +0800 Subject: [PATCH 1/4] update --- .../app/job/job-view/fabric/job-detail.jsx | 18 +- .../fabric/job-detail/components/context.jsx | 2 + .../components/task-role-container-list.jsx | 22 +- .../components/task-role-container-top.jsx | 193 ++++++++++++++++++ .../components/task-role-filter.jsx | 63 ++++++ 5 files changed, 266 insertions(+), 32 deletions(-) create mode 100644 src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-top.jsx create mode 100644 src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-filter.jsx 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..c99e0bd963 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,7 @@ export default class TaskRoleContainerList extends React.Component { items: props.tasks, ordering: { field: null, descending: false }, hideAllLogsDialog: true, + filter: new TaskRoleFilter(), }; this.showSshInfo = this.showSshInfo.bind(this); @@ -497,6 +486,7 @@ export default class TaskRoleContainerList extends React.Component { return (
+ + ); +} + +export default function TaskRoleContainerTop({ taskStatuses }) { + const { filter, setFilter } = useContext(Context); + 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); + } + if (item.containerNodeName) { + nodeNames.add(item.containerNodeName); + } + } + + const statuses = { + Waiting: true, + Succeeded: true, + Running: true, + Stopped: true, + Failed: true, + }; + + const { spacing } = getTheme(); + + 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, +}; 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..57deead2cb --- /dev/null +++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-filter.jsx @@ -0,0 +1,63 @@ +// 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( + ({ exitType, exitCode, nodeName }) => + exitType.indexOf(keyword) > -1 || + exitCode.indexOf(keyword) > -1 || + nodeName.indexOf(keyword) > -1, + ); + } + if (!isEmpty(exitType)) { + filters.push(({ exitType }) => exitType.has(exitType)); + } + if (!isEmpty(exitCode)) { + filters.push(({ exitCode }) => exitCode.has(exitCode)); + } + if (!isEmpty(nodeName)) { + filters.push(({ nodeName }) => nodeName.has(nodeName)); + } + 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; From 369bd608f0d8ab78f1f4544240b8f11a76eaca1e Mon Sep 17 00:00:00 2001 From: Yi Yi Date: Tue, 15 Dec 2020 06:03:58 +0800 Subject: [PATCH 2/4] fix filter and keyword search --- .../components/task-role-container-list.jsx | 9 ++++- .../components/task-role-container-top.jsx | 35 ++++++++++------- .../components/task-role-filter.jsx | 39 +++++++++++++++---- 3 files changed, 60 insertions(+), 23 deletions(-) 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 c99e0bd963..06e503ef60 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 @@ -481,17 +481,22 @@ export default class TaskRoleContainerList extends React.Component { tailLogUrls, hideAllLogsDialog, items, + filter, } = this.state; const { showMoreDiagnostics } = this.props; return (
- + this.setState({ filter: newFilter })} + /> - + { @@ -190,4 +195,6 @@ export default function TaskRoleContainerTop({ taskStatuses }) { TaskRoleContainerTop.propTypes = { taskStatuses: PropTypes.array.isRequired, + filter: PropTypes.object.isRequired, + setFilter: PropTypes.func.isRequired, }; 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 index 57deead2cb..6428d88822 100644 --- 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 @@ -34,20 +34,45 @@ class TaskRoleFilter { const filters = []; if (keyword !== '') { filters.push( - ({ exitType, exitCode, nodeName }) => - exitType.indexOf(keyword) > -1 || - exitCode.indexOf(keyword) > -1 || - nodeName.indexOf(keyword) > -1, + ({ + containerExitSpec, + containerNodeName, + taskState, + containerIp, + containerId, + }) => + (taskState && capitalize(taskState).indexOf(keyword) > -1) || + (containerExitSpec && + containerExitSpec.type && + containerExitSpec.type.indexOf(keyword) > -1) || + (containerExitSpec && + containerExitSpec.code !== undefined && + containerExitSpec.code.toString().indexOf(keyword) > -1) || + (containerNodeName && containerNodeName.indexOf(keyword) > -1) || + (containerIp && containerIp.indexOf(keyword) > -1) || + (containerId && containerId.indexOf(keyword) > -1), ); } if (!isEmpty(exitType)) { - filters.push(({ exitType }) => exitType.has(exitType)); + filters.push(({ containerExitSpec }) => { + return ( + containerExitSpec && + containerExitSpec.type && + exitType.has(containerExitSpec.type) + ); + }); } if (!isEmpty(exitCode)) { - filters.push(({ exitCode }) => exitCode.has(exitCode)); + filters.push(({ containerExitSpec }) => { + return ( + containerExitSpec && + containerExitSpec.code && + exitCode.has(containerExitSpec.code) + ); + }); } if (!isEmpty(nodeName)) { - filters.push(({ nodeName }) => nodeName.has(nodeName)); + filters.push(({ containerNodeName }) => nodeName.has(containerNodeName)); } if (!isEmpty(statuses)) { filters.push(({ taskState }) => statuses.has(capitalize(taskState))); From fe4d7b71fa08a220a6f5b4381349883c17a6e714 Mon Sep 17 00:00:00 2001 From: Yi Yi Date: Tue, 15 Dec 2020 07:20:56 +0800 Subject: [PATCH 3/4] Add csv exporter --- .../components/task-role-container-list.jsx | 3 + .../components/task-role-container-top.jsx | 14 ++- .../components/task-role-csv-exporter.jsx | 101 ++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-csv-exporter.jsx 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 06e503ef60..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 @@ -163,6 +163,7 @@ export default class TaskRoleContainerList extends React.Component { ordering: { field: null, descending: false }, hideAllLogsDialog: true, filter: new TaskRoleFilter(), + taskRoleName: props.taskRoleName, }; this.showSshInfo = this.showSshInfo.bind(this); @@ -482,6 +483,7 @@ export default class TaskRoleContainerList extends React.Component { hideAllLogsDialog, items, filter, + taskRoleName, } = this.state; const { showMoreDiagnostics } = this.props; return ( @@ -489,6 +491,7 @@ export default class TaskRoleContainerList extends React.Component { this.setState({ filter: newFilter })} /> diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-top.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-top.jsx index dfbc55ca2b..769ce75e10 100644 --- a/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-top.jsx +++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/task-role-container-top.jsx @@ -12,6 +12,7 @@ import { import PropTypes from 'prop-types'; import TaskRoleFilter from './task-role-filter'; import FilterButton from '../../JobList/FilterButton'; +import TaskRoleCsvExporter from './task-role-csv-exporter'; function KeywordSearchBox({ filter, setFilter }) { function onKeywordChange(keyword) { @@ -52,6 +53,7 @@ export default function TaskRoleContainerTop({ taskStatuses, filter, setFilter, + taskRoleName, }) { const exitTypes = new Set(); const exitCodes = new Set(); @@ -78,6 +80,8 @@ export default function TaskRoleContainerTop({ }; const { spacing } = getTheme(); + const csvExporter = new TaskRoleCsvExporter(); + const expCsv = () => csvExporter.apply(taskRoleName + '.csv', taskStatuses); return ( @@ -95,7 +99,14 @@ export default function TaskRoleContainerTop({ ], }} > - + + + + `${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); + } +} From 3c2f3183edd3be72ae3fd5d4e8a15c24d4c45ec7 Mon Sep 17 00:00:00 2001 From: Yi Yi Date: Tue, 15 Dec 2020 16:50:44 +0800 Subject: [PATCH 4/4] fix case sensitive in keyword search --- .../components/task-role-filter.jsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) 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 index 6428d88822..d0afbcb773 100644 --- 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 @@ -41,16 +41,26 @@ class TaskRoleFilter { containerIp, containerId, }) => - (taskState && capitalize(taskState).indexOf(keyword) > -1) || + (taskState && + taskState.toLowerCase().indexOf(keyword.toLowerCase()) > -1) || (containerExitSpec && containerExitSpec.type && - containerExitSpec.type.indexOf(keyword) > -1) || + containerExitSpec.type + .toLowerCase() + .indexOf(keyword.toLowerCase()) > -1) || (containerExitSpec && containerExitSpec.code !== undefined && - containerExitSpec.code.toString().indexOf(keyword) > -1) || - (containerNodeName && containerNodeName.indexOf(keyword) > -1) || - (containerIp && containerIp.indexOf(keyword) > -1) || - (containerId && containerId.indexOf(keyword) > -1), + 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)) {