diff --git a/components/centraldashboard/.eslintrc.json b/components/centraldashboard/.eslintrc.json index c2592d9d841..eb871cf94ec 100644 --- a/components/centraldashboard/.eslintrc.json +++ b/components/centraldashboard/.eslintrc.json @@ -5,7 +5,8 @@ }, "extends": ["eslint:recommended", "google"], "globals": { - "VERSION": true, + "VERSION": "readonly", + "DEVMODE": "readonly", "Atomics": "readonly", "SharedArrayBuffer": "readonly" }, diff --git a/components/centraldashboard/app/server.ts b/components/centraldashboard/app/server.ts index b2790d6c608..afd70a413fc 100644 --- a/components/centraldashboard/app/server.ts +++ b/components/centraldashboard/app/server.ts @@ -7,12 +7,45 @@ const {PORT, PORT_1} = process.env; const port: number = Number(PORT) || Number(PORT_1) || 8082; const frontEnd: string = resolve(__dirname, "public"); +interface Activity { + time: Date; + event: string; + isError: boolean; + source: string; +} +const actvities = _generateActivities(); +function _generateActivities(): Activity[] { + const activities: Activity[] = []; + const systems: string[] = [ + "serving-system", + "gpu-system", + "training-system" + ]; + const now = Date.now(); + for (let i = 0; i < 100; i++) { + activities.push({ + time: new Date(now - (Math.random() * 86400000)), + event: `Event #${i}`, + isError: Math.random() * 10 <= 1, // 1/10 probability + source: systems[Math.floor(Math.random() * 3)] + }); + } + return activities.sort((a, b) => b.time.getTime() - a.time.getTime()); +} + +app.use(express.json()); app.use(express.static(frontEnd)); app.get("/api", (req: express.Request, res: express.Response) => { console.info(`Request ${req.url} received`); res.send("Hello World"); }); +app.get("/api/activities", (req: express.Request, res: express.Response) => { + res.send(actvities); +}); +app.get("/*", (req: express.Request, res: express.Response) => { + res.sendFile(resolve(frontEnd, "index.html")); +}); app.listen(port, - () => console.info(`Server listening on port http://localhost:${port}`)); \ No newline at end of file + () => console.info(`Server listening on port http://localhost:${port}`)); diff --git a/components/centraldashboard/package-lock.json b/components/centraldashboard/package-lock.json index 4fb51fc187e..06204f076bf 100644 --- a/components/centraldashboard/package-lock.json +++ b/components/centraldashboard/package-lock.json @@ -845,6 +845,14 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-ajax": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-ajax/-/iron-ajax-3.0.1.tgz", + "integrity": "sha512-7+TPEAfWsRdhj1Y8UeF1759ktpVu+c3sG16rJiUC3wF9+woQ9xI1zUm2d59i7Yc3aDEJrR/Q8Y262KlOvyGVNg==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-autogrow-textarea": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.1.tgz", @@ -1022,6 +1030,14 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/iron-range-behavior": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/iron-range-behavior/-/iron-range-behavior-3.0.1.tgz", + "integrity": "sha512-+jtL9v45M/T1RJleWyQaNH84S9/mIIR+AjNbYIttbKGp1eG+98j8MDWe7LXNtg79V2LQnE/+VS82cBeELyGVeg==", + "requires": { + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/iron-resizable-behavior": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/iron-resizable-behavior/-/iron-resizable-behavior-3.0.1.tgz", @@ -1155,6 +1171,17 @@ "@polymer/polymer": "^3.0.0" } }, + "@polymer/paper-progress": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@polymer/paper-progress/-/paper-progress-3.0.1.tgz", + "integrity": "sha512-5nguG+tmnyoaWKVNG8Smtno2uLSPBgEsT3f20JY8yJTjUBYWaqa8E3l5RLkTRXgA4x9OnvLb8/CdlQWXQIogBg==", + "requires": { + "@polymer/iron-flex-layout": "^3.0.0-pre.26", + "@polymer/iron-range-behavior": "^3.0.0-pre.26", + "@polymer/paper-styles": "^3.0.0-pre.26", + "@polymer/polymer": "^3.0.0" + } + }, "@polymer/paper-ripple": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@polymer/paper-ripple/-/paper-ripple-3.0.1.tgz", @@ -4057,7 +4084,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4472,7 +4500,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4528,6 +4557,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4571,12 +4601,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/components/centraldashboard/package.json b/components/centraldashboard/package.json index 4ba98ed150b..3978979b5a0 100644 --- a/components/centraldashboard/package.json +++ b/components/centraldashboard/package.json @@ -31,6 +31,7 @@ "@babel/polyfill": "^7.2.5", "@polymer/app-layout": "^3.0.0", "@polymer/app-route": "^3.0.0", + "@polymer/iron-ajax": "^3.0.1", "@polymer/iron-collapse": "^3.0.1", "@polymer/iron-flex-layout": "^3.0.0", "@polymer/iron-icons": "^3.0.1", @@ -43,11 +44,11 @@ "@polymer/paper-dropdown-menu": "^3.0.1", "@polymer/paper-icon-button": "^3.0.0", "@polymer/paper-item": "^3.0.1", + "@polymer/paper-progress": "^3.0.1", "@polymer/paper-tabs": "^3.0.1", "@polymer/polymer": "^3.1.0", "@types/dotenv": "^6.1.0", "@webcomponents/webcomponentsjs": "^2.0.0", - "body-parser": "^1.18.3", "express": "^4.16.4", "web-animations-js": "^2.3.1" }, diff --git a/components/centraldashboard/public/components/activity-view.js b/components/centraldashboard/public/components/activity-view.js new file mode 100644 index 00000000000..0d6b2a8ad90 --- /dev/null +++ b/components/centraldashboard/public/components/activity-view.js @@ -0,0 +1,113 @@ +import {PolymerElement, html} from '@polymer/polymer'; + +import '@polymer/iron-flex-layout/iron-flex-layout-classes.js'; +import '@polymer/iron-ajax/iron-ajax.js'; +import '@polymer/iron-icon/iron-icon.js'; +import '@polymer/iron-icons/iron-icons.js'; +import '@polymer/paper-progress/paper-progress.js'; + +export class ActivityView extends PolymerElement { + static get template() { + return html` + + + + + +
+ +
+ `; + } + + /** + * Object describing property-related metadata used by Polymer features + */ + static get properties() { + return { + loading: Boolean, + activities: Array, + }; + } + + /** + * Handles the Activities response to set date format and icon. + * @param {Event} responseEvent + */ + _onResponse(responseEvent) { + const {status, response} = responseEvent.detail; + this.activities = []; + // TODO: Surface the error in some manner + if (status !== 200) return; + this.activities = response.map((a) => { + const activity = { + formattedTime: new Date(a.time).toLocaleString(), + icon: a.isError ? 'error' : 'build', + }; + return Object.assign(activity, a); + }); + } +} + +window.customElements.define('activity-view', ActivityView); diff --git a/components/centraldashboard/public/components/dashboard-view.css b/components/centraldashboard/public/components/dashboard-view.css new file mode 100644 index 00000000000..7a2ba4ebba1 --- /dev/null +++ b/components/centraldashboard/public/components/dashboard-view.css @@ -0,0 +1,89 @@ +:host { + @apply --layout-vertical; + background: #f1f3f4; + --accent-color: #007dfc; + --primary-background-color: #003c75; + --sidebar-default-color: #ffffff4f; + --border-color: #f4f4f6; +} + +article { + background: #f1f3f4; + padding: 1em; + grid-gap: 1em; + display: grid; + min-height: 0; + min-width: 0; + --primary-background-color: white; +} + +article:after { + content: ''; + grid-column: 1 span 3 +} + +article>paper-card { + border-radius: 5px; + grid-column: 1 / span 2; + max-width: 100%; + overflow: hidden; + min-width: 0; + --paper-card-header: { + font-family: "Google Sans" + } +} + +article>paper-card.thin { + grid-column: 3; + min-width: 19em; +} + +#Getting-Started paper-icon-item:not(:last-of-type) { + border-bottom: 1px solid var(--border-color); +} + +#Getting-Started paper-icon-item iron-icon { + color: var(--accent-color) +} + +#Getting-Started [secondary] { + word-break: break-word; + width: 100%; + white-space: normal; + font-size: .8em; +} + +#Quick-Links { + grid-column: 3 +} + +#Quick-Links .link { + width: 80%; + margin: .5em auto; + border: 1px solid #eeeeef; + padding: .5em 1em; + border-radius: 5px; + @apply --layout-horizontal; +} + +#Quick-Links .link.more-coming { + opacity: .4; + font-style: italic; + pointer-events: none +} + +#Quick-Links .link .button { + color: var(--accent-color); + background: rgba(0, 125, 252, 0.25); + border-radius: 50% +} + +a { + text-decoration: none; + color: initial; +} + +.header:hover { + color: var(--paper-blue-700); + text-decoration: underline; +} diff --git a/components/centraldashboard/public/components/dashboard-view.js b/components/centraldashboard/public/components/dashboard-view.js new file mode 100644 index 00000000000..653c49b1d8e --- /dev/null +++ b/components/centraldashboard/public/components/dashboard-view.js @@ -0,0 +1,71 @@ +import {html, PolymerElement} from '@polymer/polymer'; + +import css from './dashboard-view.css'; + +import template from './dashboard-view.pug'; + +export class DashboardView extends PolymerElement { + static get template() { + return html([` ${template()}`]); + } + + /** + * Object describing property-related metadata used by Polymer features + */ + static get properties() { + const kubeflowDocs = 'https://www.kubeflow.org/docs/started'; + + return { + gettingStartedItems: { + type: Array, + value: [ + { + text: 'Getting started with Kubeflow', + desc: 'Quickly get running with your ML workflow on ' + + 'an existing Kubernetes installation', + link: `${kubeflowDocs}/getting-started/`, + }, + { + text: 'Microk8s for Kubeflow', + desc: 'Quickly get Kubeflow running locally on ' + + 'native hypervisors', + link: `${kubeflowDocs}/getting-started-multipass/`, + }, + { + text: 'Minikube for Kubeflow', + desc: 'Quickly get Kubeflow running locally', + link: `${kubeflowDocs}/getting-started-minikube/`, + }, + { + text: 'Kubernetes Engine for Kubeflow', + desc: 'Get Kubeflow running on Google Cloud ' + + 'Platform. This guide is a quickstart to deploying Kubeflow ' + + 'on Google Kubernetes Engine', + link: `${kubeflowDocs}/getting-started-gke/`, + }, + { + text: 'Requirements for Kubeflow', + desc: 'Get more detailed information about using ' + + 'Kubeflow and its components', + link: `${kubeflowDocs}/requirements/`, + }, + ], + }, + quickLinks: { + type: Array, + value: [ + { + text: 'Open docs', + link: `${kubeflowDocs}/getting-started/`, + }, + { + text: 'Open Github', + link: 'https://github.com/kubeflow/kubeflow', + }, + ], + }, + }; + } +} + +window.customElements.define('dashboard-view', DashboardView); diff --git a/components/centraldashboard/public/components/dashboard-view.pug b/components/centraldashboard/public/components/dashboard-view.pug new file mode 100644 index 00000000000..7eb3e1f6e67 --- /dev/null +++ b/components/centraldashboard/public/components/dashboard-view.pug @@ -0,0 +1,19 @@ +article + paper-card#Getting-Started(heading='Getting Started') + template(is='dom-repeat', items='[[gettingStartedItems]]') + a.heading(href$='[[item.link]]', tabindex='-1', + target='_blank') + paper-icon-item + iron-icon(icon='launch', slot='item-icon') + paper-item-body(two-line) + .header [[item.text]] + aside(secondary) [[item.desc]] + paper-card.thin#Quick-Links(heading='Quick Links') + template(is='dom-repeat', items='[[quickLinks]]') + article.link + paper-item-body [[item.text]] + a(href$='[[item.link]]', tabindex='-1', target='_blank') + paper-icon-button.button(icon='arrow-forward', alt='[[item.text]]') + article.link.more-coming + paper-item-body More coming soon + paper-icon-button.button(icon='arrow-forward', disabled) diff --git a/components/centraldashboard/public/components/main-page.css b/components/centraldashboard/public/components/main-page.css index 6bc71f558ee..68f816bf166 100644 --- a/components/centraldashboard/public/components/main-page.css +++ b/components/centraldashboard/public/components/main-page.css @@ -1,4 +1,7 @@ -*, :host, :host * {box-sizing: border-box} +*, :host, :host * { + box-sizing: border-box +} + :host { @apply --layout-vertical; --accent-color: #007dfc; @@ -6,12 +9,17 @@ --sidebar-default-color: #ffffff4f; --border-color: #f4f4f6; } -.flex {flex: 1} + +.flex { + flex: 1 +} + .bottom { margin-top: auto; @apply --layout-horizontal; } -#MainDrawer { + +app-drawer { color: white; background: var(--primary-background-color); --app-drawer-content-container: { @@ -19,19 +27,55 @@ @apply --layout-vertical; } } -#MainDrawer .menu-item { + +app-drawer .menu-item { cursor: pointer; transition: background .25s; font-family: Google Sans; color: var(--sidebar-default-color); } -#MainDrawer .menu-item+.divider {width: 90%;margin: 1em auto;border-bottom: 2px solid var(--sidebar-default-color)} -#MainDrawer .menu-item:hover {background: #ffffff1b} -#MainDrawer .menu-item.iron-selected {background: #ffffff3b;color: white;font-weight: 100} -#MainDrawer .footer {padding: 1.25rem;font-size: .9em;color: var(--sidebar-default-color);line-height: 1.5em} -#MainDrawer .footer > .build {font-size: .9em;font-style: italic;color: rgba(255, 255, 255, 0.64)} -#NamespaceSelector {position: absolute;left: 1em;top: 50%;transform: translateY(-50%)} -app-toolbar {display: flex;justify-content: center} + +app-drawer .menu-item+.divider { + width: 90%; + margin: 1em auto; + border-bottom: 2px solid var(--sidebar-default-color) +} + +app-drawer .menu-item:hover { + background: #ffffff1b +} + +app-drawer .iron-selected>.menu-item { + background: #ffffff3b; + color: white; + font-weight: 100 +} + +app-drawer .footer { + padding: 1.25rem; + font-size: .9em; + color: var(--sidebar-default-color); + line-height: 1.5em +} + +app-drawer .footer>.build { + font-size: .9em; + font-style: italic; + color: rgba(255, 255, 255, 0.64) +} + +#NamespaceSelector { + position: absolute; + left: 1em; + top: 50%; + transform: translateY(-50%) +} + +app-toolbar { + display: flex; + justify-content: center +} + paper-tabs { --paper-tabs-selection-bar-color: var(--accent-color); --paper-tab-ink: var(--accent-color); @@ -40,30 +84,71 @@ paper-tabs { border-width: 3px; } } -paper-tab {padding: 0 2em} -paper-tab.iron-selected {color: var(--accent-color)} -app-header-layout {@apply --layout-vertical} -#Menu {position: absolute;right: 10px;top: 50%;transform: translateY(-50%)} + +paper-tab { + padding: 0 2em +} + +paper-tab.iron-selected { + color: var(--accent-color) +} + +paper-tab[link] a { + /* These mixins (from iron-flex-layout) center the link text. */ + @apply --layout-horizontal; + @apply --layout-center-center; + color: var(--paper-tabs-selection-bar); + text-decoration: none; +} + +app-header-layout { + @apply --layout-vertical +} + +neon-animated-pages { + height: 100%; +} + +neon-animatable { + @apply --layout-vertical; + height: 100%; +} + +neon-animatable#iframe-page { + top: -64px; +} + +#Menu { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%) +} + #PageFrame { border: 0; display: inline-block; width: 100%; height: 100%; } + #Logo { display: flex; align-items: center; margin: 0 0 1em; padding: 2em; } -#Logo > img { + +#Logo>img { width: 3em; margin-right: 1em; } -#Logo > figcaption { + +#Logo>figcaption { font-size: 1.3em; font-family: Google Sans; } + #Dashboard { background: #f1f3f4; padding: 1em; @@ -73,7 +158,8 @@ app-header-layout {@apply --layout-vertical} min-width: 0; --primary-background-color: white; } -#Dashboard > paper-card { + +#Dashboard>paper-card { border-radius: 5px; grid-column: 1 / span 2; max-width: 100%; @@ -83,27 +169,37 @@ app-header-layout {@apply --layout-vertical} font-family: "Google Sans" } } -#Dashboard > paper-card.thin {grid-column: 3;min-width: 19em} -#Dashboard > .Getting-Started paper-icon-item { + +#Dashboard>paper-card.thin { + grid-column: 3; + min-width: 19em +} + +#Dashboard>.Getting-Started paper-icon-item { cursor: pointer; } -#Dashboard > .Getting-Started paper-icon-item:not(:last-of-type) { + +#Dashboard>.Getting-Started paper-icon-item:not(:last-of-type) { border-bottom: 1px solid var(--border-color); } -#Dashboard > .Getting-Started paper-icon-item iron-icon { + +#Dashboard>.Getting-Started paper-icon-item iron-icon { color: var(--accent-color) } -#Dashboard > .Getting-Started paper-icon-item:hover .heading { + +#Dashboard>.Getting-Started paper-icon-item:hover .heading { color: var(--paper-blue-700); text-decoration: underline; } -#Dashboard > .Getting-Started [secondary] { -word-break: break-word; + +#Dashboard>.Getting-Started [secondary] { + word-break: break-word; width: 100%; white-space: normal; font-size: .8em; } -#Dashboard > .Quick-Links .link { + +#Dashboard>.Quick-Links .link { width: 80%; margin: .5em auto; border: 1px solid #eeeeef; @@ -111,21 +207,63 @@ word-break: break-word; border-radius: 5px; @apply --layout-horizontal; } -#Dashboard > .Quick-Links .link.more-coming {opacity: .4;font-style: italic;pointer-events: none} -#Dashboard > .Quick-Links .link .button {color: var(--accent-color);background: rgba(0, 125, 252, 0.25);border-radius: 50%} -#Dashboard:after {content: '';grid-column: 1 span 3} -#Activity {@apply --layout-vertical;@apply --layout-center-center} -#Activity:before, #Activity:after {font-size: 3em;opacity: .3;font-family: Google Sans} -#Activity:before {content: ":(";font-size: 6em;transform: rotateZ(90deg)} -#Activity:after {content: "No content here yet..."} - -[hides] {transition: opacity .25s} -[hidden] {opacity: 0;pointer-events: none} -[hidden]:not([hides]) {display: none} + +#Dashboard>.Quick-Links .link.more-coming { + opacity: .4; + font-style: italic; + pointer-events: none +} + +#Dashboard>.Quick-Links .link .button { + color: var(--accent-color); + background: rgba(0, 125, 252, 0.25); + border-radius: 50% +} + +#Dashboard:after { + content: ''; + grid-column: 1 span 3 +} + +#Activity { + @apply --layout-vertical; + @apply --layout-center-center +} + +#Activity:before, #Activity:after { + font-size: 3em; + opacity: .3; + font-family: Google Sans +} + +#Activity:before { + content: ":("; + font-size: 6em; + transform: rotateZ(90deg) +} + +#Activity:after { + content: "No content here yet..." +} + +[hides] { + transition: opacity .25s +} + +[hidden] { + opacity: 0; + pointer-events: none +} + +[hidden]:not([hides]) { + display: none +} + a[href] { text-decoration: none; color: initial; } + a[href]:hover { color: var(--paper-blue-700); text-decoration: underline; diff --git a/components/centraldashboard/public/components/main-page.js b/components/centraldashboard/public/components/main-page.js index c4bffa2a63e..a7b2ccdd1e4 100644 --- a/components/centraldashboard/public/components/main-page.js +++ b/components/centraldashboard/public/components/main-page.js @@ -1,6 +1,3 @@ -/* eslint-disable max-len */ -import {PolymerElement, html} from '@polymer/polymer/polymer-element.js'; - import '@polymer/app-layout/app-drawer/app-drawer.js'; import '@polymer/app-layout/app-drawer-layout/app-drawer-layout.js'; import '@polymer/app-layout/app-header/app-header.js'; @@ -12,7 +9,8 @@ import '@polymer/app-route/app-route.js'; import '@polymer/iron-icons/iron-icons.js'; import '@polymer/iron-collapse/iron-collapse.js'; import '@polymer/iron-selector/iron-selector.js'; -import '@polymer/iron-flex-layout/iron-flex-layout'; +import '@polymer/iron-flex-layout/iron-flex-layout-classes.js'; +import '@polymer/iron-flex-layout/iron-flex-layout.js'; import '@polymer/paper-card/paper-card.js'; import '@polymer/paper-tabs/paper-tabs.js'; import '@polymer/paper-item/paper-item.js'; @@ -25,112 +23,135 @@ import '@polymer/neon-animation/neon-animated-pages.js'; import '@polymer/neon-animation/animations/fade-in-animation.js'; import '@polymer/neon-animation/animations/fade-out-animation.js'; +import {html, PolymerElement} from '@polymer/polymer/polymer-element.js'; + import css from './main-page.css'; + import template from './main-page.pug'; +import './dashboard-view.js'; +import './activity-view.js'; + /** * Entry point for application UI. */ export class MainPage extends PolymerElement { static get template() { - return html([` ${template()}`]); + return html([` + ${template()} + `]); } static get properties() { return { - links: { + page: String, + routeData: Object, + subRouteData: Object, + iframeRoute: Object, + menuLinks: { type: Array, value: [ - {text: 'Home', defaultPage: true, hasDivider: true}, { - link: 'https://www.kubeflow.org/docs/about/kubeflow/', + iframeUrl: 'https://www.kubeflow.org/docs/about/kubeflow/', text: 'Kubeflow docs', + href: '/docs', }, - {link: '/jupyter/', text: 'Notebooks'}, - {link: '/tfjobs/ui/', text: 'TFJob Dashboard'}, - {link: '/katib/', text: 'Katib Dashboard'}, - {link: '/pipeline/', text: 'Pipeline Dashboard'}, - ], - }, - gettingStartedItems: { - type: Array, - value: [ { - text: 'Getting started with Kubeflow', - desc: 'Quickly get running with your ML workflow on an existing Kubernetes installation', - link: 'https://www.kubeflow.org/docs/started/getting-started/', - icon: 'launch', - }, { - text: 'Microk8s for Kubeflow', - desc: 'Quickly get Kubeflow running locally on native hypervisors', - link: 'https://www.kubeflow.org/docs/started/getting-started-multipass/', - icon: 'launch', - }, { - text: 'Minikube for Kubeflow', - desc: 'Quickly get Kubeflow running locally', - link: 'https://www.kubeflow.org/docs/started/getting-started-minikube/', - icon: 'launch', - }, { - text: 'Kubernetes Engine for Kubeflow', - desc: 'Get Kubeflow running on Google Cloud Platform. This guide is a quickstart to deploying Kubeflow on Google Kubernetes Engine', - link: 'https://www.kubeflow.org/docs/started/getting-started-gke/', - icon: 'launch', - }, { - text: 'Requirements for Kubeflow', - desc: 'Get more detailed information about using Kubeflow and its components', - link: 'https://www.kubeflow.org/docs/started/requirements/', - icon: 'launch', + iframeUrl: '/jupyter/', + text: 'Notebooks', + href: '/notebooks', + }, + { + iframeUrl: '/tfjobs/ui/', + text: 'TFJob Dashboard', + href: '/tjob-dashboard', }, - ], - }, - quickLinks: { - type: Array, - value: [ { - text: 'Open docs', - link: 'https://www.kubeflow.org/docs/started/getting-started/', + iframeUrl: '/katib/', + text: 'Katib Dashboard', + href: '/katib-dashboard', }, { - text: 'Open Github', - link: 'https://github.com/kubeflow/kubeflow', + iframeUrl: '/pipeline/', + text: 'Pipeline Dashboard', + href: '/pipeline-dashboard', }, ], }, + hideToolbar: {type: Boolean, value: false}, sidebarItemIndex: {type: Number, value: 0}, - primaryViewIndex: {type: Number, value: 0}, - homeOrIframeViewIndex: {type: Number, value: 0}, - url: {type: String, value: ''}, + iframeUrl: {type: String, value: ''}, buildVersion: {type: String, value: '0.4.1'}, dashVersion: {type: String, value: VERSION}, - _devMode: {type: Boolean, value: false}, + _devMode: {type: Boolean, value: DEVMODE}, }; } - openExternalLink(href) { - const a = document.createElement('a'); - a.href = href; - a.target = '_blank'; - a.click(); + /** + * Array of strings describing multi-property observer methods and their + * dependant properties + */ + static get observers() { + return [ + '_routePageChanged(routeData.page)', + ]; } - openQuickLink(e) { - const {link} = e.model.item; - this.openExternalLink(link); - } - - openLink(e) { - const {link, defaultPage} = e.model.item; - this.homeOrIframeViewIndex = defaultPage ? 0 : 1; - if (defaultPage) return; - this.url = link; + /** + * Intercepts any external links and ensures that they are captured in + * the route and sent to the iframe source. + * @param {MouseEvent} e + */ + openInIframe(e) { + const url = new URL(e.currentTarget.href); + window.history.pushState({}, null, `_${url.pathname}`); + window.dispatchEvent(new CustomEvent('location-changed')); + e.preventDefault(); } toggleSidebar() { this.$.MainDrawer.toggle(); } - isZero(i) { - return i === 0; + /** + * Handles route changes by evaluating the page path component + * @param {string} newPage + */ + _routePageChanged(newPage) { + this.hideToolbar = false; + switch (newPage) { + case 'activity': + this.sidebarItemIndex = 0; + this.page = 'activity'; + break; + case '_': // iframe case + this._setIframeFromRoute(this.subRouteData.path); + break; + default: + this.sidebarItemIndex = 0; + this.page = 'dashboard'; + } + } + + /** + * Sets the iframeUrl and sidebarItem based on the subpage component + * provided. + * @param {string} href + */ + _setIframeFromRoute(href) { + const menuLinkIndex = + this.menuLinks.findIndex((m) => m.href === this.subRouteData.path); + if (menuLinkIndex >= 0) { + this.page = 'iframe'; + this.iframeUrl = this.menuLinks[menuLinkIndex].iframeUrl; + this.sidebarItemIndex = menuLinkIndex + 1; + this.hideToolbar = true; + } else { + this.sidebarItemIndex = 0; + this.page = 'dashboard'; + } } } diff --git a/components/centraldashboard/public/components/main-page.pug b/components/centraldashboard/public/components/main-page.pug index e180c877b29..8430f40eee7 100644 --- a/components/centraldashboard/public/components/main-page.pug +++ b/components/centraldashboard/public/components/main-page.pug @@ -1,44 +1,44 @@ app-drawer-layout.flex(narrow='{{narrowMode}}') + app-location(route='{{route}}') + app-route(route='{{route}}', pattern='/:page', data='{{routeData}}', + tail='{{subRouteData}}') + app-drawer#MainDrawer(slot='drawer') figure#Logo img(alt='Kubeflow Logo', src='assets/kf-logo_64px.svg') figcaption Kubeflow - iron-selector#SidebarSelector(selected='{{sidebarItemIndex}}') - template(is='dom-repeat', items='[[links]]') - paper-item.menu-item(on-click='openLink') [[item.text]] - template(is='dom-if', if='[[item.hasDivider]]') - aside.divider + iron-selector(selected='{{sidebarItemIndex}}') + a(href='/', tabindex='-1') + paper-item.menu-item Home + aside.divider + template(is='dom-repeat', items='[[menuLinks]]') + a(href$='[[item.href]]', on-click='openInIframe', tabindex='-1') + paper-item.menu-item [[item.text]] aside.flex footer.footer section.privacy Privacy - section.build build version + section.build build version  span(title="Build: v[[buildVersion]] | Dashboard: v[[dashVersion]]") v[[buildVersion]] app-header-layout(fullbleed) - app-header(slot='header', fixed) + app-header(slot='header') app-toolbar - paper-tabs.bottom(selected='{{primaryViewIndex}}', hides, hidden$='[[!isZero(homeOrIframeViewIndex)]]') - paper-tab Dashboard - paper-tab(hidden$='[[!_devMode]]') Activity - aside#NamespaceSelector(hidden$='[[!_devMode]]') - paper-dropdown-menu(label='Namespace') - paper-icon-button#Menu(icon='menu', on-click='toggleSidebar', hides, hidden$='[[!narrowMode]]') - neon-animated-pages.flex.layout.vertical(selected='[[homeOrIframeViewIndex]]', entry-animation='fade-in-animation', exit-animation='fade-out-animation') - neon-animated-pages#PrimaryView.flex.layout.vertical(selected='[[primaryViewIndex]]', entry-animation='fade-in-animation', exit-animation='fade-out-animation') - article#Dashboard - paper-card.Getting-Started(heading='Getting Started') - template(is='dom-repeat', items='[[gettingStartedItems]]') - paper-icon-item(on-click='openQuickLink') - iron-icon(icon='[[item.icon]]', slot='item-icon') - paper-item-body(two-line) - .heading [[item.text]] - aside(secondary) [[item.desc]] - paper-card.thin.Quick-Links(heading='Quick Links') - template(is='dom-repeat', items='[[quickLinks]]') - article.link - paper-item-body [[item.text]] - paper-icon-button.button(icon='arrow-forward', alt='[[item.text]]', on-click='openQuickLink') - article.link.more-coming - paper-item-body More coming soon - paper-icon-button.button(icon='arrow-forward', diabled) - article#Activity - iframe#PageFrame.flex(src='[[url]]') + header(hides, hidden$='[[hideToolbar]]') + paper-tabs.bottom(selected='[[page]]', attr-for-selected='page') + paper-tab(page='dashboard', link) + a.link(tabindex='-1', href='/') Dashboard + paper-tab(page='activity', link,hidden$='[[!_devMode]]') + a.link(tabindex='-1', href='/activity') Activity + aside#NamespaceSelector(hidden$='[[!_devMode]]') + paper-dropdown-menu(label='Namespace') + paper-icon-button#Menu(icon='menu', + on-click='toggleSidebar', hides, + hidden$='[[!narrowMode]]') + neon-animated-pages(selected='[[page]]', attr-for-selected='page', + entry-animation='fade-in-animation', + exit-animation='fade-out-animation') + neon-animatable(page='dashboard') + dashboard-view + neon-animatable(page='activity') + activity-view + neon-animatable#iframe-page(page='iframe') + iframe#PageFrame.flex(src='[[iframeUrl]]') diff --git a/components/centraldashboard/webpack.config.js b/components/centraldashboard/webpack.config.js index 9dc002c05f8..ba469c7da53 100644 --- a/components/centraldashboard/webpack.config.js +++ b/components/centraldashboard/webpack.config.js @@ -1,4 +1,3 @@ -/* eslint-disable linebreak-style,no-undef */ 'use strict'; const {resolve} = require('path'); @@ -114,7 +113,6 @@ module.exports = { 'not op_mini all', ], }, - debug: true, }, ]], plugins: ['@babel/plugin-transform-runtime'], @@ -153,6 +151,7 @@ module.exports = { new CopyWebpackPlugin(POLYFILLS), new DefinePlugin({ VERSION: JSON.stringify(PKG_VERSION), + DEVMODE: JSON.stringify(ENV == 'development'), }), new HtmlWebpackPlugin({ filename: resolve(DESTINATION, 'index.html'), @@ -178,5 +177,8 @@ module.exports = { devServer: { port: 8081, proxy: {'/api': 'http://localhost:8082'}, + historyApiFallback: { + disableDotRule: true, + }, }, };