+);
diff --git a/src/webportal/src/app/components/status-badge.jsx b/src/webportal/src/app/components/status-badge.jsx
new file mode 100644
index 0000000000..6e4a4f170a
--- /dev/null
+++ b/src/webportal/src/app/components/status-badge.jsx
@@ -0,0 +1,173 @@
+// 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, mergeStyles} from '@uifabric/styling';
+import c from 'classnames';
+import {isEmpty} from 'lodash';
+import {Icon} from 'office-ui-fabric-react/lib/Icon';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import t from './tachyons.scss';
+
+import {statusColorMapping} from './theme';
+
+export const Badge = ({children, className}) => (
+
+
+);
+
+IconBadge.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ icons: PropTypes.array,
+};
+
+const bgYellow = mergeStyles({backgroundColor: statusColorMapping.waiting});
+const bgRed = mergeStyles({backgroundColor: statusColorMapping.failed});
+const bgBlue = mergeStyles({backgroundColor: statusColorMapping.running});
+const bgGreen = mergeStyles({backgroundColor: statusColorMapping.succeeded});
+const bgGray = mergeStyles({backgroundColor: statusColorMapping.unknown});
+
+export const SucceededBadge = ({children}) => (
+
+ {children}
+
+);
+
+SucceededBadge.propTypes = {
+ children: PropTypes.node,
+};
+
+export const PrimaryBadge = ({children}) => (
+
+ {children}
+
+);
+
+PrimaryBadge.propTypes = {
+ children: PropTypes.node,
+};
+
+export const WaitingBadge = ({children}) => (
+
+ {children}
+
+);
+
+WaitingBadge.propTypes = {
+ children: PropTypes.node,
+};
+
+export const FailedBadge = ({children}) => (
+
+ {children}
+
+);
+
+FailedBadge.propTypes = {
+ children: PropTypes.node,
+};
+
+export const StoppedBadge = ({children}) => (
+
+ {children}
+
+);
+
+StoppedBadge.propTypes = {
+ children: PropTypes.node,
+};
+
+export const UnknownBadge = ({children}) => (
+
+ {children || 'Unknown'}
+
+);
+
+UnknownBadge.propTypes = {
+ children: PropTypes.node,
+};
+
+export const StatusBadge = ({status}) => {
+ switch (status) {
+ case 'Running':
+ return {status};
+ case 'Stopping':
+ case 'Waiting':
+ return {status};
+ case 'Failed':
+ return {status};
+ case 'Succeeded':
+ return {status};
+ case 'Stopped':
+ return {status};
+ case 'Unknown':
+ return {status};
+ default:
+ return {status};
+ }
+};
+
+StatusBadge.propTypes = {
+ status: PropTypes.oneOf(['Running', 'Stopping', 'Waiting', 'Failed', 'Succeeded', 'Stopped', 'Unknown']),
+};
+
+
+export default StatusBadge;
diff --git a/src/webportal/src/app/components/tachyons.scss b/src/webportal/src/app/components/tachyons.scss
new file mode 100644
index 0000000000..1e39c39e9b
--- /dev/null
+++ b/src/webportal/src/app/components/tachyons.scss
@@ -0,0 +1,66 @@
+/* tachyons without normalize.css */
+
+// Variables
+// Importing here will allow you to override any variables in the modules
+
+@import '~tachyons-sass/scss/_variables';
+
+// Debugging
+@import '~tachyons-sass/scss/_debug-children';
+@import '~tachyons-sass/scss/_debug-grid';
+
+// Uncomment out the line below to help debug layout issues
+// @import '~tachyons-sass/scss/_debug';
+
+// Modules
+@import '~tachyons-sass/scss/_box-sizing';
+@import '~tachyons-sass/scss/_aspect-ratios';
+@import '~tachyons-sass/scss/_images';
+@import '~tachyons-sass/scss/_background-size';
+@import '~tachyons-sass/scss/_background-position';
+@import '~tachyons-sass/scss/_outlines';
+@import '~tachyons-sass/scss/_borders';
+@import '~tachyons-sass/scss/_border-colors';
+@import '~tachyons-sass/scss/_border-radius';
+@import '~tachyons-sass/scss/_border-style';
+@import '~tachyons-sass/scss/_border-widths';
+@import '~tachyons-sass/scss/_box-shadow';
+@import '~tachyons-sass/scss/_code';
+@import '~tachyons-sass/scss/_coordinates';
+@import '~tachyons-sass/scss/_clears';
+@import '~tachyons-sass/scss/_flexbox';
+@import '~tachyons-sass/scss/_display';
+@import '~tachyons-sass/scss/_floats';
+@import '~tachyons-sass/scss/_font-family';
+@import '~tachyons-sass/scss/_font-style';
+@import '~tachyons-sass/scss/_font-weight';
+@import '~tachyons-sass/scss/_forms';
+@import '~tachyons-sass/scss/_heights';
+@import '~tachyons-sass/scss/_letter-spacing';
+@import '~tachyons-sass/scss/_line-height';
+@import '~tachyons-sass/scss/_links';
+@import '~tachyons-sass/scss/_lists';
+@import '~tachyons-sass/scss/_max-widths';
+@import '~tachyons-sass/scss/_widths';
+@import '~tachyons-sass/scss/_overflow';
+@import '~tachyons-sass/scss/_position';
+@import '~tachyons-sass/scss/_opacity';
+@import '~tachyons-sass/scss/_rotations';
+@import '~tachyons-sass/scss/_skins';
+@import '~tachyons-sass/scss/_skins-pseudo';
+@import '~tachyons-sass/scss/_spacing';
+@import '~tachyons-sass/scss/_negative-margins';
+@import '~tachyons-sass/scss/_tables';
+@import '~tachyons-sass/scss/_text-decoration';
+@import '~tachyons-sass/scss/_text-align';
+@import '~tachyons-sass/scss/_text-transform';
+@import '~tachyons-sass/scss/_type-scale';
+@import '~tachyons-sass/scss/_typography';
+@import '~tachyons-sass/scss/_utilities';
+@import '~tachyons-sass/scss/_visibility';
+@import '~tachyons-sass/scss/_white-space';
+@import '~tachyons-sass/scss/_vertical-align';
+@import '~tachyons-sass/scss/_hovers';
+@import '~tachyons-sass/scss/_z-index';
+@import '~tachyons-sass/scss/_nested';
+@import '~tachyons-sass/scss/_styles';
diff --git a/src/webportal/src/app/components/theme.js b/src/webportal/src/app/components/theme.js
new file mode 100644
index 0000000000..fc7195f22f
--- /dev/null
+++ b/src/webportal/src/app/components/theme.js
@@ -0,0 +1,49 @@
+// 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 {loadTheme, FontWeights} from '@uifabric/styling';
+
+export function initTheme() {
+ loadTheme({
+ spacing: {
+ s2: '4px',
+ s1: '8px',
+ m: '16px',
+ l1: '20px',
+ l2: '32px',
+ l3: '64px',
+ },
+ fonts: {
+ xLarge: {
+ fontSize: 20,
+ fontWeight: FontWeights.semibold,
+ },
+ large: {
+ fontSize: 17,
+ fontWeight: FontWeights.regular,
+ },
+ },
+ });
+}
+
+export const statusColorMapping = {
+ waiting: '#fcd116',
+ failed: '#eb1123',
+ running: '#0071bc',
+ succeeded: '#7fba00',
+ unknown: '#b1b5b8',
+};
diff --git a/src/webportal/src/app/components/util/job.js b/src/webportal/src/app/components/util/job.js
new file mode 100644
index 0000000000..007196503c
--- /dev/null
+++ b/src/webportal/src/app/components/util/job.js
@@ -0,0 +1,92 @@
+// 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 {get, isNil} from 'lodash';
+import {Interval, DateTime} from 'luxon';
+
+export function getHumanizedJobStateString(job) {
+ let hjss = '';
+ if (job.state === 'JOB_NOT_FOUND') {
+ hjss = 'N/A';
+ } else if (job.state === 'WAITING') {
+ if (job.executionType === 'STOP') {
+ hjss = 'Stopping';
+ } else {
+ hjss = 'Waiting';
+ }
+ } else if (job.state === 'RUNNING') {
+ if (job.executionType === 'STOP') {
+ hjss = 'Stopping';
+ } else {
+ hjss = 'Running';
+ }
+ } else if (job.state === 'SUCCEEDED') {
+ hjss = 'Succeeded';
+ } else if (job.state === 'FAILED') {
+ hjss = 'Failed';
+ } else if (job.state === 'STOPPED') {
+ hjss = 'Stopped';
+ } else {
+ hjss = 'Unknown';
+ }
+ return hjss;
+}
+
+export function getJobDuration(jobInfo) {
+ const start = get(jobInfo, 'createdTime') && DateTime.fromMillis(jobInfo.createdTime);
+ const end = get(jobInfo, 'completedTime') && DateTime.fromMillis(jobInfo.completedTime);
+ if (start) {
+ return Interval.fromDateTimes(start, end || DateTime.utc()).toDuration(['days', 'hours', 'minutes', 'seconds']);
+ } else {
+ return null;
+ }
+}
+
+export function getJobDurationString(jobInfo) {
+ const dur = getJobDuration(jobInfo);
+ if (!isNil(dur)) {
+ if (dur.days > 0) {
+ return dur.toFormat(`d'd' h'h' m'm' s's'`);
+ } else if (dur.hours > 0) {
+ return dur.toFormat(`h'h' m'm' s's'`);
+ } else if (dur.minutes > 0) {
+ return dur.toFormat(`m'm' s's'`);
+ } else {
+ return dur.toFormat(`s's'`);
+ }
+ } else {
+ return 'N/A';
+ }
+}
+
+export function getJobModifiedTime(job) {
+ const modified = job.completedTime || job.createdTime;
+ if (!isNil(modified)) {
+ return DateTime.fromMillis(modified);
+ } else {
+ return null;
+ }
+}
+
+export function getJobModifiedTimeString(job) {
+ const time = getJobModifiedTime(job);
+ if (!isNil(time)) {
+ return time.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS);
+ } else {
+ return 'N/A';
+ }
+}
diff --git a/src/webportal/src/app/dashboard/dashboard.component.js b/src/webportal/src/app/dashboard/dashboard.component.js
index 3a72e40743..448a5f4588 100644
--- a/src/webportal/src/app/dashboard/dashboard.component.js
+++ b/src/webportal/src/app/dashboard/dashboard.component.js
@@ -33,6 +33,7 @@ window.onresize = function(envent) {
};
$(document).ready(function() {
+ document.getElementById('sidebar-menu--dashboard').classList.add('active');
resizeContentWrapper();
$('#content-wrapper').html(dashboardHtml);
});
diff --git a/src/webportal/src/app/home/home.jsx b/src/webportal/src/app/home/home.jsx
new file mode 100644
index 0000000000..2ae2b87af6
--- /dev/null
+++ b/src/webportal/src/app/home/home.jsx
@@ -0,0 +1,127 @@
+// 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 'core-js/stable';
+import 'regenerator-runtime/runtime';
+import 'whatwg-fetch';
+
+import c from 'classnames';
+import {isEmpty} from 'lodash';
+import {initializeIcons, Stack, getTheme} from 'office-ui-fabric-react';
+import React, {useState, useEffect} from 'react';
+import ReactDOM from 'react-dom';
+import styled from 'styled-components';
+
+import JobStatus from './home/job-status';
+import VirtualClusterList from './home/virtual-cluster-list';
+import GpuChart from './home/gpu-chart';
+import {listJobs, getTotalGpu, getUserInfo, listVirtualClusters, getAvailableGpuPerNode} from './home/conn';
+import RecentJobList from './home/recent-job-list';
+import {SpinnerLoading} from '../components/loading';
+import {initTheme} from '../components/theme';
+
+import t from '../components/tachyons.scss';
+
+initTheme();
+initializeIcons();
+
+const Home = () => {
+ const [loading, setLoading] = useState(true);
+ const [jobs, setJobs] = useState(null);
+ const [userInfo, setUserInfo] = useState(null);
+ const [virtualClusters, setVirtualClusters] = useState(null);
+ const [totalGpu, setTotalGpu] = useState(null);
+ const [gpuPerNode, setGpuPerNode] = useState(null);
+
+ useEffect(() => {
+ if (!isEmpty(cookies.get('user'))) {
+ Promise.all([
+ listJobs().then(setJobs),
+ getUserInfo().then(setUserInfo),
+ listVirtualClusters().then(setVirtualClusters),
+ getTotalGpu().then(setTotalGpu),
+ getAvailableGpuPerNode().then(setGpuPerNode),
+ ]).then(() => setLoading(false)).catch(alert);
+ } else {
+ // layout.component.js will redirect user to index page.
+ }
+ }, []);
+
+ if (loading) {
+ return ;
+ } else {
+ const {spacing} = getTheme();
+
+ const ResponsiveGap = styled.div` {
+ height: 0;
+ width: ${spacing.l2};
+ @media screen and (max-width: 64em) {
+ height: ${spacing.l2};
+ width: 0;
+ }
+ }`;
+
+ const ResponsiveItem = styled.div`
+ width: 33%;
+ height: auto;
+ @media screen and (max-width: 64em) {
+ width: 100%;
+ height: 32rem;
+ }
+ `;
+
+ return (
+
+ {/* top */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* recent jobs */}
+
+
+
+
+ );
+ }
+};
+
+const contentWrapper = document.getElementById('content-wrapper');
+
+ReactDOM.render(, contentWrapper);
+
+document.getElementById('sidebar-menu--home').classList.add('active');
diff --git a/src/webportal/src/app/job/job-view/fabric/job-detail/components/loading.jsx b/src/webportal/src/app/home/home/card.jsx
similarity index 56%
rename from src/webportal/src/app/job/job-view/fabric/job-detail/components/loading.jsx
rename to src/webportal/src/app/home/home/card.jsx
index 4f25419aaf..0e7f23a50b 100644
--- a/src/webportal/src/app/job/job-view/fabric/job-detail/components/loading.jsx
+++ b/src/webportal/src/app/home/home/card.jsx
@@ -15,30 +15,23 @@
// 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 c from 'classnames';
-import {Spinner, SpinnerSize} from 'office-ui-fabric-react/lib/Spinner';
+import PropTypes from 'prop-types';
+import {ColorClassNames, Stack} from 'office-ui-fabric-react';
import React from 'react';
-import t from '../../tachyons.css';
-
-import loadingGif from '../../../../../../assets/img/loading.gif';
-
-export const Loading = () => (
-
-);
+Card.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ children: PropTypes.node,
+};
+
+export default Card;
diff --git a/src/webportal/src/app/home/home/conn.js b/src/webportal/src/app/home/home/conn.js
new file mode 100644
index 0000000000..6a7927cf20
--- /dev/null
+++ b/src/webportal/src/app/home/home/conn.js
@@ -0,0 +1,100 @@
+// 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 {get, isNil} from 'lodash';
+import querystring from 'querystring';
+
+import config from '../../config/webportal.config';
+
+const username = cookies.get('user');
+const token = cookies.get('token');
+
+export async function listJobs() {
+ const res = await fetch(`${config.restServerUri}/api/v1/jobs?${querystring.stringify({username})}`);
+
+ const json = await res.json();
+ if (res.ok) {
+ return json;
+ } else {
+ throw new Error(json.message);
+ }
+}
+
+export async function getUserInfo() {
+ const res = await fetch(`${config.restServerUri}/api/v1/user/${username}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ const json = await res.json();
+ if (res.ok) {
+ return json;
+ } else {
+ throw new Error(json.message);
+ }
+}
+
+export async function listVirtualClusters() {
+ const res = await fetch(`${config.restServerUri}/api/v1/virtual-clusters`);
+
+ const json = await res.json();
+ if (res.ok) {
+ return json;
+ } else {
+ throw new Error(json.message);
+ }
+}
+
+export async function getTotalGpu() {
+ const res = await fetch(`${config.prometheusUri}/api/v1/query?query=sum(yarn_node_gpu_total)`);
+
+ if (res.ok) {
+ const json = await res.json();
+ const data = get(json, 'data.result[0].value[1]');
+ if (!isNil(data)) {
+ return parseInt(data, 10);
+ } else {
+ throw new Error('Invalid total gpu response');
+ }
+ } else {
+ const json = await res.json();
+ throw new Error(json.error);
+ }
+}
+
+export async function getAvailableGpuPerNode() {
+ const res = await fetch(`${config.prometheusUri}/api/v1/query?query=yarn_node_gpu_available`);
+
+ if (res.ok) {
+ const json = await res.json();
+ try {
+ const result = {};
+ for (const x of json.data.result) {
+ const ip = x.metric.node_ip;
+ const count = parseInt(x.value[1], 10);
+ result[ip] = count;
+ }
+ return result;
+ } catch {
+ throw new Error('Invalid available gpu per node response');
+ }
+ } else {
+ const json = await res.json();
+ throw new Error(json.error);
+ }
+}
diff --git a/src/webportal/src/app/home/home/gpu-chart.jsx b/src/webportal/src/app/home/home/gpu-chart.jsx
new file mode 100644
index 0000000000..8dae145090
--- /dev/null
+++ b/src/webportal/src/app/home/home/gpu-chart.jsx
@@ -0,0 +1,117 @@
+// 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 Chart from 'chart.js';
+import 'chartjs-plugin-datalabels'; // This plugin registers itself globally
+import c from 'classnames';
+import {range} from 'lodash';
+import PropTypes from 'prop-types';
+import {Stack, FontClassNames} from 'office-ui-fabric-react';
+import React, {useEffect, useRef} from 'react';
+
+import Card from './card';
+import {statusColorMapping} from '../../components/theme';
+
+import t from '../../components/tachyons.scss';
+
+const GpuChart = ({className, gpuPerNode}) => {
+ const maxVal = Math.max(...Object.values(gpuPerNode));
+ const data = Array(maxVal + 1).fill(0);
+ for (const key of Object.keys(gpuPerNode)) {
+ data[gpuPerNode[key]] += 1;
+ }
+
+ const chartRef = useRef(null);
+
+ useEffect(() => {
+ new Chart(chartRef.current, {
+ type: 'bar',
+ data: {
+ labels: range(maxVal + 1),
+ datasets: [{
+ backgroundColor: statusColorMapping.succeeded,
+ label: 'nodeCount',
+ data: data,
+ }],
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ legend: {
+ display: false,
+ },
+ tooltips: {
+ enabled: false,
+ },
+ scales: {
+ xAxes: [{
+ scaleLabel: {
+ display: true,
+ labelString: '#GPU',
+ },
+ gridLines: {
+ display: false,
+ },
+ }],
+ yAxes: [{
+ scaleLabel: {
+ display: true,
+ labelString: '#Node',
+ },
+ ticks: {
+ max: Math.max(...data) * 1.2,
+ display: false,
+ },
+ gridLines: {
+ display: false,
+ },
+ }],
+ },
+ plugins: {
+ datalabels: {
+ anchor: 'end',
+ align: 'end',
+ },
+ },
+ },
+ });
+ });
+
+ return (
+
+
+
+
+ Available GPU nodes
+
+
+
+
+
+
+
+
+
+ );
+};
+
+GpuChart.propTypes = {
+ className: PropTypes.string,
+ gpuPerNode: PropTypes.object.isRequired,
+};
+
+export default GpuChart;
diff --git a/src/webportal/src/app/home/home/job-status.jsx b/src/webportal/src/app/home/home/job-status.jsx
new file mode 100644
index 0000000000..682a13cf2c
--- /dev/null
+++ b/src/webportal/src/app/home/home/job-status.jsx
@@ -0,0 +1,159 @@
+// 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 c from 'classnames';
+import {isEmpty} from 'lodash';
+import PropTypes from 'prop-types';
+import querystring from 'querystring';
+import {Icon, Stack, FontClassNames, ColorClassNames, DefaultButton, getTheme} from 'office-ui-fabric-react';
+import React from 'react';
+
+import Card from './card';
+import {getHumanizedJobStateString} from '../../components/util/job';
+
+import t from '../../components/tachyons.scss';
+
+const StatusItem = ({className, icon, name, count, link}) => {
+ const {spacing} = getTheme();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+JobStatus.propTypes = {
+ className: PropTypes.string,
+ jobs: PropTypes.array,
+};
+
+export default JobStatus;
diff --git a/src/webportal/src/app/home/home/recent-job-list.jsx b/src/webportal/src/app/home/home/recent-job-list.jsx
new file mode 100644
index 0000000000..a8ece6a657
--- /dev/null
+++ b/src/webportal/src/app/home/home/recent-job-list.jsx
@@ -0,0 +1,187 @@
+// 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 c from 'classnames';
+import {isEmpty} from 'lodash';
+import {Link, PrimaryButton, DefaultButton, Stack, getTheme, FontClassNames, DetailsList, DetailsListLayoutMode, SelectionMode} from 'office-ui-fabric-react';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Card from './card';
+import {getJobDurationString, getJobModifiedTimeString, getHumanizedJobStateString, getJobModifiedTime} from '../../components/util/job';
+
+import t from '../../components/tachyons.scss';
+import StatusBadge from '../../components/status-badge';
+
+const Header = ({jobs}) => (
+
+ );
+ }
+};
+
+const RecentJobList = ({className, jobs}) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+RecentJobList.propTypes = {
+ className: PropTypes.string,
+ jobs: PropTypes.array.isRequired,
+};
+
+export default RecentJobList;
diff --git a/src/webportal/src/app/home/home/virtual-cluster-list.jsx b/src/webportal/src/app/home/home/virtual-cluster-list.jsx
new file mode 100644
index 0000000000..90574694fc
--- /dev/null
+++ b/src/webportal/src/app/home/home/virtual-cluster-list.jsx
@@ -0,0 +1,152 @@
+// 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 c from 'classnames';
+import PropTypes from 'prop-types';
+import {Stack, ColorClassNames, FontClassNames, PersonaCoin, getTheme} from 'office-ui-fabric-react';
+import React from 'react';
+
+import Card from './card';
+import {statusColorMapping} from '../../components/theme';
+
+import t from '../../components/tachyons.scss';
+
+const VirtualClusterItem = ({name, info, totalGpu}) => {
+ const availableGpu = Math.floor(totalGpu * info.maxCapacity / 100) - info.resourcesUsed.GPUs;
+ const percentage = availableGpu / totalGpu;
+ let color;
+ if (availableGpu === 0) {
+ color = statusColorMapping.failed;
+ } else {
+ color = statusColorMapping.succeeded;
+ }
+
+ const {spacing} = getTheme();
+
+ return (
+
+
+
+
+
+
+ {/* vc item title */}
+
+