From e558c337c06d575e35042ca3c491729428328feb Mon Sep 17 00:00:00 2001 From: debuggy Date: Thu, 15 Oct 2020 14:47:14 +0800 Subject: [PATCH 1/2] Add job event page --- src/webportal/config/webpack.common.js | 5 + .../fabric/job-detail/components/summary.jsx | 8 + .../src/app/job/job-view/fabric/job-event.jsx | 61 ++++++++ .../app/job/job-view/fabric/job-event/conn.js | 43 ++++++ .../fabric/job-event/job-event-list.jsx | 142 ++++++++++++++++++ 5 files changed, 259 insertions(+) create mode 100644 src/webportal/src/app/job/job-view/fabric/job-event.jsx create mode 100644 src/webportal/src/app/job/job-view/fabric/job-event/conn.js create mode 100644 src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx diff --git a/src/webportal/config/webpack.common.js b/src/webportal/config/webpack.common.js index a603444473..78622f0ae1 100644 --- a/src/webportal/config/webpack.common.js +++ b/src/webportal/config/webpack.common.js @@ -71,6 +71,7 @@ const config = (env, argv) => ({ jobList: './src/app/job/job-view/fabric/job-list.jsx', jobDetail: './src/app/job/job-view/fabric/job-detail.jsx', jobRetry: './src/app/job/job-view/fabric/job-retry.jsx', + jobEvent: './src/app/job/job-view/fabric/job-event.jsx', virtualClusters: './src/app/vc/vc.component.js', services: './src/app/cluster-view/services/services.component.js', hardware: './src/app/cluster-view/hardware/hardware.component.js', @@ -338,6 +339,10 @@ const config = (env, argv) => ({ filename: 'job-retry.html', chunks: ['layout', 'jobRetry'], }), + generateHtml({ + filename: 'job-event.html', + chunks: ['layout', 'jobEvent'], + }), generateHtml({ filename: 'virtual-clusters.html', chunks: ['layout', 'virtualClusters'], diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/summary.jsx b/src/webportal/src/app/job/job-view/fabric/job-detail/components/summary.jsx index cdd631a0ec..bb20380af4 100644 --- a/src/webportal/src/app/job/job-view/fabric/job-detail/components/summary.jsx +++ b/src/webportal/src/app/job/job-view/fabric/job-detail/components/summary.jsx @@ -574,6 +574,14 @@ export default class Summary extends React.Component { > Go to Job Metrics Page +
+ + Go to Job Event Page + {!isNil(getTensorBoardUrl(jobInfo, rawJobConfig)) && (
diff --git a/src/webportal/src/app/job/job-view/fabric/job-event.jsx b/src/webportal/src/app/job/job-view/fabric/job-event.jsx new file mode 100644 index 0000000000..d747225d32 --- /dev/null +++ b/src/webportal/src/app/job/job-view/fabric/job-event.jsx @@ -0,0 +1,61 @@ +// 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 { isEmpty } from 'lodash'; + +import { SpinnerLoading } from '../../../components/loading'; +import { fetchJobEvents } from './job-event/conn'; +import JobEventList from './job-event/job-event-list'; + +const params = new URLSearchParams(window.location.search); +const userName = params.get('userName'); +const jobName = params.get('jobName'); + +const JobEventPage = () => { + const [loading, setLoading] = useState(true); + const [jobEvents, setJobEvents] = useState([]); + + useEffect(() => { + fetchJobEvents(userName, jobName).then(res => { + setJobEvents(res.data); + setLoading(false); + }); + }, []); + + return ( +
+ {loading && } + {!loading && ( + + + Back to Job Detail + + Job Event List + + + )} +
+ ); +}; + +ReactDOM.render(, document.getElementById('content-wrapper')); diff --git a/src/webportal/src/app/job/job-view/fabric/job-event/conn.js b/src/webportal/src/app/job/job-view/fabric/job-event/conn.js new file mode 100644 index 0000000000..33b17d7a5c --- /dev/null +++ b/src/webportal/src/app/job/job-view/fabric/job-event/conn.js @@ -0,0 +1,43 @@ +// 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 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 fetchJobEvents(userName, jobName) { + return wrapper(async () => { + const restServerUri = new URL(config.restServerUri, window.location.href); + const url = `${restServerUri}/api/v2/jobs/${userName}~${jobName}/events?type=Warning`; + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const result = await res.json(); + return result; + }); +} diff --git a/src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx b/src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx new file mode 100644 index 0000000000..1a6ebf9147 --- /dev/null +++ b/src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx @@ -0,0 +1,142 @@ +// 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 } from '@uifabric/styling'; +import { DateTime } from 'luxon'; +import { + DetailsList, + SelectionMode, + DetailsListLayoutMode, +} from 'office-ui-fabric-react/lib/DetailsList'; +import PropTypes from 'prop-types'; +import React from 'react'; + +const JobEventList = props => { + const { jobEvents } = props; + + const columns = [ + { + key: 'uid', + name: 'uid', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return
{item.uid}
; + }, + }, + { + key: 'taskRoleName', + name: 'Task Role Name', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return ( +
{item.taskroleName}
+ ); + }, + }, + { + key: 'taskIndex', + name: 'Task Index', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return ( +
{item.taskIndex}
+ ); + }, + }, + { + key: 'type', + name: 'Type', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return
{item.type}
; + }, + }, + { + key: 'reason', + name: 'Reason', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return
{item.reason}
; + }, + }, + { + key: 'message', + name: 'message', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return
{item.message}
; + }, + }, + { + key: 'firstTimestamp', + name: 'First Timestamp', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return ( +
+ {DateTime.fromISO(item.firstTimestamp).toLocaleString()} +
+ ); + }, + }, + { + key: 'lastTimestamp', + name: 'Last Timestamp', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return ( +
+ {DateTime.fromISO(item.lastTimestamp).toLocaleString()} +
+ ); + }, + }, + { + key: 'count', + name: 'Count', + headerClassName: FontClassNames.medium, + isResizable: true, + onRender: (item, idx) => { + return
{item.count}
; + }, + }, + ]; + + return ( + + ); +}; + +JobEventList.propTypes = { + jobEvents: PropTypes.arrayOf(PropTypes.object), +}; + +export default JobEventList; From 0a28c19f42f024504eb356c8b1a94bce5441dd29 Mon Sep 17 00:00:00 2001 From: debuggy Date: Thu, 15 Oct 2020 17:51:59 +0800 Subject: [PATCH 2/2] add dialog --- .../fabric/job-event/job-event-list.jsx | 88 ++++++++++++++----- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx b/src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx index 1a6ebf9147..47b06b7218 100644 --- a/src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx +++ b/src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx @@ -15,29 +15,32 @@ // 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 } from '@uifabric/styling'; import { DateTime } from 'luxon'; import { DetailsList, SelectionMode, DetailsListLayoutMode, -} from 'office-ui-fabric-react/lib/DetailsList'; + FontClassNames, + Text, + Stack, + Dialog, + DialogFooter, + PrimaryButton, + CommandBarButton, +} from 'office-ui-fabric-react'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useState } from 'react'; const JobEventList = props => { const { jobEvents } = props; + const [hideDialog, setHideDialog] = useState(true); + const [dialogMessage, setDialogMessage] = useState(null); + + const toggleHideDialog = () => { + setHideDialog(!hideDialog); + }; const columns = [ - { - key: 'uid', - name: 'uid', - headerClassName: FontClassNames.medium, - isResizable: true, - onRender: (item, idx) => { - return
{item.uid}
; - }, - }, { key: 'taskRoleName', name: 'Task Role Name', @@ -52,6 +55,7 @@ const JobEventList = props => { { key: 'taskIndex', name: 'Task Index', + maxWidth: 60, headerClassName: FontClassNames.medium, isResizable: true, onRender: (item, idx) => { @@ -72,6 +76,7 @@ const JobEventList = props => { { key: 'reason', name: 'Reason', + minWidth: 150, headerClassName: FontClassNames.medium, isResizable: true, onRender: (item, idx) => { @@ -82,20 +87,44 @@ const JobEventList = props => { key: 'message', name: 'message', headerClassName: FontClassNames.medium, + minWidth: 550, + maxWidth: 1000, isResizable: true, onRender: (item, idx) => { - return
{item.message}
; + return ( + + + {item.message} + + { + toggleHideDialog(); + setDialogMessage(item.message); + }} + /> + + ); }, }, { key: 'firstTimestamp', name: 'First Timestamp', + minWidth: 160, headerClassName: FontClassNames.medium, isResizable: true, onRender: (item, idx) => { return (
- {DateTime.fromISO(item.firstTimestamp).toLocaleString()} + {DateTime.fromISO(item.firstTimestamp).toLocaleString( + DateTime.DATETIME_SHORT, + )}
); }, @@ -104,11 +133,14 @@ const JobEventList = props => { key: 'lastTimestamp', name: 'Last Timestamp', headerClassName: FontClassNames.medium, + minWidth: 160, isResizable: true, onRender: (item, idx) => { return (
- {DateTime.fromISO(item.lastTimestamp).toLocaleString()} + {DateTime.fromISO(item.lastTimestamp).toLocaleString( + DateTime.DATETIME_SHORT, + )}
); }, @@ -116,6 +148,7 @@ const JobEventList = props => { { key: 'count', name: 'Count', + maxWidth: 50, headerClassName: FontClassNames.medium, isResizable: true, onRender: (item, idx) => { @@ -125,13 +158,24 @@ const JobEventList = props => { ]; return ( - + + + + ); };