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;