Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Commit

Permalink
Add job event page
Browse files Browse the repository at this point in the history
  • Loading branch information
debuggy committed Oct 15, 2020
1 parent 1163cb8 commit e558c33
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/webportal/config/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,14 @@ export default class Summary extends React.Component {
>
Go to Job Metrics Page
</Link>
<div className={c(t.bl, t.mh3)}></div>
<Link
styles={{ root: [FontClassNames.mediumPlus] }}
href={`job-event.html?userName=${namespace}&jobName=${jobName}`}
target='_blank'
>
Go to Job Event Page
</Link>
{!isNil(getTensorBoardUrl(jobInfo, rawJobConfig)) && (
<div className={c(t.flex)}>
<div className={c(t.bl, t.mh3)}></div>
Expand Down
61 changes: 61 additions & 0 deletions src/webportal/src/app/job/job-view/fabric/job-event.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{loading && <SpinnerLoading />}
{!loading && (
<Stack styles={{ root: { margin: '30px', overflow: 'auto' } }} gap='l1'>
<ActionButton
iconProps={{ iconName: 'revToggleKey' }}
href={`job-detail.html?username=${userName}&jobName=${jobName}`}
>
Back to Job Detail
</ActionButton>
<Text variant='xLarge'>Job Event List</Text>
<JobEventList jobEvents={isEmpty(jobEvents) ? null : jobEvents} />
</Stack>
)}
</div>
);
};

ReactDOM.render(<JobEventPage />, document.getElementById('content-wrapper'));
43 changes: 43 additions & 0 deletions src/webportal/src/app/job/job-view/fabric/job-event/conn.js
Original file line number Diff line number Diff line change
@@ -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;
});
}
142 changes: 142 additions & 0 deletions src/webportal/src/app/job/job-view/fabric/job-event/job-event-list.jsx
Original file line number Diff line number Diff line change
@@ -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 <div className={FontClassNames.mediumPlus}>{item.uid}</div>;
},
},
{
key: 'taskRoleName',
name: 'Task Role Name',
headerClassName: FontClassNames.medium,
isResizable: true,
onRender: (item, idx) => {
return (
<div className={FontClassNames.mediumPlus}>{item.taskroleName}</div>
);
},
},
{
key: 'taskIndex',
name: 'Task Index',
headerClassName: FontClassNames.medium,
isResizable: true,
onRender: (item, idx) => {
return (
<div className={FontClassNames.mediumPlus}>{item.taskIndex}</div>
);
},
},
{
key: 'type',
name: 'Type',
headerClassName: FontClassNames.medium,
isResizable: true,
onRender: (item, idx) => {
return <div className={FontClassNames.mediumPlus}>{item.type}</div>;
},
},
{
key: 'reason',
name: 'Reason',
headerClassName: FontClassNames.medium,
isResizable: true,
onRender: (item, idx) => {
return <div className={FontClassNames.mediumPlus}>{item.reason}</div>;
},
},
{
key: 'message',
name: 'message',
headerClassName: FontClassNames.medium,
isResizable: true,
onRender: (item, idx) => {
return <div className={FontClassNames.mediumPlus}>{item.message}</div>;
},
},
{
key: 'firstTimestamp',
name: 'First Timestamp',
headerClassName: FontClassNames.medium,
isResizable: true,
onRender: (item, idx) => {
return (
<div className={FontClassNames.mediumPlus}>
{DateTime.fromISO(item.firstTimestamp).toLocaleString()}
</div>
);
},
},
{
key: 'lastTimestamp',
name: 'Last Timestamp',
headerClassName: FontClassNames.medium,
isResizable: true,
onRender: (item, idx) => {
return (
<div className={FontClassNames.mediumPlus}>
{DateTime.fromISO(item.lastTimestamp).toLocaleString()}
</div>
);
},
},
{
key: 'count',
name: 'Count',
headerClassName: FontClassNames.medium,
isResizable: true,
onRender: (item, idx) => {
return <div className={FontClassNames.mediumPlus}>{item.count}</div>;
},
},
];

return (
<DetailsList
columns={columns}
disableSelectionZone
items={jobEvents}
layoutMode={DetailsListLayoutMode.justified}
selectionMode={SelectionMode.none}
/>
);
};

JobEventList.propTypes = {
jobEvents: PropTypes.arrayOf(PropTypes.object),
};

export default JobEventList;

0 comments on commit e558c33

Please sign in to comment.