[UI] Getting started page for hosted pipelines (#2935)

* Implement getting started page.

* Add feature flag to only show getting started page on hosted pipelines

* Add tests

* Fix format

* Implement requested layout in getting started page

* Minor adjust layout

* Fix tests

* Fix snapshots

* Update page title
This commit is contained in:
Yuan (Bob) Gong 2020-02-01 04:35:20 +08:00 committed by GitHub
parent c83aff2738
commit 4709c6b42f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 219 additions and 35 deletions

View File

@ -24,7 +24,6 @@ import { Storage as GCSStorage } from '@google-cloud/storage';
import { UIServer } from './app';
import { loadConfigs } from './configs';
import * as minioHelper from './minio-helper';
import { getTensorboardInstance } from './k8s-helper';
jest.mock('minio');
jest.mock('node-fetch');
@ -46,15 +45,6 @@ describe('UIServer apis', () => {
</script>
<script id="kubeflow-client-placeholder"></script>
</head>
</html>`;
const expectedIndexHtml = `
<html>
<head>
<script>
window.KFP_FLAGS.DEPLOYMENT="KUBEFLOW"
</script>
<script id="kubeflow-client-placeholder" src="/dashboard_lib.bundle.js"></script>
</head>
</html>`;
beforeAll(() => {
@ -92,6 +82,15 @@ describe('UIServer apis', () => {
});
it('responds with a modified index.html if it is a kubeflow deployment', done => {
const expectedIndexHtml = `
<html>
<head>
<script>
window.KFP_FLAGS.DEPLOYMENT="KUBEFLOW"
</script>
<script id="kubeflow-client-placeholder" src="/dashboard_lib.bundle.js"></script>
</head>
</html>`;
const configs = loadConfigs(argv, { DEPLOYMENT: 'kubeflow' });
app = new UIServer(configs);
@ -101,6 +100,26 @@ describe('UIServer apis', () => {
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(200, expectedIndexHtml, done);
});
it('responds with flag DEPLOYMENT=MARKETPLACE if it is a marketplace deployment', done => {
const expectedIndexHtml = `
<html>
<head>
<script>
window.KFP_FLAGS.DEPLOYMENT="MARKETPLACE"
</script>
<script id="kubeflow-client-placeholder"></script>
</head>
</html>`;
const configs = loadConfigs(argv, { DEPLOYMENT: 'marketplace' });
app = new UIServer(configs);
const request = requests(app.start());
request
.get('/')
.expect('Content-Type', 'text/html; charset=utf-8')
.expect(200, expectedIndexHtml, done);
});
});
describe('/apis/v1beta1/healthz', () => {

View File

@ -20,6 +20,7 @@ export const apiVersionPrefix = `apis/${apiVersion}`;
export enum Deployments {
NOT_SPECIFIED = 'NOT_SPECIFIED',
KUBEFLOW = 'KUBEFLOW',
MARKETPLACE = 'MARKETPLACE',
}
/** converts string to bool */
@ -132,8 +133,10 @@ export function loadConfigs(
apiVersionPrefix,
basePath: BASEPATH,
deployment:
DEPLOYMENT_STR.toUpperCase() === 'KUBEFLOW'
DEPLOYMENT_STR.toUpperCase() === Deployments.KUBEFLOW
? Deployments.KUBEFLOW
: DEPLOYMENT_STR.toUpperCase() === Deployments.MARKETPLACE
? Deployments.MARKETPLACE
: Deployments.NOT_SPECIFIED,
port,
staticDir,

View File

@ -66,5 +66,8 @@ function replaceRuntimeContent(content: string | undefined, deployment: Deployme
`<script id="kubeflow-client-placeholder" src="/dashboard_lib.bundle.js"></script>`,
);
}
if (content && deployment === Deployments.MARKETPLACE) {
return content.replace(DEFAULT_FLAG, 'window.KFP_FLAGS.DEPLOYMENT="MARKETPLACE"');
}
return content;
}

View File

@ -16,4 +16,4 @@ const css = stylesheet({
export const ExternalLink: React.FC<
DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>
> = props => <a {...props} className={css.link} target='_blank' rel='noreferrer noopener' />;
> = props => <a {...props} className={css.link} target='_blank' rel='noopener' />;

View File

@ -43,6 +43,8 @@ import { Route, Switch, Redirect } from 'react-router-dom';
import { classes, stylesheet } from 'typestyle';
import { commonCss } from '../Css';
import NewPipelineVersion from '../pages/NewPipelineVersion';
import { GettingStarted } from '../pages/GettingStarted';
import { KFP_FLAGS, Deployments } from '../lib/Flags';
export type RouteConfig = { path: string; Component: React.ComponentType<any>; view?: any };
@ -102,6 +104,7 @@ export const RoutePage = {
RECURRING_RUN: `/recurringrun/details/:${RouteParams.runId}`,
RUNS: '/runs',
RUN_DETAILS: `/runs/details/:${RouteParams.runId}`,
START: '/start',
};
export const RoutePageFactory = {
@ -139,9 +142,13 @@ export interface RouterProps {
configs?: RouteConfig[]; // only used in tests
}
const DEFAULT_ROUTE =
KFP_FLAGS.DEPLOYMENT === Deployments.MARKETPLACE ? RoutePage.START : RoutePage.PIPELINES;
// This component is made as a wrapper to separate toolbar state for different pages.
const Router: React.FC<RouterProps> = ({ configs }) => {
const routes: RouteConfig[] = configs || [
{ path: RoutePage.START, Component: GettingStarted },
{ path: RoutePage.ARCHIVE, Component: Archive },
{ path: RoutePage.ARTIFACTS, Component: ArtifactList },
{ path: RoutePage.ARTIFACT_DETAILS, Component: ArtifactDetails },
@ -170,7 +177,7 @@ const Router: React.FC<RouterProps> = ({ configs }) => {
<Route
exact={true}
path={'/'}
render={({ ...props }) => <Redirect to={RoutePage.PIPELINES} {...props} />}
render={({ ...props }) => <Redirect to={DEFAULT_ROUTE} {...props} />}
/>
{/* Normal routes */}

View File

@ -36,6 +36,7 @@ import { RouterProps } from 'react-router';
import { classes, stylesheet } from 'typestyle';
import { fontsize, commonCss } from '../Css';
import { logger } from '../lib/Utils';
import { KFP_FLAGS, Deployments } from '../lib/Flags';
export const sideNavColors = {
bg: '#f8fafb',
@ -267,6 +268,39 @@ export default class SideNav extends React.Component<SideNavProps, SideNavState>
)}
>
<div style={{ flexGrow: 1 }}>
{KFP_FLAGS.DEPLOYMENT === Deployments.MARKETPLACE && (
<>
<div
className={classes(
css.indicator,
!page.startsWith(RoutePage.START) && css.indicatorHidden,
)}
/>
<Tooltip
title={'Getting Started'}
enterDelay={300}
placement={'right-start'}
disableFocusListener={!collapsed}
disableHoverListener={!collapsed}
disableTouchListener={!collapsed}
>
<Link id='gettingStartedBtn' to={RoutePage.START} className={commonCss.unstyled}>
<Button
className={classes(
css.button,
page.startsWith(RoutePage.START) && css.active,
collapsed && css.collapsedButton,
)}
>
<DescriptionIcon />
<span className={classes(collapsed && css.collapsedLabel, css.label)}>
Getting Started
</span>
</Button>
</Link>
</Tooltip>
</>
)}
<div
className={classes(
css.indicator,

View File

@ -4,7 +4,7 @@ exports[`Description When in inline mode renders markdown link 1`] = `
<a
class="link"
href="https://www.google.com"
rel="noreferrer noopener"
rel="noopener"
target="_blank"
>
google
@ -39,7 +39,7 @@ exports[`Description When in inline mode renders raw link 1`] = `
<a
class="link"
href="https://www.google.com"
rel="noreferrer noopener"
rel="noopener"
target="_blank"
>
https://www.google.com
@ -52,7 +52,7 @@ exports[`Description When in normal mode renders markdown link 1`] = `
<a
class="link"
href="https://www.google.com"
rel="noreferrer noopener"
rel="noopener"
target="_blank"
>
google
@ -91,7 +91,7 @@ exports[`Description When in normal mode renders raw link 1`] = `
<a
class="link"
href="https://www.google.com"
rel="noreferrer noopener"
rel="noopener"
target="_blank"
>
https://www.google.com

View File

@ -10,102 +10,108 @@ exports[`Router initial render 1`] = `
<Route
exact={true}
key="0"
path="/archive"
path="/start"
render={[Function]}
/>
<Route
exact={true}
key="1"
path="/artifacts"
path="/archive"
render={[Function]}
/>
<Route
exact={true}
key="2"
path="/artifact_types/:artifactType+/artifacts/:id"
path="/artifacts"
render={[Function]}
/>
<Route
exact={true}
key="3"
path="/executions"
path="/artifact_types/:artifactType+/artifacts/:id"
render={[Function]}
/>
<Route
exact={true}
key="4"
path="/execution_types/:executionType+/executions/:id"
path="/executions"
render={[Function]}
/>
<Route
exact={true}
key="5"
path="/experiments"
path="/execution_types/:executionType+/executions/:id"
render={[Function]}
/>
<Route
exact={true}
key="6"
path="/experiments/details/:eid"
path="/experiments"
render={[Function]}
/>
<Route
exact={true}
key="7"
path="/experiments/new"
path="/experiments/details/:eid"
render={[Function]}
/>
<Route
exact={true}
key="8"
path="/pipeline_versions/new"
path="/experiments/new"
render={[Function]}
/>
<Route
exact={true}
key="9"
path="/runs/new"
path="/pipeline_versions/new"
render={[Function]}
/>
<Route
exact={true}
key="10"
path="/pipelines"
path="/runs/new"
render={[Function]}
/>
<Route
exact={true}
key="11"
path="/pipelines/details/:pid/version/:vid?"
path="/pipelines"
render={[Function]}
/>
<Route
exact={true}
key="12"
path="/pipelines/details/:pid?"
path="/pipelines/details/:pid/version/:vid?"
render={[Function]}
/>
<Route
exact={true}
key="13"
path="/runs"
path="/pipelines/details/:pid?"
render={[Function]}
/>
<Route
exact={true}
key="14"
path="/recurringrun/details/:rid"
path="/runs"
render={[Function]}
/>
<Route
exact={true}
key="15"
path="/runs/details/:rid"
path="/recurringrun/details/:rid"
render={[Function]}
/>
<Route
exact={true}
key="16"
path="/runs/details/:rid"
render={[Function]}
/>
<Route
exact={true}
key="17"
path="/compare"
render={[Function]}
/>

View File

@ -1,11 +1,18 @@
export enum Deployments {
KUBEFLOW = 'KUBEFLOW',
MARKETPLACE = 'MARKETPLACE',
}
export const KFP_FLAGS = {
DEPLOYMENT:
// tslint:disable-next-line:no-string-literal
window && window['KFP_FLAGS'] && window['KFP_FLAGS']['DEPLOYMENT'] === Deployments.KUBEFLOW
? Deployments.KUBEFLOW
window && window['KFP_FLAGS']
? // tslint:disable-next-line:no-string-literal
window['KFP_FLAGS']['DEPLOYMENT'] === Deployments.KUBEFLOW
? Deployments.KUBEFLOW
: // tslint:disable-next-line:no-string-literal
window['KFP_FLAGS']['DEPLOYMENT'] === Deployments.MARKETPLACE
? Deployments.MARKETPLACE
: undefined
: undefined,
};

View File

@ -0,0 +1,103 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import Buttons from '../lib/Buttons';
import { Page } from './Page';
import { ToolbarProps } from '../components/Toolbar';
import Markdown from 'markdown-to-jsx';
import { ExternalLink } from '../atoms/ExternalLink';
import { cssRaw, classes } from 'typestyle';
import { commonCss, padding } from '../Css';
const options = {
overrides: { a: { component: ExternalLink } },
};
const PAGE_CONTENT_MD = `
## Build your own pipeline
Build a end-to-end ML pipeline with TFX [Start Here](https://console.cloud.google.com/mlengine/notebooks/deploy-notebook?q=download_url%3Dhttps%253A%252F%252Fraw.githubusercontent.com%252Fkubeflow%252Fpipelines%252F0.1.40%252Fsamples%252Fcore%252Fparameterized_tfx_oss%252Ftaxi_pipeline_notebook.ipynb) (Alpha)
## Demos and Tutorials
This section contains demo and tutorial pipelines.
**Demos** - Try an end-to-end demonstration pipeline.
* [TFX pipeline demo](https://www.google.com) \\- A trainer that does end-to-end distributed training for XGBoost models. [source code](https://github.com/kubeflow/pipelines/tree/master/samples/core/parameterized_tfx_oss)
* [XGBoost Pipeline](https://www.google.com) \\- Example pipeline that does classification with model analysis based on a public taxi cab BigQuery dataset. [source code](https://github.com/kubeflow/pipelines/tree/master/samples/core/xgboost_training_cm)
**Tutorials** - Learn pipeline concepts by following a tutorial.
* [Name of Tutorial 1] - \\<tutorial 1 description\\>. [source code]()
* [Name of Tutorial 2] - \\<tutorial 2 description\\>. [source code]()
You can find additional tutorials and samples [here]()
### Additional resources and documentation
* [TFX Landing page]()
* [Hosted Pipeline documentation]()
* [Troubleshooting guide]()
`;
cssRaw(`
.kfp-start-page li {
font-size: 14px;
margin-block-start: 0.83em;
margin-block-end: 0.83em;
margin-left: 2em;
}
.kfp-start-page p {
font-size: 14px;
margin-block-start: 0.83em;
margin-block-end: 0.83em;
}
.kfp-start-page h2 {
font-size: 18px;
margin-block-start: 1em;
margin-block-end: 1em;
}
.kfp-start-page h3 {
font-size: 16px;
margin-block-start: 1em;
margin-block-end: 1em;
}
`);
export class GettingStarted extends Page<{}, {}> {
public getInitialToolbarState(): ToolbarProps {
const buttons = new Buttons(this.props, this.refresh.bind(this));
return {
actions: buttons.getToolbarActionMap(),
breadcrumbs: [],
pageTitle: 'Getting Started',
};
}
public async refresh() {
// do nothing
}
public render(): JSX.Element {
return (
<div className={classes(commonCss.page, padding(20, 'lr'), 'kfp-start-page')}>
<Markdown options={options}>{PAGE_CONTENT_MD}</Markdown>
</div>
);
}
}

View File

@ -557,6 +557,8 @@ spec:
value: {{ .Release.Namespace }}
- name: ALLOW_CUSTOM_VISUALIZATIONS
value: "true"
- name: DEPLOYMENT
value: MARKETPLACE
image: {{ .Values.images.frontend }}
imagePullPolicy: IfNotPresent
name: ml-pipeline-ui