From f0ac272ac7e7b10ba4081d6b59ef2d12d20962e2 Mon Sep 17 00:00:00 2001 From: christianvogt Date: Thu, 13 Jun 2019 15:04:08 -0400 Subject: [PATCH 01/18] add action item feature to dropdown --- frontend/public/components/utils/dropdown.jsx | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/frontend/public/components/utils/dropdown.jsx b/frontend/public/components/utils/dropdown.jsx index 28a647a80c6..45751def64f 100644 --- a/frontend/public/components/utils/dropdown.jsx +++ b/frontend/public/components/utils/dropdown.jsx @@ -64,6 +64,11 @@ export class DropdownMixin extends React.PureComponent { toggle(e) { e.preventDefault(); + + if (this.props.disabled) { + return; + } + if (this.state.active) { this.hide(e); } else { @@ -146,9 +151,10 @@ export class Dropdown extends DropdownMixin { this.state.items = Object.assign({}, bookmarks, props.items); + const defaultTitle = React.isValidElement(props.title) ? props.title : {props.title}; this.state.title = props.noSelection ? props.title - : _.get(props.items, props.selectedKey, {props.title}); + : _.get(props.items, props.selectedKey, defaultTitle); this.onKeyDown = e => this.onKeyDown_(e); this.changeTextFilter = e => this.applyTextFilter_(e.target.value, this.props.items); @@ -293,9 +299,35 @@ export class Dropdown extends DropdownMixin { localStorage.setItem(this.bookmarkStorageKey, JSON.stringify(bookmarks)); } + renderActionItem() { + const { actionItem } = this.props; + if (actionItem) { + const { selectedKey, keyboardHoverKey, noSelection } = this.props; + const { actionTitle, actionKey } = actionItem; + const selected = (actionKey === selectedKey) && !noSelection; + const hover = actionKey === keyboardHoverKey; + return ( + + +
  • +
    +
  • +
    + ); + } + return null; + } + render() { const {active, autocompleteText, selectedKey, items, title, bookmarks, keyboardHoverKey, favoriteKey} = this.state; - const {autocompleteFilter, autocompletePlaceholder, className, buttonClassName, menuClassName, storageKey, canFavorite, dropDownClassName, titlePrefix, describedBy} = this.props; + const {autocompleteFilter, autocompletePlaceholder, className, buttonClassName, menuClassName, storageKey, canFavorite, dropDownClassName, titlePrefix, describedBy, disabled} = this.props; const spacerBefore = this.props.spacerBefore || new Set(); const headerBefore = this.props.headerBefore || {}; @@ -324,7 +356,7 @@ export class Dropdown extends DropdownMixin { return
    -
    ; } @@ -370,10 +371,12 @@ class NamespaceBarDropdowns_ extends React.Component { const NamespaceBarDropdowns = connect(namespaceBarDropdownStateToProps, namespaceBarDropdownDispatchToProps)(NamespaceBarDropdowns_); -const NamespaceBar_ = ({useProjects}) => { +const NamespaceBar_ = ({useProjects, children}) => { return
    - + + {children} +
    ; }; From c5f64a1db364f07b3e563e82bc3bd829326469b9 Mon Sep 17 00:00:00 2001 From: christianvogt Date: Thu, 13 Jun 2019 15:44:09 -0400 Subject: [PATCH 10/18] support namespace redirect for any URL that supports namespaces --- frontend/public/actions/ui.ts | 23 ++++++++++++----------- frontend/public/components/utils/link.tsx | 5 ----- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/frontend/public/actions/ui.ts b/frontend/public/actions/ui.ts index eef7090223f..06b1081825c 100644 --- a/frontend/public/actions/ui.ts +++ b/frontend/public/actions/ui.ts @@ -9,7 +9,6 @@ import { LAST_NAMESPACE_NAME_LOCAL_STORAGE_KEY, LAST_PERSPECTIVE_LOCAL_STORAGE_KEY, } from '../const'; -import { getNSPrefix } from '../components/utils/link'; import { allModels } from '../module/k8s/k8s-models'; import { detectFeatures } from './features'; import { OverviewSpecialGroup } from '../components/overview/constants'; @@ -61,15 +60,12 @@ export const formatNamespacedRouteForResource = (resource, activeNamespace = get }; export const formatNamespaceRoute = (activeNamespace, originalPath, location?) => { - const prefix = getNSPrefix(originalPath); - if (!prefix) { - return originalPath; - } + let path = originalPath.substr(window.SERVER_FLAGS.basePath.length); - originalPath = originalPath.substr(prefix.length + window.SERVER_FLAGS.basePath.length); + let parts = path.split('/').filter(p => p); + const prefix = parts.shift(); - let parts = originalPath.split('/').filter(p => p); - let previousNS = ''; + let previousNS; if (parts[0] === 'all-namespaces') { parts.shift(); previousNS = ALL_NAMESPACES_KEY; @@ -78,6 +74,10 @@ export const formatNamespaceRoute = (activeNamespace, originalPath, location?) = previousNS = parts.shift(); } + if (!previousNS) { + return originalPath; + } + if ((previousNS !== activeNamespace && (parts[1] !== 'new' || activeNamespace !== ALL_NAMESPACES_KEY)) || activeNamespace === ALL_NAMESPACES_KEY && parts[1] === 'new') { // a given resource will not exist when we switch namespaces, so pop off the tail end parts = parts.slice(0, 1); @@ -85,7 +85,7 @@ export const formatNamespaceRoute = (activeNamespace, originalPath, location?) = const namespacePrefix = activeNamespace === ALL_NAMESPACES_KEY ? 'all-namespaces' : `ns/${activeNamespace}`; - let path = `${prefix}/${namespacePrefix}`; + path = `/${prefix}/${namespacePrefix}`; if (parts.length) { path += `/${parts.join('/')}`; } @@ -105,8 +105,9 @@ export const setActiveNamespace = (namespace: string = '') => { // broken direct links and bookmarks if (namespace !== getActiveNamespace()) { const oldPath = window.location.pathname; - if (getNSPrefix(oldPath)) { - history.pushPath(formatNamespaceRoute(namespace, oldPath, window.location)); + const newPath = formatNamespaceRoute(namespace, oldPath, window.location); + if (newPath !== oldPath) { + history.pushPath(newPath); } // remember the most recently-viewed project, which is automatically // selected when returning to the console diff --git a/frontend/public/components/utils/link.tsx b/frontend/public/components/utils/link.tsx index 71c409378b3..7a6bb64161a 100644 --- a/frontend/public/components/utils/link.tsx +++ b/frontend/public/components/utils/link.tsx @@ -23,11 +23,6 @@ export const namespacedPrefixes = ['/search', '/status', '/k8s', '/overview', '/ export const stripBasePath = (path: string): string => path.replace(basePathPattern, '/'); -export const getNSPrefix = (path: string): string => { - path = stripBasePath(path); - return namespacedPrefixes.filter(p => path.startsWith(p))[0]; -}; - export const getNamespace = (path: string): string => { path = stripBasePath(path); const split = path.split('/').filter(x => x); From f2eca9d3c4881082cd6f9d01b4affef70085d522 Mon Sep 17 00:00:00 2001 From: christianvogt Date: Thu, 13 Jun 2019 15:45:47 -0400 Subject: [PATCH 11/18] add active application redux state, action, reducer --- frontend/public/actions/ui.ts | 7 +++++++ frontend/public/const.ts | 4 ++++ frontend/public/reducers/ui.ts | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/frontend/public/actions/ui.ts b/frontend/public/actions/ui.ts index 06b1081825c..a834500d7d3 100644 --- a/frontend/public/actions/ui.ts +++ b/frontend/public/actions/ui.ts @@ -18,6 +18,7 @@ export enum ActionType { DismissOverviewDetails = 'dismissOverviewDetails', SelectOverviewDetailsTab = 'selectOverviewDetailsTab', SelectOverviewItem = 'selectOverviewItem', + SetActiveApplication = 'setActiveApplication', SetActiveNamespace = 'setActiveNamespace', SetActivePerspective = 'setActivePerspective', SetCreateProjectMessage = 'setCreateProjectMessage', @@ -98,6 +99,11 @@ export const formatNamespaceRoute = (activeNamespace, originalPath, location?) = }; export const setCurrentLocation = (location: string) => action(ActionType.SetCurrentLocation, {location}); + +export const setActiveApplication = (application: string) => { + return action(ActionType.SetActiveApplication, {application}); +}; + export const setActiveNamespace = (namespace: string = '') => { namespace = namespace.trim(); // make it noop when new active namespace is the same @@ -199,6 +205,7 @@ export const monitoringToggleGraphs = () => action(ActionType.ToggleMonitoringGr // TODO(alecmerdler): Implement all actions using `typesafe-actions` and add them to this export const uiActions = { setCurrentLocation, + setActiveApplication, setActiveNamespace, setActivePerspective, beginImpersonate, diff --git a/frontend/public/const.ts b/frontend/public/const.ts index fad4864a0f4..a240cefe530 100644 --- a/frontend/public/const.ts +++ b/frontend/public/const.ts @@ -20,11 +20,15 @@ export const KEYBOARD_SHORTCUTS = Object.freeze({ // Use a key for the "all" namespaces option that would be an invalid namespace name to avoid a potential clash export const ALL_NAMESPACES_KEY = '#ALL_NS#'; +// Use a key for the "all" applications option that would be an invalid application name to avoid a potential clash +export const ALL_APPLICATIONS_KEY = '#ALL_APPS#'; + // Prefix our localStorage items to avoid conflicts if another app ever runs on the same domain. export const STORAGE_PREFIX = 'bridge'; // This localStorage key predates the storage prefix. export const NAMESPACE_LOCAL_STORAGE_KEY = 'dropdown-storage-namespaces'; +export const APPLICATION_LOCAL_STORAGE_KEY = 'dropdown-storage-applications'; export const LAST_NAMESPACE_NAME_LOCAL_STORAGE_KEY = `${STORAGE_PREFIX}/last-namespace-name`; export const LAST_PERSPECTIVE_LOCAL_STORAGE_KEY = `${STORAGE_PREFIX}/last-perspective`; export const API_DISCOVERY_RESOURCES_LOCAL_STORAGE_KEY = `${STORAGE_PREFIX}/api-discovery-resources`; diff --git a/frontend/public/reducers/ui.ts b/frontend/public/reducers/ui.ts index 37f6dffdefc..5b3dd95232b 100644 --- a/frontend/public/reducers/ui.ts +++ b/frontend/public/reducers/ui.ts @@ -4,6 +4,7 @@ import { Map as ImmutableMap } from 'immutable'; import { ActionType, UIAction } from '../actions/ui'; import { ALL_NAMESPACES_KEY, + ALL_APPLICATIONS_KEY, LAST_NAMESPACE_NAME_LOCAL_STORAGE_KEY, NAMESPACE_LOCAL_STORAGE_KEY, LAST_PERSPECTIVE_LOCAL_STORAGE_KEY, @@ -50,6 +51,7 @@ export default (state: UIState, action: UIAction): UIState => { activeNavSectionId: 'workloads', location: pathname, activeNamespace: activeNamespace || 'default', + activeApplication: ALL_APPLICATIONS_KEY, activePerspective: getDefaultPerspective(), createProjectMessage: '', overview: ImmutableMap({ @@ -66,13 +68,17 @@ export default (state: UIState, action: UIAction): UIState => { } switch (action.type) { + case ActionType.SetActiveApplication: + return state.set('activeApplication', action.payload.application); + case ActionType.SetActiveNamespace: if (!action.payload.namespace) { // eslint-disable-next-line no-console console.warn('setActiveNamespace: Not setting to falsy!'); return state; } - return state.set('activeNamespace', action.payload.namespace); + + return state.set('activeApplication', ALL_APPLICATIONS_KEY).set('activeNamespace', action.payload.namespace); case ActionType.SetActivePerspective: return state.set('activePerspective', action.payload.perspective); @@ -178,4 +184,8 @@ export const impersonateStateToProps = ({UI}: RootState) => { return {impersonate: UI.get('impersonate')}; }; -export const getActivePerspective = ({ UI }: RootState) => UI.get('activePerspective'); +export const getActiveNamespace = ({ UI }: RootState): string => UI.get('activeNamespace'); + +export const getActivePerspective = ({ UI }: RootState): string => UI.get('activePerspective'); + +export const getActiveApplication = ({ UI }: RootState): string => UI.get('activeApplication'); From 2701ab383c84579b2eff9c3e7496f38498ac9a8b Mon Sep 17 00:00:00 2001 From: christianvogt Date: Thu, 13 Jun 2019 15:46:18 -0400 Subject: [PATCH 12/18] update ownerReference types --- frontend/public/module/k8s/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/public/module/k8s/index.ts b/frontend/public/module/k8s/index.ts index ef63b917031..b8925cdb0db 100644 --- a/frontend/public/module/k8s/index.ts +++ b/frontend/public/module/k8s/index.ts @@ -18,6 +18,8 @@ export type OwnerReference = { kind: string; uid: string; apiVersion: string; + controller?: boolean; + blockOwnerDeletion?: boolean; }; export type ObjectReference = { From b48c7b018d75d94e829e79e99ce9bd5a01540dd4 Mon Sep 17 00:00:00 2001 From: christianvogt Date: Thu, 13 Jun 2019 15:46:47 -0400 Subject: [PATCH 13/18] export routes#getRouteWebURL --- frontend/public/components/routes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/components/routes.tsx b/frontend/public/components/routes.tsx index 63cc9c7df41..8ce679ebced 100644 --- a/frontend/public/components/routes.tsx +++ b/frontend/public/components/routes.tsx @@ -42,7 +42,7 @@ const isWebRoute = (route) => { _.get(route, 'spec.wildcardPolicy') !== 'Subdomain'; }; -const getRouteWebURL = (route) => { +export const getRouteWebURL = (route) => { const scheme = _.get(route, 'spec.tls.termination') ? 'https' : 'http'; let url = `${scheme }://${getRouteHost(route, false)}`; if (route.spec.path) { From 6dcf8a1adf67c9f8001ce3cfc019672624f758e7 Mon Sep 17 00:00:00 2001 From: christianvogt Date: Thu, 13 Jun 2019 15:47:17 -0400 Subject: [PATCH 14/18] export create-secret#SSHAuthSubform --- frontend/public/components/secrets/create-secret.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/components/secrets/create-secret.tsx b/frontend/public/components/secrets/create-secret.tsx index 63b24d4362c..b9f59050211 100644 --- a/frontend/public/components/secrets/create-secret.tsx +++ b/frontend/public/components/secrets/create-secret.tsx @@ -685,7 +685,7 @@ export class BasicAuthSubform extends React.Component import('../utils/file-input').then(c => c.DroppableFileInput)} {...props} />; -class SSHAuthSubform extends React.Component { +export class SSHAuthSubform extends React.Component { constructor(props) { super(props); this.state = { From 0998cb70ad55d7d5b55680f411e1c7b9ecaf09c6 Mon Sep 17 00:00:00 2001 From: christianvogt Date: Fri, 14 Jun 2019 17:07:25 -0400 Subject: [PATCH 15/18] support highlighting active root nav items outside of a section --- frontend/public/components/nav/items.tsx | 74 ++++++++++++++++--- .../public/components/nav/perspective-nav.tsx | 2 +- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/frontend/public/components/nav/items.tsx b/frontend/public/components/nav/items.tsx index a8019ecad88..9a674697c18 100644 --- a/frontend/public/components/nav/items.tsx +++ b/frontend/public/components/nav/items.tsx @@ -2,11 +2,14 @@ import * as React from 'react'; import { Link, LinkProps } from 'react-router-dom'; import * as _ from 'lodash-es'; import { NavItem } from '@patternfly/react-core'; - +import { connect } from 'react-redux'; import { formatNamespacedRouteForResource } from '../../actions/ui'; import { referenceForModel, K8sKind } from '../../module/k8s'; import { stripBasePath } from '../utils'; import * as plugins from '../../plugins'; +import { featureReducerName } from '../../reducers/features'; +import { RootState } from '../../redux'; +import { getActiveNamespace } from '../../reducers/ui'; export const matchesPath = (resourcePath, prefix) => resourcePath === prefix || _.startsWith(resourcePath, `${prefix}/`); export const matchesModel = (resourcePath, model) => model && matchesPath(resourcePath, referenceForModel(model)); @@ -118,14 +121,67 @@ export type HrefLinkProps = NavLinkProps & { href: string; }; -export const createLink = (item: plugins.NavItem): React.ReactElement => { - if (plugins.isHrefNavItem(item)) { - return ; - } - if (plugins.isResourceNSNavItem(item)) { - return ; +type NavLinkComponent = React.ComponentType & { + isActive: (props: T, resourcePath: string, activeNamespace: string) => boolean; +}; + +export const createLink = (item: plugins.NavItem, rootNavLink = false): React.ReactElement => { + if (plugins.isNavItem(item)) { + let Component: NavLinkComponent = null; + if (plugins.isHrefNavItem(item)) { + Component = HrefLink; + } + if (plugins.isResourceNSNavItem(item)) { + Component = ResourceNSLink; + } + if (plugins.isResourceClusterNavItem(item)) { + Component = ResourceClusterLink; + } + if (Component) { + const key = item.properties.componentProps.name; + const props = item.properties.componentProps; + if (rootNavLink) { + return ; + } + return ; + } } - if (plugins.isResourceClusterNavItem(item)) { - return ; + return undefined; +}; + +type RootNavLinkStateProps = { + canRender: boolean; + isActive: boolean; + activeNamespace: string, +}; + +type RootNavLinkProps = NavLinkProps & { + component: NavLinkComponent; +}; + +const RootNavLink_: React.FC = ({ + canRender, + component: Component, + isActive, + ...props +}) => { + if (!canRender) { + return null; } + return ; }; + +const rootNavLinkMapStateToProps = ( + state: RootState, + { required, component: Component, ...props }: RootNavLinkProps, +): RootNavLinkStateProps => ({ + canRender: required ? _.castArray(required).every((r) => state[featureReducerName].get(r)) : true, + activeNamespace: getActiveNamespace(state), + isActive: Component.isActive( + props, + stripNS(state.UI.get('location')), + getActiveNamespace(state), + ), +}); + +export const RootNavLink = connect(rootNavLinkMapStateToProps)(RootNavLink_); diff --git a/frontend/public/components/nav/perspective-nav.tsx b/frontend/public/components/nav/perspective-nav.tsx index ea3e853a4ab..eba03746abc 100644 --- a/frontend/public/components/nav/perspective-nav.tsx +++ b/frontend/public/components/nav/perspective-nav.tsx @@ -36,7 +36,7 @@ const PerspectiveNav: React.FC = ({ perspective }) => { renderedSections.push(section); return ; } - return createLink(item); + return createLink(item, true); }) )} From a1fef270edbb9429abaa42eafabe3f1f85e32c2d Mon Sep 17 00:00:00 2001 From: christianvogt Date: Thu, 13 Jun 2019 15:48:43 -0400 Subject: [PATCH 16/18] add dev-console dependencies --- frontend/package.json | 7 +- frontend/yarn.lock | 287 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 270 insertions(+), 24 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 893f45a4724..858821b2eaa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -70,8 +70,10 @@ "brace": "0.11.x", "classnames": "2.x", "core-js": "2.x", + "d3": "^5.9.2", "file-saver": "1.3.x", "font-awesome": "4.7.x", + "formik": "2.0.1-rc.1", "fuzzysearch": "1.0.x", "history": "4.x", "immutable": "3.x", @@ -95,6 +97,7 @@ "react-jsonschema-form": "^1.0.4", "react-lightweight-tooltip": "1.x", "react-linkify": "^0.2.2", + "react-measure": "^2.2.6", "react-modal": "3.x", "react-redux": "5.x", "react-router-dom": "4.3.x", @@ -111,7 +114,8 @@ "url-polyfill": "^1.1.5", "url-search-params-polyfill": "2.x", "whatwg-fetch": "2.x", - "xterm": "^3.12.2" + "xterm": "^3.12.2", + "yup": "^0.27.0" }, "devDependencies": { "@types/classnames": "^2.2.7", @@ -156,6 +160,7 @@ "protractor-fail-fast": "3.x", "protractor-jasmine2-screenshot-reporter": "0.5.x", "read-pkg": "5.x", + "redux-mock-store": "^1.5.3", "resolve-url-loader": "2.x", "sass-loader": "6.x", "thread-loader": "1.x", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 72e8fef146e..58bef2f51f5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -45,7 +45,7 @@ core-js "^2.5.7" regenerator-runtime "^0.12.0" -"@babel/runtime@^7.2.0": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.2.0": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== @@ -559,10 +559,15 @@ dependencies: immutable "*" -"@types/jasmine@*", "@types/jasmine@2.8.x": +"@types/jasmine@*": version "2.8.6" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.6.tgz#14445b6a1613cf4e05dd61c3c3256d0e95c0421e" +"@types/jasmine@2.8.x": + version "2.8.16" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.16.tgz#a6cb24b1149d65293bd616923500014838e14e7d" + integrity sha512-056oRlBBp7MDzr+HoU5su099s/s7wjZ3KcHxLfv+Byqb9MwdLUvsfLgw1VS97hsh3ddxSPyQu+olHMnoVTUY6g== + "@types/jasminewd2@2.0.x": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.3.tgz#0d2886b0cbdae4c0eeba55e30792f584bf040a95" @@ -1021,7 +1026,7 @@ agent-base@^4.1.0: dependencies: es6-promisify "^5.0.0" -airbnb-prop-types@^2.12.0: +airbnb-prop-types@^2.13.2: version "2.13.2" resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.13.2.tgz#43147a5062dd2a4a5600e748a47b64004cc5f7fc" integrity sha512-2FN6DlHr6JCSxPPi25EnqGaXC4OC3/B3k1lCd6MMYrZ51/Gf/1qDfaR+JElzWa+Tl7cY2aYOlsYJGFeQyVHIeQ== @@ -3873,11 +3878,35 @@ d3-array@1: version "1.2.1" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc" -d3-array@^1.2.0, d3-array@^1.2.1: +d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== +d3-axis@1: + version "1.0.12" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9" + integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ== + +d3-brush@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.6.tgz#33691f2032d9db6c5d8cb684ff255a9883629e21" + integrity sha512-lGSiF5SoSqO5/mYGD5FAeGKKS62JdA1EV7HPrU2b5rTX4qEJJtpjaGLJngjnkewQy7UnGstnFd3168wpf5z76w== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3-chord@1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f" + integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA== + dependencies: + d3-array "1" + d3-path "1" + d3-collection@1: version "1.0.4" resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2" @@ -3890,15 +3919,56 @@ d3-color@1: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b" +d3-contour@1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3" + integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg== + dependencies: + d3-array "^1.1.1" + d3-dispatch@1: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8" -d3-ease@^1.0.0: +d3-drag@1: + version "1.2.3" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.3.tgz#46e206ad863ec465d88c588098a1df444cd33c64" + integrity sha512-8S3HWCAg+ilzjJsNtWW1Mutl74Nmzhb9yU6igspilaJzeZVFktmY6oO9xOh5TDk+BM2KrNFjttZNoJJmDnkjkg== + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-dsv@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.1.1.tgz#aaa830ecb76c4b5015572c647cc6441e3c7bb701" + integrity sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw== + dependencies: + commander "2" + iconv-lite "0.4" + rw "1" + +d3-ease@1, d3-ease@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.5.tgz#8ce59276d81241b1b72042d6af2d40e76d936ffb" integrity sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ== +d3-fetch@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.1.2.tgz#957c8fbc6d4480599ba191b1b2518bf86b3e1be2" + integrity sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA== + dependencies: + d3-dsv "1" + +d3-force@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b" + integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg== + dependencies: + d3-collection "1" + d3-dispatch "1" + d3-quadtree "1" + d3-timer "1" + d3-force@^1.0.6: version "1.1.0" resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3" @@ -3913,9 +3983,17 @@ d3-format@1: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562" integrity sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ== -d3-hierarchy@^1.1.8: +d3-geo@1: + version "1.11.3" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.11.3.tgz#5bb08388f45e4b281491faa72d3abd43215dbd1c" + integrity sha512-n30yN9qSKREvV2fxcrhmHUdXP9TNH7ZZj3C/qnaoU0cVf/Ea85+yT7HY7i8ySPwkwjCNYtmKqQFTvLFngfkItQ== + dependencies: + d3-array "1" + +d3-hierarchy@1, d3-hierarchy@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz#7a6317bd3ed24e324641b6f1e76e978836b008cc" + integrity sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w== d3-interpolate@1: version "1.1.6" @@ -3935,10 +4013,20 @@ d3-path@1: resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.7.tgz#8de7cd693a75ac0b5480d3abaccd94793e58aae8" integrity sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA== +d3-polygon@1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.5.tgz#9a645a0a64ff6cbf9efda96ee0b4a6909184c363" + integrity sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w== + d3-quadtree@1: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438" +d3-random@1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" + integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== + d3-sankey-circular@0.33.0: version "0.33.0" resolved "https://registry.yarnpkg.com/d3-sankey-circular/-/d3-sankey-circular-0.33.0.tgz#756d3ea3f5d74d468226d6886d0ef5c2f746a819" @@ -3948,6 +4036,26 @@ d3-sankey-circular@0.33.0: d3-shape "^1.2.0" elementary-circuits-directed-graph "^1.0.4" +d3-scale-chromatic@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz#dad4366f0edcb288f490128979c3c793583ed3c0" + integrity sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw== + dependencies: + d3-color "1" + d3-interpolate "1" + +d3-scale@2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" + integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + d3-scale@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d" @@ -3961,7 +4069,12 @@ d3-scale@^1.0.0: d3-time "1" d3-time-format "2" -d3-shape@^1.0.0, d3-shape@^1.2.0: +d3-selection@1, d3-selection@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.0.tgz#ab9ac1e664cf967ebf1b479cc07e28ce9908c474" + integrity sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg== + +d3-shape@1, d3-shape@^1.0.0, d3-shape@^1.2.0: version "1.3.5" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.5.tgz#e81aea5940f59f0a79cfccac012232a8987c6033" integrity sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg== @@ -3989,15 +4102,75 @@ d3-timer@^1.0.0: resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.9.tgz#f7bb8c0d597d792ff7131e1c24a36dd471a471ba" integrity sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg== -d3-voronoi@^1.1.2: +d3-transition@1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.2.0.tgz#f538c0e21b2aa1f05f3e965f8567e81284b3b2b8" + integrity sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw== + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-voronoi@1, d3-voronoi@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== +d3-zoom@1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.3.tgz#f444effdc9055c38077c4299b4df999eb1d47ccb" + integrity sha512-xEBSwFx5Z9T3/VrwDkMt+mr0HCzv7XjpGURJ8lWmIC8wxe32L39eWHIasEe/e7Ox8MPU4p1hvH8PKN2olLzIBg== + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + d3@^3.5.12, d3@~3.5.0, d3@~3.5.17: version "3.5.17" resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8" +d3@^5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.2.tgz#64e8a7e9c3d96d9e6e4999d2c8a2c829767e67f5" + integrity sha512-ydrPot6Lm3nTWH+gJ/Cxf3FcwuvesYQ5uk+j/kXEH/xbuYWYWTMAHTJQkyeuG8Y5WM5RSEYB41EctUrXQQytRQ== + dependencies: + d3-array "1" + d3-axis "1" + d3-brush "1" + d3-chord "1" + d3-collection "1" + d3-color "1" + d3-contour "1" + d3-dispatch "1" + d3-drag "1" + d3-dsv "1" + d3-ease "1" + d3-fetch "1" + d3-force "1" + d3-format "1" + d3-geo "1" + d3-hierarchy "1" + d3-interpolate "1" + d3-path "1" + d3-polygon "1" + d3-quadtree "1" + d3-random "1" + d3-scale "2" + d3-scale-chromatic "1" + d3-selection "1" + d3-shape "1" + d3-time "1" + d3-time-format "2" + d3-timer "1" + d3-transition "1" + d3-voronoi "1" + d3-zoom "1" + d@1: version "1.0.0" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" @@ -4138,7 +4311,7 @@ deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" -deepmerge@^2.0.1: +deepmerge@^2.0.1, deepmerge@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== @@ -4584,11 +4757,11 @@ enzyme-adapter-react-16@1.12.1: semver "^5.6.0" enzyme-adapter-utils@^1.11.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.11.0.tgz#6ffff782b1b57dd46c72a845a91fc4103956a117" - integrity sha512-0VZeoE9MNx+QjTfsjmO1Mo+lMfunucYB4wt5ficU85WB/LoetTJrbuujmHP3PJx6pSoaAuLA+Mq877x4LoxdNg== + version "1.12.0" + resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.0.tgz#96e3730d76b872f593e54ce1c51fa3a451422d93" + integrity sha512-wkZvE0VxcFx/8ZsBw0iAbk3gR1d9hK447ebnSYBf95+r32ezBq+XDSAvRErkc4LZosgH8J7et7H7/7CtUuQfBA== dependencies: - airbnb-prop-types "^2.12.0" + airbnb-prop-types "^2.13.2" function.prototype.name "^1.1.0" object.assign "^4.1.0" object.fromentries "^2.0.0" @@ -5558,6 +5731,11 @@ flush-write-stream@^1.0.0: inherits "^2.0.1" readable-stream "^2.0.4" +fn-name@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7" + integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc= + focus-trap-react@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/focus-trap-react/-/focus-trap-react-4.0.1.tgz#3cffd39341df3b2f546a4a2fe94cfdea66154683" @@ -5674,6 +5852,19 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formik@2.0.1-rc.1: + version "2.0.1-rc.1" + resolved "https://registry.yarnpkg.com/formik/-/formik-2.0.1-rc.1.tgz#5025b1731378db1c9f0560f9ba748e744854e086" + integrity sha512-mFIjzOtvyCPE37xWA9XSwMtMxXiieBNnQARtZpdbP2DMIzbSMcwuup9WIzeO16nNROMBqKzb+Swd47t/HOSeyA== + dependencies: + deepmerge "^2.1.1" + hoist-non-react-statics "^3.3.0" + lodash "^4.17.11" + lodash-es "^4.17.11" + react-fast-compare "^2.0.1" + tiny-warning "^1.0.2" + tslib "^1.9.3" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -6975,6 +7166,13 @@ hyphenate-style-name@^1.0.0: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + iconv-lite@0.4.19, iconv-lite@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -6985,13 +7183,6 @@ iconv-lite@0.4.23, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -8471,6 +8662,11 @@ lodash-es@4.x, lodash-es@^4.17.5, lodash-es@^4.2.1: version "4.17.7" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.7.tgz#db240a3252c3dd8360201ac9feef91ac977ea856" +lodash-es@^4.17.11: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0" + integrity sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q== + lodash._baseassign@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" @@ -10880,6 +11076,11 @@ prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, loose-envify "^1.3.1" object-assign "^4.1.1" +property-expr@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.5.1.tgz#22e8706894a0c8e28d58735804f6ba3a3673314f" + integrity sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g== + protocol-buffers-schema@^2.0.2: version "2.2.0" resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-2.2.0.tgz#d29c6cd73fb655978fb6989691180db844119f61" @@ -11270,7 +11471,7 @@ react-ellipsis-with-tooltip@^1.0.8: dependencies: uuid "^3.1.0" -react-fast-compare@^2.0.0: +react-fast-compare@^2.0.0, react-fast-compare@^2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== @@ -11331,7 +11532,7 @@ react-linkify@^0.2.2: prop-types "^15.5.8" tlds "^1.57.0" -react-measure@2.x: +react-measure@2.x, react-measure@^2.2.6: version "2.3.0" resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.3.0.tgz#75835d39abec9ae13517f35a819c160997a7a44e" integrity sha512-dwAvmiOeblj5Dvpnk8Jm7Q8B4THF/f1l1HtKVi0XDecsG6LXwGvzV5R1H32kq3TW6RW64OAf5aoQxpIgLa4z8A== @@ -11710,6 +11911,13 @@ reduce-simplicial-complex@^1.0.0: compare-cell "^1.0.0" compare-oriented-cell "^1.0.1" +redux-mock-store@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d" + integrity sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA== + dependencies: + lodash.isplainobject "^4.0.6" + redux@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" @@ -12369,9 +12577,10 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rw@^1.3.3: +rw@1, rw@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= rx-lite-aggregates@^4.0.8: version "4.0.8" @@ -13385,6 +13594,11 @@ symbol-tree@^3.2.1, symbol-tree@^3.2.2: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" integrity sha1-rifbOPZgp64uHDt9G8KQgZuFGeY= +synchronous-promise@^2.0.6: + version "2.0.9" + resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.9.tgz#b83db98e9e7ae826bf9c8261fd8ac859126c780a" + integrity sha512-LO95GIW16x69LuND1nuuwM4pjgFGupg7pZ/4lU86AmchPKrhk0o2tpMU2unXRrqo81iAFe1YJ0nAGEVwsrZAgg== + tabbable@^3.1.0: version "3.1.2" resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-3.1.2.tgz#f2d16cccd01f400e38635c7181adfe0ad965a4a2" @@ -13593,6 +13807,11 @@ tiny-sdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/tiny-sdf/-/tiny-sdf-1.0.2.tgz#28e76985c44c4e584c4b67d8ecdd9b33a1cac28c" +tiny-warning@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28" + integrity sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q== + tinycolor2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" @@ -13706,6 +13925,11 @@ toposort@^1.0.0: version "1.0.6" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + touch@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" @@ -13845,6 +14069,11 @@ tslib@^1.9.0: version "1.9.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.2.tgz#8be0cc9a1f6dc7727c38deb16c2ebd1a2892988e" +tslib@^1.9.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tsutils@^3.7.0: version "3.10.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.10.0.tgz#6f1c95c94606e098592b0dff06590cf9659227d6" @@ -15280,6 +15509,18 @@ yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" +yup@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.27.0.tgz#f8cb198c8e7dd2124beddc2457571329096b06e7" + integrity sha512-v1yFnE4+u9za42gG/b/081E7uNW9mUj3qtkmelLbW5YPROZzSH/KUUyJu9Wt8vxFJcT9otL/eZopS0YK1L5yPQ== + dependencies: + "@babel/runtime" "^7.0.0" + fn-name "~2.0.1" + lodash "^4.17.11" + property-expr "^1.5.0" + synchronous-promise "^2.0.6" + toposort "^2.0.2" + zero-crossings@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/zero-crossings/-/zero-crossings-1.0.1.tgz#c562bd3113643f3443a245d12406b88b69b9a9ff" From 4a74c4b734033f5f31a584c4bc42722e2677a6d3 Mon Sep 17 00:00:00 2001 From: christianvogt Date: Thu, 13 Jun 2019 15:50:22 -0400 Subject: [PATCH 17/18] contribute dev-console plugin --- frontend/packages/dev-console/package.json | 3 + .../dev-console/src/components/AddPage.tsx | 17 + .../src/components/EmptyState.scss | 18 + .../dev-console/src/components/EmptyState.tsx | 100 + .../src/components/NamespacedPage.scss | 23 + .../src/components/NamespacedPage.tsx | 16 + .../components/__tests__/EmptyState.spec.tsx | 23 + .../components/dropdown/AppNameSelector.tsx | 66 + .../dropdown/ApplicationDropdown.tsx | 54 + .../dropdown/ApplicationSelector.tsx | 72 + .../components/dropdown/ResourceDropdown.tsx | 157 ++ .../dropdown/SourceSecretDropdown.tsx | 42 + .../formik-fields/CheckboxField.tsx | 29 + .../formik-fields/DropdownField.tsx | 32 + .../formik-fields/EnvironmentField.tsx | 26 + .../components/formik-fields/InputField.tsx | 23 + .../formik-fields/NSDropdownField.tsx | 32 + .../formik-fields/NumberSpinnerField.tsx | 35 + .../components/formik-fields/field-types.ts | 45 + .../components/formik-fields/field-utils.ts | 10 + .../src/components/formik-fields/index.ts | 6 + .../src/components/import/GitImport.tsx | 116 + .../src/components/import/GitImportForm.tsx | 53 + .../src/components/import/ImportPage.tsx | 29 + .../__tests__/import-validation-utils.spec.ts | 96 + .../import/advanced/BuildConfigSection.tsx | 44 + .../advanced/DeploymentConfigSection.tsx | 39 + .../import/advanced/LabelSection.tsx | 26 + .../import/advanced/ScalingSection.tsx | 22 + .../src/components/import/app/AppSection.tsx | 33 + .../import/app/ApplicationSelector.tsx | 61 + .../import/builder/BuilderImageCard.scss | 40 + .../import/builder/BuilderImageCard.tsx | 46 + .../import/builder/BuilderImageSelector.scss | 13 + .../import/builder/BuilderImageSelector.tsx | 78 + .../builder/BuilderImageTagSelector.tsx | 67 + .../import/builder/BuilderSection.tsx | 27 + .../import/git/CreateSourceSecret.tsx | 93 + .../src/components/import/git/GitSection.tsx | 68 + .../import/git/SourceSecretSelector.tsx | 84 + .../components/import/import-submit-utils.ts | 239 ++ .../src/components/import/import-types.ts | 72 + .../import/import-validation-utils.ts | 56 + .../components/import/route/RouteSection.tsx | 17 + .../components/import/section/FormSection.tsx | 22 + .../import/section/FormSectionDivider.tsx | 13 + .../import/section/FormSectionHeading.tsx | 18 + .../import/section/FormSectionSubHeading.tsx | 12 + .../pipelineruns/PipelineRunDetails.tsx | 22 + .../pipelineruns/PipelineRunDetailsPage.tsx | 10 + .../pipelineruns/PipelineRunHeader.tsx | 49 + .../pipelineruns/PipelineRunList.tsx | 17 + .../pipelineruns/PipelineRunRow.tsx | 57 + .../pipelineruns/PipelineRunVisualization.tsx | 46 + .../pipelineruns/pipelinerun-table.ts | 11 + .../pipelines/PipelineAugmentRuns.tsx | 29 + .../components/pipelines/PipelineDetails.tsx | 41 + .../pipelines/PipelineDetailsPage.tsx | 61 + .../pipelines/PipelineEnvironment.tsx | 18 + .../components/pipelines/PipelineHeader.tsx | 43 + .../src/components/pipelines/PipelineList.tsx | 31 + .../src/components/pipelines/PipelineRow.tsx | 70 + .../src/components/pipelines/PipelineRuns.tsx | 42 + .../pipelines/PipelineVisualization.tsx | 15 + .../pipelines/PipelineVisualizationGraph.scss | 81 + .../pipelines/PipelineVisualizationGraph.tsx | 49 + .../PipelineVisualizationStepList.scss | 20 + .../PipelineVisualizationStepList.tsx | 20 + .../pipelines/PipelineVisualizationTask.scss | 116 + .../pipelines/PipelineVisualizationTask.tsx | 108 + .../components/pipelines/PipelinesPage.tsx | 31 + .../__mocks__/PipelineVisualizationTask.tsx | 12 + .../pipelines/__tests__/Pipeline.spec.tsx | 57 + .../PipelineVisualizationGraph.spec.tsx | 49 + .../PipelineVisualizationGraph.spec.tsx.snap | 197 ++ .../pipelines/__tests__/pipeline-mock.ts | 94 + .../pipeline-visualization-test-data.ts | 133 + .../pipelines/__tests__/pipelinerun-mock.ts | 170 ++ .../components/pipelines/pipeline-table.ts | 10 + .../source-to-image/ImageStreamInfo.tsx | 45 + .../source-to-image/SourceToImage.tsx | 403 +++ .../source-to-image/SourceToImagePage.tsx | 45 + .../SourceToImageResourceDetails.tsx | 32 + .../src/components/svg/SvgBoxedText.tsx | 81 + .../src/components/svg/SvgDefs.tsx | 55 + .../src/components/svg/SvgDefsContext.ts | 10 + .../src/components/svg/SvgDefsProvider.tsx | 92 + .../components/svg/SvgDropShadowFilter.tsx | 42 + .../src/components/svg/__mocks__/SvgDefs.tsx | 7 + .../svg/__tests__/SvgBoxedText.spec.tsx | 33 + .../components/svg/__tests__/SvgDefs.spec.tsx | 53 + .../svg/__tests__/SvgDefsProvider.spec.tsx | 25 + .../topology/D3ForceDirectedRenderer.tsx | 541 +++++ .../src/components/topology/Graph.scss | 12 + .../src/components/topology/Graph.tsx | 98 + .../src/components/topology/GraphToolbar.scss | 10 + .../src/components/topology/GraphToolbar.tsx | 13 + .../topology/GraphToolbarButton.scss | 21 + .../topology/GraphToolbarButton.tsx | 21 + .../components/topology/SvgPodTooltip.scss | 16 + .../src/components/topology/SvgPodTooltip.tsx | 81 + .../src/components/topology/Topology.tsx | 90 + .../topology/TopologyDataController.tsx | 102 + .../src/components/topology/TopologyPage.tsx | 83 + .../components/topology/TopologySideBar.scss | 25 + .../components/topology/TopologySideBar.tsx | 58 + .../__tests__/TopologyDataController.spec.tsx | 25 + .../__tests__/TopologySideBar.spec.tsx | 30 + .../__snapshots__/topology-utils.spec.ts.snap | 877 +++++++ .../__tests__/topology-knative-test-data.ts | 224 ++ .../topology/__tests__/topology-test-data.ts | 901 +++++++ .../topology/__tests__/topology-utils.spec.ts | 142 ++ .../components/topology/shape-providers.ts | 31 + .../components/topology/shapes/BaseEdge.scss | 4 + .../components/topology/shapes/BaseEdge.tsx | 26 + .../components/topology/shapes/BaseNode.scss | 16 + .../components/topology/shapes/BaseNode.tsx | 116 + .../topology/shapes/ConnectsTo.scss | 3 + .../components/topology/shapes/ConnectsTo.tsx | 22 + .../components/topology/shapes/Decorator.scss | 13 + .../components/topology/shapes/Decorator.tsx | 63 + .../topology/shapes/DefaultEdge.tsx | 11 + .../topology/shapes/DefaultGroup.scss | 16 + .../topology/shapes/DefaultGroup.tsx | 61 + .../topology/shapes/DefaultNode.tsx | 16 + .../topology/shapes/KnativeIcon.tsx | 48 + .../components/topology/shapes/PodStatus.tsx | 98 + .../topology/shapes/SvgArrowMarker.tsx | 27 + .../topology/shapes/WorkloadNode.tsx | 86 + .../shapes/__tests__/BaseNode.spec.tsx | 112 + .../src/components/topology/topology-types.ts | 145 ++ .../src/components/topology/topology-utils.ts | 436 ++++ .../packages/dev-console/src/models/index.ts | 1 + .../dev-console/src/models/pipelines.ts | 49 + frontend/packages/dev-console/src/plugin.tsx | 205 ++ .../dev-console/src/test/browser-mock.ts | 18 + .../dev-console/src/test/test-utils.ts | 11 + .../__tests__/create-route-utils.spec.ts | 26 + .../utils/__tests__/pipeline-actions.spec.ts | 62 + .../__tests__/pipeline-augment-test-data.ts | 96 + .../utils/__tests__/pipeline-augment.spec.ts | 63 + .../__tests__/pipeline-filter-reducer.spec.ts | 68 + .../src/utils/__tests__/pipeline-test-data.ts | 231 ++ .../utils/__tests__/pipeline-utils.spec.ts | 19 + .../src/utils/__tests__/svg-utils.spec.ts | 12 + .../src/utils/create-resource-utils.ts | 226 ++ .../src/utils/create-route-utils.ts | 37 + .../src/utils/imagestream-utils.ts | 127 + .../src/utils/pipeline-actions.tsx | 206 ++ .../dev-console/src/utils/pipeline-augment.ts | 145 ++ .../src/utils/pipeline-filter-reducer.ts | 49 + .../dev-console/src/utils/pipeline-utils.ts | 199 ++ .../src/utils/resource-label-utils.ts | 16 + .../dev-console/src/utils/svg-utils.ts | 92 + .../yamls/applications/analytics/build.yaml | 25 + .../applications/analytics/deployment.yaml | 35 + .../applications/analytics/imagestream.yaml | 13 + .../yamls/applications/analytics/route.yaml | 17 + .../yamls/applications/analytics/service.yaml | 19 + .../yamls/applications/wit/build.yaml | 25 + .../yamls/applications/wit/deployment.yaml | 33 + .../yamls/applications/wit/imagestream.yaml | 13 + .../yamls/applications/wit/route.yaml | 17 + .../yamls/applications/wit/service.yaml | 19 + .../dev-console/yamls/catalog_source3.yaml | 10 + .../dev-console/yamls/catalog_source4.yaml | 10 + .../dev-console/yamls/pipelines/base.yaml | 469 ++++ .../yamls/pipelines/install_pipeline_mocks.sh | 28 + .../dev-console/yamls/pipelines/pipeline.yaml | 145 ++ .../yamls/pipelines/pipelinerun.yaml | 34 + .../yamls/pipelines/resources.yaml | 113 + .../dev-console/yamls/pipelines/task.yaml | 188 ++ .../dev-console/yamls/pipelines/taskrun.yaml | 2163 +++++++++++++++++ .../dev-console/yamls/subscription3.yaml | 10 + .../dev-console/yamls/subscription4.yaml | 10 + 175 files changed, 15019 insertions(+) create mode 100644 frontend/packages/dev-console/src/components/AddPage.tsx create mode 100644 frontend/packages/dev-console/src/components/EmptyState.scss create mode 100644 frontend/packages/dev-console/src/components/EmptyState.tsx create mode 100644 frontend/packages/dev-console/src/components/NamespacedPage.scss create mode 100644 frontend/packages/dev-console/src/components/NamespacedPage.tsx create mode 100644 frontend/packages/dev-console/src/components/__tests__/EmptyState.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/dropdown/AppNameSelector.tsx create mode 100644 frontend/packages/dev-console/src/components/dropdown/ApplicationDropdown.tsx create mode 100644 frontend/packages/dev-console/src/components/dropdown/ApplicationSelector.tsx create mode 100644 frontend/packages/dev-console/src/components/dropdown/ResourceDropdown.tsx create mode 100644 frontend/packages/dev-console/src/components/dropdown/SourceSecretDropdown.tsx create mode 100644 frontend/packages/dev-console/src/components/formik-fields/CheckboxField.tsx create mode 100644 frontend/packages/dev-console/src/components/formik-fields/DropdownField.tsx create mode 100644 frontend/packages/dev-console/src/components/formik-fields/EnvironmentField.tsx create mode 100644 frontend/packages/dev-console/src/components/formik-fields/InputField.tsx create mode 100644 frontend/packages/dev-console/src/components/formik-fields/NSDropdownField.tsx create mode 100644 frontend/packages/dev-console/src/components/formik-fields/NumberSpinnerField.tsx create mode 100644 frontend/packages/dev-console/src/components/formik-fields/field-types.ts create mode 100644 frontend/packages/dev-console/src/components/formik-fields/field-utils.ts create mode 100644 frontend/packages/dev-console/src/components/formik-fields/index.ts create mode 100644 frontend/packages/dev-console/src/components/import/GitImport.tsx create mode 100644 frontend/packages/dev-console/src/components/import/GitImportForm.tsx create mode 100644 frontend/packages/dev-console/src/components/import/ImportPage.tsx create mode 100644 frontend/packages/dev-console/src/components/import/__tests__/import-validation-utils.spec.ts create mode 100644 frontend/packages/dev-console/src/components/import/advanced/BuildConfigSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/advanced/DeploymentConfigSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/advanced/LabelSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/advanced/ScalingSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/app/AppSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/app/ApplicationSelector.tsx create mode 100644 frontend/packages/dev-console/src/components/import/builder/BuilderImageCard.scss create mode 100644 frontend/packages/dev-console/src/components/import/builder/BuilderImageCard.tsx create mode 100644 frontend/packages/dev-console/src/components/import/builder/BuilderImageSelector.scss create mode 100644 frontend/packages/dev-console/src/components/import/builder/BuilderImageSelector.tsx create mode 100644 frontend/packages/dev-console/src/components/import/builder/BuilderImageTagSelector.tsx create mode 100644 frontend/packages/dev-console/src/components/import/builder/BuilderSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/git/CreateSourceSecret.tsx create mode 100644 frontend/packages/dev-console/src/components/import/git/GitSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/git/SourceSecretSelector.tsx create mode 100644 frontend/packages/dev-console/src/components/import/import-submit-utils.ts create mode 100644 frontend/packages/dev-console/src/components/import/import-types.ts create mode 100644 frontend/packages/dev-console/src/components/import/import-validation-utils.ts create mode 100644 frontend/packages/dev-console/src/components/import/route/RouteSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/section/FormSection.tsx create mode 100644 frontend/packages/dev-console/src/components/import/section/FormSectionDivider.tsx create mode 100644 frontend/packages/dev-console/src/components/import/section/FormSectionHeading.tsx create mode 100644 frontend/packages/dev-console/src/components/import/section/FormSectionSubHeading.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelineruns/PipelineRunDetails.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelineruns/PipelineRunDetailsPage.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelineruns/PipelineRunHeader.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelineruns/PipelineRunList.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelineruns/PipelineRunRow.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelineruns/PipelineRunVisualization.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelineruns/pipelinerun-table.ts create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineAugmentRuns.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineDetails.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineDetailsPage.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineEnvironment.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineHeader.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineList.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineRow.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineRuns.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineVisualization.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationGraph.scss create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationGraph.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationStepList.scss create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationStepList.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationTask.scss create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationTask.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/PipelinesPage.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/__mocks__/PipelineVisualizationTask.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/__tests__/Pipeline.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/__tests__/PipelineVisualizationGraph.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/pipelines/__tests__/__snapshots__/PipelineVisualizationGraph.spec.tsx.snap create mode 100644 frontend/packages/dev-console/src/components/pipelines/__tests__/pipeline-mock.ts create mode 100644 frontend/packages/dev-console/src/components/pipelines/__tests__/pipeline-visualization-test-data.ts create mode 100644 frontend/packages/dev-console/src/components/pipelines/__tests__/pipelinerun-mock.ts create mode 100644 frontend/packages/dev-console/src/components/pipelines/pipeline-table.ts create mode 100644 frontend/packages/dev-console/src/components/source-to-image/ImageStreamInfo.tsx create mode 100644 frontend/packages/dev-console/src/components/source-to-image/SourceToImage.tsx create mode 100644 frontend/packages/dev-console/src/components/source-to-image/SourceToImagePage.tsx create mode 100644 frontend/packages/dev-console/src/components/source-to-image/SourceToImageResourceDetails.tsx create mode 100644 frontend/packages/dev-console/src/components/svg/SvgBoxedText.tsx create mode 100644 frontend/packages/dev-console/src/components/svg/SvgDefs.tsx create mode 100644 frontend/packages/dev-console/src/components/svg/SvgDefsContext.ts create mode 100644 frontend/packages/dev-console/src/components/svg/SvgDefsProvider.tsx create mode 100644 frontend/packages/dev-console/src/components/svg/SvgDropShadowFilter.tsx create mode 100644 frontend/packages/dev-console/src/components/svg/__mocks__/SvgDefs.tsx create mode 100644 frontend/packages/dev-console/src/components/svg/__tests__/SvgBoxedText.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/svg/__tests__/SvgDefs.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/svg/__tests__/SvgDefsProvider.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/D3ForceDirectedRenderer.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/Graph.scss create mode 100644 frontend/packages/dev-console/src/components/topology/Graph.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/GraphToolbar.scss create mode 100644 frontend/packages/dev-console/src/components/topology/GraphToolbar.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/GraphToolbarButton.scss create mode 100644 frontend/packages/dev-console/src/components/topology/GraphToolbarButton.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/SvgPodTooltip.scss create mode 100644 frontend/packages/dev-console/src/components/topology/SvgPodTooltip.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/Topology.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/TopologyDataController.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/TopologyPage.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/TopologySideBar.scss create mode 100644 frontend/packages/dev-console/src/components/topology/TopologySideBar.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/__tests__/TopologyDataController.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/__tests__/TopologySideBar.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/__tests__/__snapshots__/topology-utils.spec.ts.snap create mode 100644 frontend/packages/dev-console/src/components/topology/__tests__/topology-knative-test-data.ts create mode 100644 frontend/packages/dev-console/src/components/topology/__tests__/topology-test-data.ts create mode 100644 frontend/packages/dev-console/src/components/topology/__tests__/topology-utils.spec.ts create mode 100644 frontend/packages/dev-console/src/components/topology/shape-providers.ts create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/BaseEdge.scss create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/BaseEdge.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/BaseNode.scss create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/BaseNode.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/ConnectsTo.scss create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/ConnectsTo.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/Decorator.scss create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/Decorator.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/DefaultEdge.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/DefaultGroup.scss create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/DefaultGroup.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/DefaultNode.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/KnativeIcon.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/PodStatus.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/SvgArrowMarker.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/WorkloadNode.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/shapes/__tests__/BaseNode.spec.tsx create mode 100644 frontend/packages/dev-console/src/components/topology/topology-types.ts create mode 100644 frontend/packages/dev-console/src/components/topology/topology-utils.ts create mode 100644 frontend/packages/dev-console/src/models/index.ts create mode 100644 frontend/packages/dev-console/src/models/pipelines.ts create mode 100644 frontend/packages/dev-console/src/plugin.tsx create mode 100644 frontend/packages/dev-console/src/test/browser-mock.ts create mode 100644 frontend/packages/dev-console/src/test/test-utils.ts create mode 100644 frontend/packages/dev-console/src/utils/__tests__/create-route-utils.spec.ts create mode 100644 frontend/packages/dev-console/src/utils/__tests__/pipeline-actions.spec.ts create mode 100644 frontend/packages/dev-console/src/utils/__tests__/pipeline-augment-test-data.ts create mode 100644 frontend/packages/dev-console/src/utils/__tests__/pipeline-augment.spec.ts create mode 100644 frontend/packages/dev-console/src/utils/__tests__/pipeline-filter-reducer.spec.ts create mode 100644 frontend/packages/dev-console/src/utils/__tests__/pipeline-test-data.ts create mode 100644 frontend/packages/dev-console/src/utils/__tests__/pipeline-utils.spec.ts create mode 100644 frontend/packages/dev-console/src/utils/__tests__/svg-utils.spec.ts create mode 100644 frontend/packages/dev-console/src/utils/create-resource-utils.ts create mode 100644 frontend/packages/dev-console/src/utils/create-route-utils.ts create mode 100644 frontend/packages/dev-console/src/utils/imagestream-utils.ts create mode 100644 frontend/packages/dev-console/src/utils/pipeline-actions.tsx create mode 100644 frontend/packages/dev-console/src/utils/pipeline-augment.ts create mode 100644 frontend/packages/dev-console/src/utils/pipeline-filter-reducer.ts create mode 100644 frontend/packages/dev-console/src/utils/pipeline-utils.ts create mode 100644 frontend/packages/dev-console/src/utils/resource-label-utils.ts create mode 100644 frontend/packages/dev-console/src/utils/svg-utils.ts create mode 100644 frontend/packages/dev-console/yamls/applications/analytics/build.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/analytics/deployment.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/analytics/imagestream.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/analytics/route.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/analytics/service.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/wit/build.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/wit/deployment.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/wit/imagestream.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/wit/route.yaml create mode 100644 frontend/packages/dev-console/yamls/applications/wit/service.yaml create mode 100644 frontend/packages/dev-console/yamls/catalog_source3.yaml create mode 100644 frontend/packages/dev-console/yamls/catalog_source4.yaml create mode 100644 frontend/packages/dev-console/yamls/pipelines/base.yaml create mode 100755 frontend/packages/dev-console/yamls/pipelines/install_pipeline_mocks.sh create mode 100644 frontend/packages/dev-console/yamls/pipelines/pipeline.yaml create mode 100644 frontend/packages/dev-console/yamls/pipelines/pipelinerun.yaml create mode 100644 frontend/packages/dev-console/yamls/pipelines/resources.yaml create mode 100644 frontend/packages/dev-console/yamls/pipelines/task.yaml create mode 100644 frontend/packages/dev-console/yamls/pipelines/taskrun.yaml create mode 100644 frontend/packages/dev-console/yamls/subscription3.yaml create mode 100644 frontend/packages/dev-console/yamls/subscription4.yaml diff --git a/frontend/packages/dev-console/package.json b/frontend/packages/dev-console/package.json index ca43fcb5a2c..c2d06821e9b 100644 --- a/frontend/packages/dev-console/package.json +++ b/frontend/packages/dev-console/package.json @@ -5,5 +5,8 @@ "private": true, "dependencies": { "@console/plugin-sdk": "0.0.0-fixed" + }, + "consolePlugin": { + "entry": "src/plugin.tsx" } } diff --git a/frontend/packages/dev-console/src/components/AddPage.tsx b/frontend/packages/dev-console/src/components/AddPage.tsx new file mode 100644 index 00000000000..653f0bc59e8 --- /dev/null +++ b/frontend/packages/dev-console/src/components/AddPage.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { Helmet } from 'react-helmet'; +import ODCEmptyState from './EmptyState'; +import NamespacedPage from './NamespacedPage'; + +const AddPage: React.FC = () => ( + + + +Add + + + + + +); + +export default AddPage; diff --git a/frontend/packages/dev-console/src/components/EmptyState.scss b/frontend/packages/dev-console/src/components/EmptyState.scss new file mode 100644 index 00000000000..35ec4f1dd45 --- /dev/null +++ b/frontend/packages/dev-console/src/components/EmptyState.scss @@ -0,0 +1,18 @@ +.odc-empty-state { + // work around status-box injecting intermediate node preventing pages from controling the height of their content + &__title { + background-color: var(--pf-global--BackgroundColor--light-100); + display: flex; + flex-direction: column; + } + + &__content { + padding: var(--pf-global--spacer--lg); + flex: 1; + } + + &__card { + height: 100%; + text-align: center; + } +} diff --git a/frontend/packages/dev-console/src/components/EmptyState.tsx b/frontend/packages/dev-console/src/components/EmptyState.tsx new file mode 100644 index 00000000000..8a83e547e14 --- /dev/null +++ b/frontend/packages/dev-console/src/components/EmptyState.tsx @@ -0,0 +1,100 @@ +import * as React from 'react'; +import { Card, CardBody, CardHeader, CardFooter, Grid, GridItem } from '@patternfly/react-core'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { formatNamespacedRouteForResource } from '@console/internal/actions/ui'; +import { PageHeading } from '@console/internal/components/utils'; +import './EmptyState.scss'; + +interface StateProps { + activeNamespace: string; +} + +export interface EmptySProps { + title: string; +} + +type Props = EmptySProps & StateProps; + +const ODCEmptyState: React.FunctionComponent = ({ title, activeNamespace }) => ( + +
    + +
    +
    + + + + Import from Git + Import code from your git repository to be built and deployed + + + Import from Git + + + + + + + Browse Catalog + Browse the catalog to discover, deploy and connect to services + + + Browse Catalog + + + + + + + Deploy Image + Deploy an existing image from an image registry or image stream tag + + + Deploy Image + + + + + + + Import YAML + Create or replace resources from their YAML or JSON definitions. + + + Import YAML + + + + + + + Add Database + + Browse the catalog to discover database services to add to your application + + + + Add Database + + + + + +
    +
    +); + +const mapStateToProps = (state): StateProps => { + return { + activeNamespace: state.UI.get('activeNamespace'), + }; +}; + +export default connect(mapStateToProps)(ODCEmptyState); diff --git a/frontend/packages/dev-console/src/components/NamespacedPage.scss b/frontend/packages/dev-console/src/components/NamespacedPage.scss new file mode 100644 index 00000000000..20859b68b76 --- /dev/null +++ b/frontend/packages/dev-console/src/components/NamespacedPage.scss @@ -0,0 +1,23 @@ +.odc-namespaced-page { + height: 100%; + max-height: 100%; + display: flex; + flex-direction: column; + + &__content { + flex-grow: 1; + position: relative; + overflow: auto; + background-color: var(--pf-global--Color--light-200); + } + + // Override styles of namespace bar to support the addition of the application selector on mobile + & .co-namespace { + &-selector { + max-width: 100%; + } + &-bar__items { + flex-wrap: wrap; + } + } +} diff --git a/frontend/packages/dev-console/src/components/NamespacedPage.tsx b/frontend/packages/dev-console/src/components/NamespacedPage.tsx new file mode 100644 index 00000000000..865c557015b --- /dev/null +++ b/frontend/packages/dev-console/src/components/NamespacedPage.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { NamespaceBar } from '@console/internal/components/namespace'; +import ApplicationSelector from './dropdown/ApplicationSelector'; + +import './NamespacedPage.scss'; + +const NamespacedPage: React.FC = ({ children }) => ( +
    + + + +
    {children}
    +
    +); + +export default NamespacedPage; diff --git a/frontend/packages/dev-console/src/components/__tests__/EmptyState.spec.tsx b/frontend/packages/dev-console/src/components/__tests__/EmptyState.spec.tsx new file mode 100644 index 00000000000..79179cc2374 --- /dev/null +++ b/frontend/packages/dev-console/src/components/__tests__/EmptyState.spec.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import configureMockStore from 'redux-mock-store'; +import { Map as ImmutableMap } from 'immutable'; +import { shallow } from 'enzyme'; +import ConnectedEmptyStateComponent from '../EmptyState'; +import { getStoreTypedComponent } from '../../test/test-utils'; + +describe('EmptyState', () => { + const mockStore = configureMockStore(); + const ConnectedComponent = getStoreTypedComponent(ConnectedEmptyStateComponent); + + it('should pass activeNamespace from state as prop', () => { + const store = mockStore({ + UI: ImmutableMap({ + activeNamespace: 'project', + }), + }); + + const topologyWrapper = shallow(); + + expect(topologyWrapper.props().activeNamespace).toEqual('project'); + }); +}); diff --git a/frontend/packages/dev-console/src/components/dropdown/AppNameSelector.tsx b/frontend/packages/dev-console/src/components/dropdown/AppNameSelector.tsx new file mode 100644 index 00000000000..c30f32d0829 --- /dev/null +++ b/frontend/packages/dev-console/src/components/dropdown/AppNameSelector.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { FormGroup, ControlLabel, FormControl, HelpBlock } from 'patternfly-react'; +import ApplicationDropdown from './ApplicationDropdown'; + +const CREATE_APPLICATION_KEY = 'create-application-key'; + +interface AppNameSelectorProps { + namespace?: string; + application: string; + selectedKey: string; + onChange?: (name: string, key: string) => void; +} + +const AppNameSelector: React.FC = ({ + application, + namespace, + selectedKey, + onChange, +}) => { + const onDropdownChange = (appName: string, key: string) => { + if (key === CREATE_APPLICATION_KEY) { + onChange('', key); + } else { + onChange(appName, key); + } + }; + + const onInputChange: React.ReactEventHandler = (event) => { + onChange(event.currentTarget.value, selectedKey); + }; + + return ( + + + Application + + + {selectedKey === CREATE_APPLICATION_KEY ? ( + + Application Name + + Names the application. + + ) : null} + + ); +}; + +export default AppNameSelector; diff --git a/frontend/packages/dev-console/src/components/dropdown/ApplicationDropdown.tsx b/frontend/packages/dev-console/src/components/dropdown/ApplicationDropdown.tsx new file mode 100644 index 00000000000..ddf3a585c3c --- /dev/null +++ b/frontend/packages/dev-console/src/components/dropdown/ApplicationDropdown.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { Firehose } from '@console/internal/components/utils'; +import ResourceDropdown from './ResourceDropdown'; + +interface ApplicationDropdownProps { + className?: string; + dropDownClassName?: string; + menuClassName?: string; + buttonClassName?: string; + title?: React.ReactNode; + titlePrefix?: string; + allApplicationsKey?: string; + storageKey?: string; + disabled?: boolean; + allSelectorItem?: { + allSelectorKey?: string; + allSelectorTitle?: string; + }; + namespace?: string; + actionItem?: { + actionTitle: string; + actionKey: string; + }; + selectedKey: string; + onChange?: (name: string, key: string) => void; +} + +const ApplicationDropdown: React.FC = ({ namespace, ...props }) => { + const resources = [ + { + isList: true, + namespace, + kind: 'DeploymentConfig', + prop: 'deploymentConfigs', + }, + { + isList: true, + namespace, + kind: 'Deployment', + prop: 'deployments', + }, + ]; + return ( + + + + ); +}; + +export default ApplicationDropdown; diff --git a/frontend/packages/dev-console/src/components/dropdown/ApplicationSelector.tsx b/frontend/packages/dev-console/src/components/dropdown/ApplicationSelector.tsx new file mode 100644 index 00000000000..f688715b03f --- /dev/null +++ b/frontend/packages/dev-console/src/components/dropdown/ApplicationSelector.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { connect, Dispatch } from 'react-redux'; +import { + ALL_NAMESPACES_KEY, + ALL_APPLICATIONS_KEY, + APPLICATION_LOCAL_STORAGE_KEY, +} from '@console/internal/const'; +import { setActiveApplication } from '@console/internal/actions/ui'; +import { RootState } from '@console/internal/redux'; +import { getActiveNamespace, getActiveApplication } from '@console/internal/reducers/ui'; +import ApplicationDropdown from './ApplicationDropdown'; + +interface ApplicationSelectorProps { + namespace: string; + application: string; + onChange: (name: string) => void; +} + +const ApplicationSelector: React.FC = ({ + namespace, + application, + onChange, +}) => { + const onApplicationChange = (newApplication: string, key: string) => { + key === ALL_APPLICATIONS_KEY ? onChange(key) : onChange(newApplication); + }; + const allApplicationsTitle = 'all applications'; + + const disabled = namespace === ALL_NAMESPACES_KEY; + + let title: string = application; + if (disabled) { + title = 'No applications'; + } else if (title === ALL_APPLICATIONS_KEY) { + title = allApplicationsTitle; + } + + return ( + {title}} + titlePrefix="Application" + allSelectorItem={{ + allSelectorKey: ALL_APPLICATIONS_KEY, + allSelectorTitle: allApplicationsTitle, + }} + selectedKey={application || ALL_APPLICATIONS_KEY} + onChange={onApplicationChange} + storageKey={APPLICATION_LOCAL_STORAGE_KEY} + disabled={disabled} + /> + ); +}; + +const mapStateToProps = (state: RootState) => ({ + namespace: getActiveNamespace(state), + application: getActiveApplication(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + onChange: (app: string) => { + dispatch(setActiveApplication(app)); + }, +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(ApplicationSelector); diff --git a/frontend/packages/dev-console/src/components/dropdown/ResourceDropdown.tsx b/frontend/packages/dev-console/src/components/dropdown/ResourceDropdown.tsx new file mode 100644 index 00000000000..f48f4ff01e5 --- /dev/null +++ b/frontend/packages/dev-console/src/components/dropdown/ResourceDropdown.tsx @@ -0,0 +1,157 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as fuzzy from 'fuzzysearch'; +import { Dropdown, LoadingInline } from '@console/internal/components/utils'; +import { K8sResourceKind } from '@console/internal/module/k8s'; + +type FirehoseList = { + data?: K8sResourceKind[]; + [key: string]: any; +}; + +interface State { + items: {}; + title: React.ReactNode; +} + +interface ResourceDropdownProps { + className?: string; + dropDownClassName?: string; + menuClassName?: string; + buttonClassName?: string; + title?: React.ReactNode; + titlePrefix?: string; + allApplicationsKey?: string; + storageKey?: string; + disabled?: boolean; + allSelectorItem?: { + allSelectorKey?: string; + allSelectorTitle?: string; + }; + actionItem?: { + actionTitle: string; + actionKey: string; + }; + dataSelector: string[] | number[] | symbol[]; + loaded?: boolean; + loadError?: string; + placeholder?: string; + resources?: FirehoseList[]; + selectedKey: string; + resourceFilter?: (resource: any) => boolean; + onChange?: (name: string, key: string) => void; +} + +class ResourceDropdown extends React.Component { + constructor(props) { + super(props); + this.state = { + items: {}, + title: this.props.loaded ? ( + {this.props.placeholder} + ) : ( + + ), + }; + } + + componentWillReceiveProps(nextProps: ResourceDropdownProps) { + const { + resources, + loaded, + loadError, + placeholder, + allSelectorItem, + resourceFilter, + dataSelector, + } = nextProps; + + if (!loaded) { + this.setState({ title: }); + return; + } + if (!this.props.loaded) { + this.setState({ + title: {placeholder}, + }); + } + + if (loadError) { + this.setState({ + title: Error Loading - {placeholder}, + }); + } + + const unsortedList = {}; + _.each(resources, ({ data }) => { + _.reduce( + data, + (acc, resource) => { + let dataValue; + if (resourceFilter && resourceFilter(resource)) { + dataValue = _.get(resource, dataSelector); + } else if (!resourceFilter) { + dataValue = _.get(resource, dataSelector); + } + if (dataValue) { + acc[dataValue] = dataValue; + } + return acc; + }, + unsortedList, + ); + }); + + const sortedList = {}; + + if (allSelectorItem && !_.isEmpty(unsortedList)) { + sortedList[allSelectorItem.allSelectorKey] = allSelectorItem.allSelectorTitle; + } + + _.keys(unsortedList) + .sort() + .forEach((key) => { + sortedList[key] = unsortedList[key]; + }); + + this.setState({ items: sortedList }); + } + + shouldComponentUpdate(nextProps, nextState) { + if (_.isEqual(this.state, nextState)) { + return false; + } + return true; + } + + private onChange = (key: string) => { + const name = this.state.items[key]; + const { actionItem, onChange } = this.props; + const title = actionItem && key === actionItem.actionKey ? actionItem.actionTitle : name; + onChange && this.props.onChange(name, key); + this.setState({ title }); + }; + + render() { + return ( + + ); + } +} + +export default ResourceDropdown; diff --git a/frontend/packages/dev-console/src/components/dropdown/SourceSecretDropdown.tsx b/frontend/packages/dev-console/src/components/dropdown/SourceSecretDropdown.tsx new file mode 100644 index 00000000000..ca433835169 --- /dev/null +++ b/frontend/packages/dev-console/src/components/dropdown/SourceSecretDropdown.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { SecretModel } from '@console/internal/models'; +import { Firehose } from '@console/internal/components/utils'; +import ResourceDropdown from './ResourceDropdown'; + +interface SourceSecretDropdownProps { + dropDownClassName?: string; + menuClassName?: string; + namespace?: string; + actionItem?: { + actionTitle: string; + actionKey: string; + }; + selectedKey: string; + onChange?: (name: string, key: string) => void; +} + +const SourceSecretDropdown: React.FC = (props) => { + const filterData = (item) => { + return item.type === 'kubernetes.io/basic-auth' || item.type === 'kubernetes.io/ssh-auth'; + }; + const resources = [ + { + isList: true, + namespace: props.namespace, + kind: SecretModel.kind, + prop: 'secrets', + }, + ]; + return ( + + + + ); +}; + +export default SourceSecretDropdown; diff --git a/frontend/packages/dev-console/src/components/formik-fields/CheckboxField.tsx b/frontend/packages/dev-console/src/components/formik-fields/CheckboxField.tsx new file mode 100644 index 00000000000..cdbc070f4db --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/CheckboxField.tsx @@ -0,0 +1,29 @@ +/* eslint-disable no-unused-vars, no-undef */ +import * as React from 'react'; +import { useField } from 'formik'; +import { FormGroup, HelpBlock, Checkbox } from 'patternfly-react'; +import { InputFieldProps } from './field-types'; +import { getValidationState } from './field-utils'; + +const CheckboxField: React.FC = ({ label, helpText, ...props }) => { + const [field, { touched, error }] = useField(props.name); + return ( + + + {label} + + {helpText && {helpText}} + {touched && error && {error}} + + ); +}; + +export default CheckboxField; diff --git a/frontend/packages/dev-console/src/components/formik-fields/DropdownField.tsx b/frontend/packages/dev-console/src/components/formik-fields/DropdownField.tsx new file mode 100644 index 00000000000..69b2521be56 --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/DropdownField.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import cx from 'classnames'; +import { useField, useFormikContext, FormikValues } from 'formik'; +import { FormGroup, ControlLabel, HelpBlock } from 'patternfly-react'; +import { Dropdown } from '@console/internal/components/utils'; +import { DropdownFieldProps } from './field-types'; +import { getValidationState } from './field-utils'; + +const DropdownField: React.FC = ({ label, helpText, ...props }) => { + const [field, { touched, error }] = useField(props.name); + const { setFieldValue, setFieldTouched } = useFormikContext(); + return ( + + {label} + setFieldValue(props.name, value)} + onBlur={() => setFieldTouched(props.name, true)} + /> + {helpText && {helpText}} + {touched && error && {error}} + + ); +}; + +export default DropdownField; diff --git a/frontend/packages/dev-console/src/components/formik-fields/EnvironmentField.tsx b/frontend/packages/dev-console/src/components/formik-fields/EnvironmentField.tsx new file mode 100644 index 00000000000..df8671ed29d --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/EnvironmentField.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import cx from 'classnames'; +import { useFormikContext, FormikValues } from 'formik'; +import { FormGroup, ControlLabel, HelpBlock } from 'patternfly-react'; +import { EnvironmentPage } from '@console/internal/components/environment'; +import { EnvironmentFieldProps, NameValuePair, NameValueFromPair } from './field-types'; + +const EnvironmentField: React.FC = ({ label, helpText, ...props }) => { + const { setFieldValue } = useFormikContext(); + return ( + + {label} + setFieldValue(props.name, obj)} + addConfigMapSecret + useLoadingInline + /> + {helpText && {helpText}} + + ); +}; + +export default EnvironmentField; diff --git a/frontend/packages/dev-console/src/components/formik-fields/InputField.tsx b/frontend/packages/dev-console/src/components/formik-fields/InputField.tsx new file mode 100644 index 00000000000..e27fdf8f06d --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/InputField.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import cx from 'classnames'; +import { useField } from 'formik'; +import { FormGroup, ControlLabel, FormControl, HelpBlock } from 'patternfly-react'; +import { InputFieldProps } from './field-types'; +import { getValidationState } from './field-utils'; + +const InputField: React.FC = ({ label, helpText, ...props }) => { + const [field, { touched, error }] = useField(props.name); + return ( + + {label} + + {helpText && {helpText}} + {touched && error && {error}} + + ); +}; + +export default InputField; diff --git a/frontend/packages/dev-console/src/components/formik-fields/NSDropdownField.tsx b/frontend/packages/dev-console/src/components/formik-fields/NSDropdownField.tsx new file mode 100644 index 00000000000..48615b6e237 --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/NSDropdownField.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import cx from 'classnames'; +import { useField, useFormikContext, FormikValues } from 'formik'; +import { FormGroup, ControlLabel, HelpBlock } from 'patternfly-react'; +import { NsDropdown } from '@console/internal/components/utils'; +import { DropdownFieldProps } from './field-types'; +import { getValidationState } from './field-utils'; + +const NSDropdownField: React.FC = ({ label, helpText, ...props }) => { + const [field, { touched, error }] = useField(props.name); + const { setFieldValue, setFieldTouched } = useFormikContext(); + return ( + + {label} + setFieldValue(props.name, value)} + onBlur={() => setFieldTouched(props.name, true)} + /> + {helpText && {helpText}} + {touched && error && {error}} + + ); +}; + +export default NSDropdownField; diff --git a/frontend/packages/dev-console/src/components/formik-fields/NumberSpinnerField.tsx b/frontend/packages/dev-console/src/components/formik-fields/NumberSpinnerField.tsx new file mode 100644 index 00000000000..d694d0bebbe --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/NumberSpinnerField.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import cx from 'classnames'; +import { useField, useFormikContext, FormikValues } from 'formik'; +import { FormGroup, ControlLabel, HelpBlock } from 'patternfly-react'; +import { NumberSpinner } from '@console/internal/components/utils'; +import { InputFieldProps } from './field-types'; +import { getValidationState } from './field-utils'; + +const NumberSpinnerField: React.FC = ({ label, helpText, ...props }) => { + const [field, { touched, error }] = useField(props.name); + const { setFieldValue, setFieldTouched } = useFormikContext(); + return ( + + {label} + { + setFieldValue(props.name, _.toInteger(field.value) + operation); + setFieldTouched(props.name, true); + }} + aria-describedby={helpText && `${props.name}-help`} + /> + {helpText && {helpText}} + {touched && error && {error}} + + ); +}; + +export default NumberSpinnerField; diff --git a/frontend/packages/dev-console/src/components/formik-fields/field-types.ts b/frontend/packages/dev-console/src/components/formik-fields/field-types.ts new file mode 100644 index 00000000000..cc8f04551f4 --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/field-types.ts @@ -0,0 +1,45 @@ +export interface InputFieldProps { + type?: string; + name: string; + label: string; + helpText?: string; + required?: boolean; + onChange?: (event) => void; + onBlur?: (event) => void; +} + +export interface DropdownFieldProps extends InputFieldProps { + items?: object; + selectedKey: string; + title?: React.ReactNode; + fullWidth?: boolean; +} + +export interface EnvironmentFieldProps extends InputFieldProps { + obj?: object; + envPath: string[]; +} + +export interface NameValuePair { + name: string; + value: string; +} + +export interface NameValueFromPair { + name: string; + valueForm: ConfigMapKeyRef | SecretKeyRef; +} + +export interface ConfigMapKeyRef { + configMapKeyRef: { + key: string; + name: string; + }; +} + +export interface SecretKeyRef { + secretKeyRef: { + key: string; + name: string; + }; +} diff --git a/frontend/packages/dev-console/src/components/formik-fields/field-utils.ts b/frontend/packages/dev-console/src/components/formik-fields/field-utils.ts new file mode 100644 index 00000000000..71dea3550c1 --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/field-utils.ts @@ -0,0 +1,10 @@ +import cx from 'classnames'; + +export const getValidationState = (error: string, touched: boolean, warning?: string) => { + const state = cx({ + success: touched && !error, + error: touched && error, + warning: touched && warning, + }); + return state || null; +}; diff --git a/frontend/packages/dev-console/src/components/formik-fields/index.ts b/frontend/packages/dev-console/src/components/formik-fields/index.ts new file mode 100644 index 00000000000..f1811da3733 --- /dev/null +++ b/frontend/packages/dev-console/src/components/formik-fields/index.ts @@ -0,0 +1,6 @@ +export { default as InputField } from './InputField'; +export { default as DropdownField } from './DropdownField'; +export { default as NSDropdownField } from './NSDropdownField'; +export { default as CheckboxField } from './CheckboxField'; +export { default as NumberSpinnerField } from './NumberSpinnerField'; +export { default as EnvironmentField } from './EnvironmentField'; diff --git a/frontend/packages/dev-console/src/components/import/GitImport.tsx b/frontend/packages/dev-console/src/components/import/GitImport.tsx new file mode 100644 index 00000000000..f429029cac9 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/GitImport.tsx @@ -0,0 +1,116 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { Formik } from 'formik'; +import { history } from '@console/internal/components/utils'; +import { GitImportFormData, FirehoseList } from './import-types'; +import { NormalizedBuilderImages, normalizeBuilderImages } from '../../utils/imagestream-utils'; +import { + createDeploymentConfig, + createImageStream, + createBuildConfig, + createService, + createRoute, +} from './import-submit-utils'; +import { validationSchema } from './import-validation-utils'; +import GitImportForm from './GitImportForm'; + +export interface GitImportProps { + namespace: string; + imageStreams?: FirehoseList; +} + +const GitImport: React.FC = ({ namespace, imageStreams }) => { + const initialValues: GitImportFormData = { + name: '', + project: { + name: namespace || '', + }, + application: { + name: '', + selectedKey: '', + }, + git: { + url: '', + type: '', + ref: '', + dir: '/', + showGitType: false, + }, + image: { + selected: '', + recommended: '', + tag: '', + ports: [], + }, + route: { + create: true, + }, + build: { + env: [], + triggers: { + webhook: true, + image: true, + config: true, + }, + }, + deployment: { + env: [], + triggers: { + image: true, + config: true, + }, + replicas: 1, + }, + labels: {}, + }; + + const builderImages: NormalizedBuilderImages = + imageStreams && imageStreams.loaded && normalizeBuilderImages(imageStreams.data); + + const handleSubmit = (values, actions) => { + const imageStream = builderImages[values.image.selected].obj; + + const { + project: { name: projectName }, + route: { create: canCreateRoute }, + image: { ports }, + } = values; + + const requests = [ + createDeploymentConfig(values, imageStream), + createImageStream(values, imageStream), + createBuildConfig(values, imageStream), + ]; + + // Only create a service or route if the builder image has ports. + if (!_.isEmpty(ports)) { + requests.push(createService(values, imageStream)); + if (canCreateRoute) { + requests.push(createRoute(values, imageStream)); + } + } + + requests.forEach((r) => r.catch((err) => actions.setStatus({ submitError: err.message }))); + Promise.all(requests) + .then(() => { + actions.setSubmitting(false); + history.push(`/topology/ns/${projectName}`); + }) + .catch((err) => { + actions.setSubmitting(false); + actions.setStatus({ submitError: err.message }); + }); + }; + + return ( + } + /> + ); +}; + +export default GitImport; diff --git a/frontend/packages/dev-console/src/components/import/GitImportForm.tsx b/frontend/packages/dev-console/src/components/import/GitImportForm.tsx new file mode 100644 index 00000000000..29fc3603638 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/GitImportForm.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { Form, Button, ExpandCollapse } from 'patternfly-react'; +import { FormikProps, FormikValues } from 'formik'; +import { ButtonBar } from '@console/internal/components/utils'; +import { NormalizedBuilderImages } from '../../utils/imagestream-utils'; +import GitSection from './git/GitSection'; +import BuilderSection from './builder/BuilderSection'; +import AppSection from './app/AppSection'; +import RouteSection from './route/RouteSection'; +import ScalingSection from './advanced/ScalingSection'; +import BuildConfigSection from './advanced/BuildConfigSection'; +import DeploymentConfigSection from './advanced/DeploymentConfigSection'; +import LabelSection from './advanced/LabelSection'; + +export interface GitImportFormProps { + builderImages?: NormalizedBuilderImages; +} + +const GitImportForm: React.FC & GitImportFormProps> = ({ + values, + errors, + handleSubmit, + handleReset, + builderImages, + status, + isSubmitting, + dirty, +}) => ( +
    +
    + + + + + + + + + + +
    +
    + + + + +
    +); + +export default GitImportForm; diff --git a/frontend/packages/dev-console/src/components/import/ImportPage.tsx b/frontend/packages/dev-console/src/components/import/ImportPage.tsx new file mode 100644 index 00000000000..741086a77b9 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/ImportPage.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { match as RMatch } from 'react-router-dom'; +import { Helmet } from 'react-helmet'; +import { PageHeading, Firehose } from '@console/internal/components/utils'; +import { ImageStreamModel } from '@console/internal/models'; +import GitImport from './GitImport'; + +export interface ImportPageProps { + match: RMatch<{ ns?: string }>; +} + +const ImportPage: React.FunctionComponent = ({ match }) => { + const namespace = match.params.ns; + return ( + + + Import from Git + + +
    + + + +
    +
    + ); +}; + +export default ImportPage; diff --git a/frontend/packages/dev-console/src/components/import/__tests__/import-validation-utils.spec.ts b/frontend/packages/dev-console/src/components/import/__tests__/import-validation-utils.spec.ts new file mode 100644 index 00000000000..8cc66613362 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/__tests__/import-validation-utils.spec.ts @@ -0,0 +1,96 @@ +import { validationSchema, detectGitType } from '../import-validation-utils'; +import { GitImportFormData } from '../import-types'; + +describe('ValidationUtils', () => { + describe('Detect Git Type', () => { + it('should return undefined for invalid git url', () => { + const gitType = detectGitType('test'); + expect(gitType).toEqual(undefined); + }); + it('should return empty string for valid but unknown git url ', () => { + const gitType = detectGitType('https://svnsource.test.com'); + expect(gitType).toEqual(''); + }); + it('should return proper git type for valid known git url', () => { + const gitType = detectGitType('https://github.com/test/repo'); + expect(gitType).toEqual('github'); + }); + }); + + describe('Validation Schema', () => { + const mockFormData: GitImportFormData = { + name: 'test-app', + project: { + name: 'mock-project', + }, + application: { + name: 'mock-app', + selectedKey: 'mock-app', + }, + git: { + url: 'https://github.com/test/repo', + type: 'github', + ref: '', + dir: '', + showGitType: false, + }, + image: { + selected: 'nodejs', + recommended: '', + tag: 'latest', + ports: [], + }, + route: { + create: false, + }, + build: { + env: [], + triggers: { + webhook: true, + image: true, + config: true, + }, + }, + deployment: { + env: [], + triggers: { + image: true, + config: true, + }, + replicas: 1, + }, + labels: {}, + }; + + it('should validate the form data', async () => { + await validationSchema.isValid(mockFormData).then((valid) => expect(valid).toEqual(true)); + }); + + it('should throw an error if url is invalid', async () => { + mockFormData.git.url = 'something.com'; + await validationSchema.isValid(mockFormData).then((valid) => expect(valid).toEqual(false)); + await validationSchema + .validate(mockFormData) + .catch((err) => expect(err.message).toBe('Invalid Git URL')); + }); + + it('should throw an error if url is valid but git type is not valid', async () => { + mockFormData.git.url = 'https://something.com/test/repo'; + mockFormData.git.type = ''; + await validationSchema.isValid(mockFormData).then((valid) => expect(valid).toEqual(true)); + mockFormData.git.showGitType = true; + await validationSchema.validate(mockFormData).catch((err) => { + expect(err.message).toBe('We failed to detect the git type. Please choose a git type.'); + }); + }); + + it('should throw an error for required fields if empty', async () => { + mockFormData.name = ''; + await validationSchema.isValid(mockFormData).then((valid) => expect(valid).toEqual(false)); + await validationSchema.validate(mockFormData).catch((err) => { + expect(err.message).toBe('Required'); + expect(err.type).toBe('required'); + }); + }); + }); +}); diff --git a/frontend/packages/dev-console/src/components/import/advanced/BuildConfigSection.tsx b/frontend/packages/dev-console/src/components/import/advanced/BuildConfigSection.tsx new file mode 100644 index 00000000000..8607f49e27e --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/advanced/BuildConfigSection.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import { CheckboxField, EnvironmentField } from '../../formik-fields'; +import FormSection from '../section/FormSection'; + +export interface BuildConfigSectionProps { + namespace: string; +} + +const BuildConfigSection: React.FC = ({ namespace }) => { + const buildConfigObj = { + kind: 'BuildConfig', + metadata: { + namespace, + }, + }; + + return ( + + + + + + + ); +}; + +export default BuildConfigSection; diff --git a/frontend/packages/dev-console/src/components/import/advanced/DeploymentConfigSection.tsx b/frontend/packages/dev-console/src/components/import/advanced/DeploymentConfigSection.tsx new file mode 100644 index 00000000000..c79abd1bb1f --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/advanced/DeploymentConfigSection.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { CheckboxField, EnvironmentField } from '../../formik-fields'; +import FormSection from '../section/FormSection'; + +export interface DeploymentConfigSectionProps { + namespace: string; +} + +const DeploymentConfigSection: React.FC = ({ namespace }) => { + const deploymentConfigObj = { + kind: 'DeploymentConfig', + metadata: { + namespace, + }, + }; + + return ( + + + + + + ); +}; + +export default DeploymentConfigSection; diff --git a/frontend/packages/dev-console/src/components/import/advanced/LabelSection.tsx b/frontend/packages/dev-console/src/components/import/advanced/LabelSection.tsx new file mode 100644 index 00000000000..6a33ba3eb7d --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/advanced/LabelSection.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { NameValueEditor } from '@console/internal/components/utils/name-value-editor'; +import { useFormikContext, FormikValues } from 'formik'; +import FormSection from '../section/FormSection'; + +const LabelSection: React.FC = () => { + const { values, setFieldValue } = useFormikContext(); + const labels = _.isEmpty(values.labels) ? [['', '']] : _.toPairs(values.labels); + return ( + + setFieldValue('labels', _.fromPairs(obj.nameValuePairs))} + useLoadingInline + readOnly={false} + /> + + ); +}; + +export default LabelSection; diff --git a/frontend/packages/dev-console/src/components/import/advanced/ScalingSection.tsx b/frontend/packages/dev-console/src/components/import/advanced/ScalingSection.tsx new file mode 100644 index 00000000000..98b76a963d2 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/advanced/ScalingSection.tsx @@ -0,0 +1,22 @@ +/* eslint-disable no-unused-vars, no-undef */ +import * as React from 'react'; +import { NumberSpinnerField } from '../../formik-fields'; +import FormSection from '../section/FormSection'; + +const ScalingSection: React.FC = () => { + return ( + + + + ); +}; + +export default ScalingSection; diff --git a/frontend/packages/dev-console/src/components/import/app/AppSection.tsx b/frontend/packages/dev-console/src/components/import/app/AppSection.tsx new file mode 100644 index 00000000000..3c4fbdca1fc --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/app/AppSection.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { InputField, NSDropdownField } from '../../formik-fields'; +import { ProjectData } from '../import-types'; +import FormSection from '../section/FormSection'; +import ApplicationSelector from './ApplicationSelector'; + +export interface AppSectionProps { + project: ProjectData; +} + +const AppSection: React.FC = ({ project }) => { + return ( + + + + + + ); +}; + +export default AppSection; diff --git a/frontend/packages/dev-console/src/components/import/app/ApplicationSelector.tsx b/frontend/packages/dev-console/src/components/import/app/ApplicationSelector.tsx new file mode 100644 index 00000000000..7275faa0e98 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/app/ApplicationSelector.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { FormGroup, ControlLabel, HelpBlock } from 'patternfly-react'; +import { useFormikContext, FormikValues, useField } from 'formik'; +import { InputField } from '../../formik-fields'; +import { getValidationState } from '../../formik-fields/field-utils'; +import ApplicationDropdown from '../../dropdown/ApplicationDropdown'; + +export const CREATE_APPLICATION_KEY = 'create-application-key'; + +export interface ApplicationSelectorProps { + namespace?: string; +} + +const ApplicationSelector: React.FC = ({ namespace }) => { + const [selectedKey, { touched, error }] = useField('application.selectedKey'); + const { setFieldValue, setFieldTouched } = useFormikContext(); + const onDropdownChange = (application: string, key: string) => { + setFieldTouched('application.selectedKey', true); + if (key === CREATE_APPLICATION_KEY) { + setFieldValue('application.name', ''); + setFieldValue('application.selectedKey', key); + } else { + setFieldValue('application.name', application); + setFieldValue('application.selectedKey', key); + } + }; + + return ( + + + Application + + {touched && error && {error}} + + {selectedKey.value === CREATE_APPLICATION_KEY && ( + + )} + + ); +}; + +export default ApplicationSelector; diff --git a/frontend/packages/dev-console/src/components/import/builder/BuilderImageCard.scss b/frontend/packages/dev-console/src/components/import/builder/BuilderImageCard.scss new file mode 100644 index 00000000000..80b25e64382 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/builder/BuilderImageCard.scss @@ -0,0 +1,40 @@ +.odc-builder-image-card { + position: relative; + height: 112px; + width: 136px; + margin: 4px; + align-items: center; + border: 0; + + &__icon { + height: 32px; + max-width: 80px; + min-width: 40px; + } + + &__title { + font-size: var(--pf-global--FontSize--sm); + font-weight: var(--pf-global--FontWeight--bold); + line-height: var(--pf-global--LineHeight--sm); + } + + &__recommended { + position: absolute; + color: var(--pf-global--success-color--100); + font-size: var(--pf-global--FontSize--md); + top: 0px; + right: 3px; + } + + &.is-selected, + &:hover, + &:focus, + &:active { + outline: var(--pf-global--active-color--100) 3px solid; + outline-offset: -3px; + } + + .pf-c-card__body { + padding: 5px !important; + } +} diff --git a/frontend/packages/dev-console/src/components/import/builder/BuilderImageCard.tsx b/frontend/packages/dev-console/src/components/import/builder/BuilderImageCard.tsx new file mode 100644 index 00000000000..0c9938c7023 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/builder/BuilderImageCard.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { Card, CardHeader, CardBody } from '@patternfly/react-core'; +import { StarIcon } from '@patternfly/react-icons'; +import { BuilderImage } from '../../../utils/imagestream-utils'; + +import './BuilderImageCard.scss'; + +export interface BuilderImageCardProps { + image: BuilderImage; + selected: boolean; + recommended: boolean; + onChange: (name: string) => void; +} + +const BuilderImageCard: React.FC = ({ + image, + selected, + recommended, + onChange, +}) => { + const classes = classNames('odc-builder-image-card', { 'is-selected': selected }); + return ( + onChange(image.name)} + > + + {image.displayName} + + + {image.title} + + {recommended && ( + + + + )} + + ); +}; + +export default BuilderImageCard; diff --git a/frontend/packages/dev-console/src/components/import/builder/BuilderImageSelector.scss b/frontend/packages/dev-console/src/components/import/builder/BuilderImageSelector.scss new file mode 100644 index 00000000000..3069d28dc72 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/builder/BuilderImageSelector.scss @@ -0,0 +1,13 @@ +.odc-builder-image-selector { + display: flex; + flex-direction: column; + flex-flow: wrap; + background: var(--pf-global--Color--light-200); + padding: 4px; + + &__success-icon { + color: var(--pf-global--success-color--100); + font-size: var(--pf-global--FontSize--md); + margin-left: var(--pf-global--spacer--sm); + } +} diff --git a/frontend/packages/dev-console/src/components/import/builder/BuilderImageSelector.tsx b/frontend/packages/dev-console/src/components/import/builder/BuilderImageSelector.tsx new file mode 100644 index 00000000000..e8d27b4bbb8 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/builder/BuilderImageSelector.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { useField, useFormikContext, FormikValues } from 'formik'; +import { LoadingInline } from '@console/internal/components/utils'; +import { FormGroup, ControlLabel, HelpBlock } from 'patternfly-react'; +import { CheckCircleIcon, StarIcon } from '@patternfly/react-icons'; +import { NormalizedBuilderImages } from '../../../utils/imagestream-utils'; +import { getValidationState } from '../../formik-fields/field-utils'; +import BuilderImageCard from './BuilderImageCard'; + +import './BuilderImageSelector.scss'; + +export interface BuilderImageSelectorProps { + loadingImageStream: boolean; + loadingRecommendedImage?: boolean; + builderImages: NormalizedBuilderImages; +} + +const BuilderImageSelector: React.FC = ({ + loadingImageStream, + loadingRecommendedImage, + builderImages, +}) => { + const [selected, { error: selectedError, touched: selectedTouched }] = useField('image.selected'); + const [recommended] = useField('image.recommended'); + const { values, setValues, setFieldTouched } = useFormikContext(); + + const handleImageChange = (image: string) => { + const newValues = { + ...values, + image: { + ...values.image, + selected: image, + tag: builderImages[image].recentTag.name, + }, + }; + setValues(newValues); + setFieldTouched('image.selected', true); + setFieldTouched('image.tag', true); + }; + + return ( + + Builder Image + {loadingRecommendedImage && } + {recommended.value && ( + + + + Recommended builder images are represented by{' '} + icon + + + )} + {loadingImageStream ? ( + + ) : ( +
    + {_.values(builderImages).map((image) => ( + + ))} +
    + )} + {selectedTouched && selectedError && {selectedError}} +
    + ); +}; + +export default BuilderImageSelector; diff --git a/frontend/packages/dev-console/src/components/import/builder/BuilderImageTagSelector.tsx b/frontend/packages/dev-console/src/components/import/builder/BuilderImageTagSelector.tsx new file mode 100644 index 00000000000..32d0072851f --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/builder/BuilderImageTagSelector.tsx @@ -0,0 +1,67 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { useFormikContext, FormikValues } from 'formik'; +import { ResourceName } from '@console/internal/components/utils'; +import { getPorts } from '@console/internal/components/source-to-image'; +import { K8sResourceKind, k8sGet } from '@console/internal/module/k8s'; +import { ImageStreamTagModel } from '@console/internal/models'; +import ImageStreamInfo from '../../source-to-image/ImageStreamInfo'; +import { BuilderImage, getTagDataWithDisplayName } from '../../../utils/imagestream-utils'; +import { DropdownField } from '../../formik-fields'; + +export interface BuilderImageTagSelectorProps { + selectedBuilderImage: BuilderImage; + selectedImageTag: string; +} + +const BuilderImageTagSelector: React.FC = ({ + selectedBuilderImage, + selectedImageTag, +}) => { + const { setFieldValue } = useFormikContext(); + const { + name: imageName, + tags: imageTags, + displayName: imageDisplayName, + imageStreamNamespace, + } = selectedBuilderImage; + + const tagItems = {}; + _.each( + imageTags, + ({ name }) => (tagItems[name] = ), + ); + + const [imageTag, displayName] = getTagDataWithDisplayName( + imageTags, + selectedImageTag, + imageDisplayName, + ); + + React.useEffect(() => { + // eslint-disable-next-line promise/catch-or-return + k8sGet(ImageStreamTagModel, `${imageName}:${selectedImageTag}`, imageStreamNamespace).then( + (imageStreamTag: K8sResourceKind) => { + const ports = getPorts(imageStreamTag); + setFieldValue('image.ports', ports); + }, + ); + }, [imageName, imageStreamNamespace, selectedImageTag, setFieldValue]); + + return ( + + + {imageTag && } + + ); +}; + +export default BuilderImageTagSelector; diff --git a/frontend/packages/dev-console/src/components/import/builder/BuilderSection.tsx b/frontend/packages/dev-console/src/components/import/builder/BuilderSection.tsx new file mode 100644 index 00000000000..7081fcb5c36 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/builder/BuilderSection.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import { NormalizedBuilderImages } from '../../../utils/imagestream-utils'; +import { ImageData } from '../import-types'; +import FormSection from '../section/FormSection'; +import BuilderImageSelector from './BuilderImageSelector'; +import BuilderImageTagSelector from './BuilderImageTagSelector'; + +export interface ImageSectionProps { + image: ImageData; + builderImages: NormalizedBuilderImages; +} + +const BuilderSection: React.FC = ({ image, builderImages }) => { + return ( + + + {image.tag && ( + + )} + + ); +}; + +export default BuilderSection; diff --git a/frontend/packages/dev-console/src/components/import/git/CreateSourceSecret.tsx b/frontend/packages/dev-console/src/components/import/git/CreateSourceSecret.tsx new file mode 100644 index 00000000000..1a9469c2df1 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/git/CreateSourceSecret.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { FormGroup, ControlLabel } from 'patternfly-react'; +import { Dropdown } from '@console/internal/components/utils'; +import { + BasicAuthSubform, + SSHAuthSubform, +} from '@console/internal/components/secrets/create-secret'; + +export enum SecretType { + basicAuth = 'kubernetes.io/basic-auth', + sshAuth = 'kubernetes.io/ssh-auth', +} + +export type CreateSourceSecretState = { + type: SecretType; + stringData: { + [key: string]: string; + }; + authType: SecretType.basicAuth | SecretType.sshAuth; +}; + +export type CreateSourceSecretProps = { + onChange: Function; + selectedKey: string; + secretType: SecretType; + secretName: string; + secretCredentials: { + [key: string]: string; + }; +}; + +export default class CreateSourceSecret extends React.Component< + CreateSourceSecretProps, + CreateSourceSecretState +> { + constructor(props) { + super(props); + this.state = { + type: this.props.secretType, + stringData: this.props.secretCredentials, + authType: SecretType.basicAuth, + }; + } + + onDataChange = (secretsData) => { + this.setState( + { + stringData: { ...secretsData }, + }, + () => + this.props.onChange( + this.props.secretName, + this.props.selectedKey, + this.state.type, + this.state.stringData, + ), + ); + }; + + changeAuthenticationType = (type: SecretType) => { + this.setState({ + type, + stringData: {}, + }); + }; + + render() { + const authTypes = { + [SecretType.basicAuth]: 'Basic Authentication', + [SecretType.sshAuth]: 'SSH Key', + }; + return ( + + + Authentication Type + + + + {this.state.type === SecretType.basicAuth ? ( + + ) : ( + + )} + + ); + } +} diff --git a/frontend/packages/dev-console/src/components/import/git/GitSection.tsx b/frontend/packages/dev-console/src/components/import/git/GitSection.tsx new file mode 100644 index 00000000000..207438edf19 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/git/GitSection.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { useFormikContext, FormikValues } from 'formik'; +import { ExpandCollapse } from 'patternfly-react'; +import { InputField, DropdownField } from '../../formik-fields'; +import FormSection from '../section/FormSection'; +import { GitTypes } from '../import-types'; +import { detectGitType } from '../import-validation-utils'; + +const GitSection: React.FC = () => { + const { values, setValues, setFieldTouched } = useFormikContext(); + const handleGitUrlBlur = () => { + const gitType = detectGitType(values.git.url); + const showGitType = gitType === ''; + const newValues = { + ...values, + git: { + ...values.git, + type: gitType, + showGitType, + }, + }; + setValues(newValues); + setFieldTouched('git.url', true); + setFieldTouched('git.type', showGitType); + }; + + return ( + + + {values.git.showGitType && ( + + )} + + + + + + ); +}; + +export default GitSection; diff --git a/frontend/packages/dev-console/src/components/import/git/SourceSecretSelector.tsx b/frontend/packages/dev-console/src/components/import/git/SourceSecretSelector.tsx new file mode 100644 index 00000000000..cf3c98afc02 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/git/SourceSecretSelector.tsx @@ -0,0 +1,84 @@ +import * as React from 'react'; +import { FormGroup, ControlLabel, FormControl, HelpBlock } from 'patternfly-react'; +import SourceSecretDropdown from '../../dropdown/SourceSecretDropdown'; +import CreateSourceSecret, { SecretType } from './CreateSourceSecret'; + +const CREATE_SOURCE_SECRET = 'create-source-secret'; + +interface SourceSecretSelectorProps { + namespace?: string; + sourceSecret: string; + selectedKey: string; + secretCredentials: { [key: string]: string }; + onChange: ( + name: string, + key: string, + authType?: string, + credentials?: { [key: string]: string }, + ) => void; +} + +const SourceSecretSelector: React.FC = ({ + sourceSecret, + namespace, + secretCredentials, + selectedKey, + onChange, +}) => { + const onDropdownChange = (secretName: string, key: string) => { + if (key === CREATE_SOURCE_SECRET) { + onChange('', key, SecretType.basicAuth, {}); + } else { + onChange(secretName, key); + } + }; + + const onInputChange: React.ReactEventHandler = (event) => { + onChange(event.currentTarget.value, selectedKey, SecretType.basicAuth, secretCredentials); + }; + + return ( + + + Source Secret + + + {selectedKey === CREATE_SOURCE_SECRET ? ( + + + Secret Name + + Unique name of the new secret. + + + + + + ) : null} + + ); +}; + +export default SourceSecretSelector; diff --git a/frontend/packages/dev-console/src/components/import/import-submit-utils.ts b/frontend/packages/dev-console/src/components/import/import-submit-utils.ts new file mode 100644 index 00000000000..c6f30902111 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/import-submit-utils.ts @@ -0,0 +1,239 @@ +import * as _ from 'lodash'; +import { + ImageStreamModel, + BuildConfigModel, + DeploymentConfigModel, + ServiceModel, + RouteModel, +} from '@console/internal/models'; +import { k8sCreate, K8sResourceKind } from '@console/internal/module/k8s'; +import { makePortName } from '../../utils/imagestream-utils'; +import { getAppLabels, getPodLabels } from '../../utils/resource-label-utils'; +import { GitImportFormData } from './import-types'; + +export const createImageStream = ( + formData: GitImportFormData, + { metadata: { name: imageStreamName } }: K8sResourceKind, +): Promise => { + const { + name, + project: { name: namespace }, + application: { name: application }, + labels: userLabels, + } = formData; + const defaultLabels = getAppLabels(name, application, imageStreamName); + const imageStream = { + apiVersion: 'image.openshift.io/v1', + kind: 'ImageStream', + metadata: { + name, + namespace, + labels: { ...defaultLabels, ...userLabels }, + }, + }; + + return k8sCreate(ImageStreamModel, imageStream); +}; + +export const createBuildConfig = ( + formData: GitImportFormData, + imageStream: K8sResourceKind, +): Promise => { + const { + name, + project: { name: namespace }, + application: { name: application }, + git: { url: repository, ref = 'master', dir: contextDir }, + image: { tag: selectedTag }, + build: { env, triggers }, + labels: userLabels, + } = formData; + + const defaultLabels = getAppLabels(name, application, imageStream.metadata.name); + const buildConfig = { + apiVersion: 'build.openshift.io/v1', + kind: 'BuildConfig', + metadata: { + name, + namespace, + labels: { ...defaultLabels, ...userLabels }, + }, + spec: { + output: { + to: { + kind: 'ImageStreamTag', + name: `${name}:latest`, + }, + }, + source: { + contextDir, + git: { + uri: repository, + ref, + type: 'Git', + }, + }, + strategy: { + type: 'Source', + sourceStrategy: { + env, + from: { + kind: 'ImageStreamTag', + name: `${imageStream.metadata.name}:${selectedTag}`, + namespace: imageStream.metadata.namespace, + }, + }, + }, + triggers: [ + ...(triggers.image ? [{ type: 'ImageChange', imageChange: {} }] : []), + ...(triggers.config ? [{ type: 'ConfigChange' }] : []), + ], + }, + }; + + return k8sCreate(BuildConfigModel, buildConfig); +}; + +export const createDeploymentConfig = ( + formData: GitImportFormData, + imageStream: K8sResourceKind, +): Promise => { + const { + name, + project: { name: namespace }, + application: { name: application }, + image: { ports }, + deployment: { env, replicas, triggers }, + labels: userLabels, + } = formData; + + const defaultLabels = getAppLabels(name, application, imageStream.metadata.name); + const podLabels = getPodLabels(name); + + const deploymentConfig = { + apiVersion: 'apps.openshift.io/v1', + kind: 'DeploymentConfig', + metadata: { + name, + namespace, + labels: { ...defaultLabels, ...userLabels }, + }, + spec: { + selector: podLabels, + replicas, + template: { + metadata: { + labels: podLabels, + }, + spec: { + containers: [ + { + name, + image: `${name}:latest`, + ports, + env, + }, + ], + }, + }, + triggers: [ + { + type: 'ImageChange', + imageChangeParams: { + automatic: triggers.image, + containerNames: [name], + from: { + kind: 'ImageStreamTag', + name: `${name}:latest`, + }, + }, + }, + ...(triggers.config ? [{ type: 'ConfigChange' }] : []), + ], + }, + }; + + return k8sCreate(DeploymentConfigModel, deploymentConfig); +}; + +export const createService = ( + formData: GitImportFormData, + imageStream: K8sResourceKind, +): Promise => { + const { + name, + project: { name: namespace }, + application: { name: application }, + image: { ports }, + labels: userLabels, + } = formData; + + const firstPort = _.head(ports); + const defaultLabels = getAppLabels(name, application, imageStream.metadata.name); + const podLabels = getPodLabels(name); + const service = { + kind: 'Service', + apiVersion: 'v1', + metadata: { + name, + namespace, + labels: { ...defaultLabels, ...userLabels }, + }, + spec: { + selector: podLabels, + ports: [ + { + port: firstPort.containerPort, + targetPort: firstPort.containerPort, + protocol: firstPort.protocol, + // Use the same naming convention as the CLI. + name: makePortName(firstPort), + }, + ], + }, + }; + + return k8sCreate(ServiceModel, service); +}; + +export const createRoute = ( + formData: GitImportFormData, + imageStream: K8sResourceKind, +): Promise => { + const { + name, + project: { name: namespace }, + application: { name: application }, + image: { ports }, + labels: userLabels, + } = formData; + + const firstPort = _.head(ports); + const defaultLabels = getAppLabels(name, application, imageStream.metadata.name); + const route = { + kind: 'Route', + apiVersion: 'route.openshift.io/v1', + metadata: { + name, + namespace, + labels: { ...defaultLabels, ...userLabels }, + }, + spec: { + to: { + kind: 'Service', + name, + }, + // The service created by `createService` uses the same port as the container port. + port: { + // Use the port name, not the number for targetPort. The router looks + // at endpoints, not services, when resolving ports, so port numbers + // will not resolve correctly if the service port and container port + // numbers don't match. + targetPort: makePortName(firstPort), + }, + wildcardPolicy: 'None', + }, + }; + + return k8sCreate(RouteModel, route); +}; diff --git a/frontend/packages/dev-console/src/components/import/import-types.ts b/frontend/packages/dev-console/src/components/import/import-types.ts new file mode 100644 index 00000000000..0b62c6b3fb3 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/import-types.ts @@ -0,0 +1,72 @@ +import { K8sResourceKind, ContainerPort } from '@console/internal/module/k8s'; +import { NameValuePair, NameValueFromPair } from '../formik-fields/field-types'; + +export interface FirehoseList { + data?: K8sResourceKind[]; + [key: string]: any; +} + +export interface GitImportFormData { + name: string; + project: ProjectData; + application: ApplicationData; + git: GitData; + image: ImageData; + route: RouteData; + build: BuildData; + deployment: DeploymentData; + labels: { [name: string]: string }; +} + +export interface ApplicationData { + name: string; + selectedKey: string; +} + +export interface ImageData { + selected: string; + recommended: string; + tag: string; + ports: ContainerPort[]; +} + +export interface ProjectData { + name: string; +} + +export interface GitData { + url: string; + type: string; + ref: string; + dir: string; + showGitType: boolean; +} + +export interface RouteData { + create: boolean; +} + +export interface BuildData { + triggers: { + webhook: boolean; + image: boolean; + config: boolean; + }; + env: (NameValuePair | NameValueFromPair)[]; +} + +export interface DeploymentData { + triggers: { + image: boolean; + config: boolean; + }; + replicas: number; + env: (NameValuePair | NameValueFromPair)[]; +} + +export enum GitTypes { + '' = 'Please choose Git type', + github = 'GitHub', + gitlab = 'GitLab', + bitbucket = 'Bitbucket', +} diff --git a/frontend/packages/dev-console/src/components/import/import-validation-utils.ts b/frontend/packages/dev-console/src/components/import/import-validation-utils.ts new file mode 100644 index 00000000000..12d583c6528 --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/import-validation-utils.ts @@ -0,0 +1,56 @@ +import * as yup from 'yup'; + +const urlRegex = /^(((ssh|git|https?):\/\/[\w]+)|(git@[\w]+.[\w]+:))([\w\-._~/?#[\]!$&'()*+,;=])+$/; + +export const validationSchema = yup.object().shape({ + name: yup.string().required('Required'), + project: yup.object().shape({ + name: yup.string().required('Required'), + }), + application: yup.object().shape({ + name: yup.string().required('Required'), + selectedKey: yup.string().required('Required'), + }), + image: yup.object().shape({ + selected: yup.string().required('Required'), + tag: yup.string().required('Required'), + }), + git: yup.object().shape({ + url: yup + .string() + .matches(urlRegex, 'Invalid Git URL') + .required('Required'), + type: yup.string().when('showGitType', { + is: true, + then: yup.string().required('We failed to detect the git type. Please choose a git type.'), + }), + showGitType: yup.boolean(), + }), + deployment: yup.object().shape({ + replicas: yup + .number() + .integer('Replicas must be an Integer') + .min(0, 'Replicas must be greater than or equal to 0.') + .test({ + name: 'isEmpty', + test: (value) => value !== undefined, + message: 'This field cannot be empty', + }), + }), +}); + +export const detectGitType = (url: string): string => { + if (!urlRegex.test(url)) { + return undefined; + } + if (url.includes('github.com')) { + return 'github'; + } + if (url.includes('bitbucket.org')) { + return 'bitbucket'; + } + if (url.includes('gitlab.com')) { + return 'gitlab'; + } + return ''; +}; diff --git a/frontend/packages/dev-console/src/components/import/route/RouteSection.tsx b/frontend/packages/dev-console/src/components/import/route/RouteSection.tsx new file mode 100644 index 00000000000..bda90465dcf --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/route/RouteSection.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { CheckboxField } from '../../formik-fields'; +import FormSection from '../section/FormSection'; + +const RouteSection: React.FC = () => { + return ( + + + + ); +}; + +export default RouteSection; diff --git a/frontend/packages/dev-console/src/components/import/section/FormSection.tsx b/frontend/packages/dev-console/src/components/import/section/FormSection.tsx new file mode 100644 index 00000000000..5dea7cf65ba --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/section/FormSection.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import FormSectionHeading from './FormSectionHeading'; +import FormSectionDivider from './FormSectionDivider'; +import FormSectionSubHeading from './FormSectionSubHeading'; + +export interface FormSectionProps { + title: string; + subTitle?: string; + divider?: boolean; + children: React.ReactNode; +} + +const FormSection: React.FC = ({ title, subTitle, divider, children }) => ( + + + {subTitle && } + {children} + {divider && } + +); + +export default FormSection; diff --git a/frontend/packages/dev-console/src/components/import/section/FormSectionDivider.tsx b/frontend/packages/dev-console/src/components/import/section/FormSectionDivider.tsx new file mode 100644 index 00000000000..965d47220ae --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/section/FormSectionDivider.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +const FormSectionDivider: React.FC = () => ( +
    +); + +export default FormSectionDivider; diff --git a/frontend/packages/dev-console/src/components/import/section/FormSectionHeading.tsx b/frontend/packages/dev-console/src/components/import/section/FormSectionHeading.tsx new file mode 100644 index 00000000000..d2751b7065b --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/section/FormSectionHeading.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { SectionHeading } from '@console/internal/components/utils'; + +export interface FormSectionHeadingProps { + title: string; +} + +const FormSectionHeading: React.FC = ({ title }) => ( + +); + +export default FormSectionHeading; diff --git a/frontend/packages/dev-console/src/components/import/section/FormSectionSubHeading.tsx b/frontend/packages/dev-console/src/components/import/section/FormSectionSubHeading.tsx new file mode 100644 index 00000000000..1a660ad128d --- /dev/null +++ b/frontend/packages/dev-console/src/components/import/section/FormSectionSubHeading.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { HelpBlock } from 'patternfly-react'; + +export interface FormSectionHeadingProps { + title: string; +} + +const FormSectionSubHeading = ({ subTitle }) => ( + {subTitle} +); + +export default FormSectionSubHeading; diff --git a/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunDetails.tsx b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunDetails.tsx new file mode 100644 index 00000000000..1491eabd5fd --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunDetails.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { SectionHeading, ResourceSummary } from '@console/internal/components/utils'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { PipelineRunVisualization } from './PipelineRunVisualization'; + +export interface PipelineRunDetailsProps { + obj: K8sResourceKind; +} + +export const PipelineRunDetails: React.FC = ({ obj: pipelineRun }) => { + return ( +
    + + +
    +
    + +
    +
    +
    + ); +}; diff --git a/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunDetailsPage.tsx b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunDetailsPage.tsx new file mode 100644 index 00000000000..4cee98e7c28 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunDetailsPage.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import { DetailsPage, DetailsPageProps } from '@console/internal/components/factory'; +import { navFactory } from '@console/internal/components/utils'; +import { PipelineRunDetails } from './PipelineRunDetails'; + +const PipelineRunDetailsPage: React.FC = (props) => ( + +); + +export default PipelineRunDetailsPage; diff --git a/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunHeader.tsx b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunHeader.tsx new file mode 100644 index 00000000000..b2f3b7eef24 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunHeader.tsx @@ -0,0 +1,49 @@ +import { sortable } from '@patternfly/react-table'; +import { tableColumnClasses } from './pipelinerun-table'; + +const PipelineRunHeader = () => { + return [ + { + title: 'Name', + sortField: 'metadata.name', + transforms: [sortable], + props: { className: tableColumnClasses[0] }, + }, + { + title: 'Started', + sortField: 'metadata.labels', + transforms: [sortable], + props: { className: tableColumnClasses[1] }, + }, + { + title: 'Status', + sortField: 'podPhase', + transforms: [sortable], + props: { className: tableColumnClasses[2] }, + }, + { + title: 'Task Progress', + sortField: 'podReadiness', + transforms: [sortable], + props: { className: tableColumnClasses[3] }, + }, + { + title: 'Duration', + sortField: 'podPhase', + transforms: [sortable], + props: { className: tableColumnClasses[4] }, + }, + { + title: 'Trigger', + sortField: 'podReadiness', + transforms: [sortable], + props: { className: tableColumnClasses[5] }, + }, + { + title: '', + props: { className: tableColumnClasses[6] }, + }, + ]; +}; + +export default PipelineRunHeader; diff --git a/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunList.tsx b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunList.tsx new file mode 100644 index 00000000000..4581a158552 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunList.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { Table } from '@console/internal/components/factory'; +import PipelineRunHeader from './PipelineRunHeader'; +import PipelineRunRow from './PipelineRunRow'; +import { PipelineRunModel } from '../../models'; + +export const PipelineRunList: React.FC = (props) => ( + +); + +export default PipelineRunList; diff --git a/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunRow.tsx b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunRow.tsx new file mode 100644 index 00000000000..28cb28cb064 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunRow.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { TableRow, TableData } from '@console/internal/components/factory'; +import { + Kebab, + ResourceLink, + StatusIcon, + Timestamp, + ResourceKebab, +} from '@console/internal/components/utils'; +import { referenceForModel } from '@console/internal/module/k8s'; +import { pipelineRunFilterReducer } from '../../utils/pipeline-filter-reducer'; +import { reRunPipelineRun, stopPipelineRun } from '../../utils/pipeline-actions'; +import { PipelineRun } from '../../utils/pipeline-augment'; +import { tableColumnClasses } from './pipelinerun-table'; +import { PipelineRunModel } from '../../models'; + +const pipelinerunReference = referenceForModel(PipelineRunModel); + +interface PipelineRunRowProps { + obj: PipelineRun; + index: number; + key?: string; + style: object; +} + +const PipelineRunRow: React.FC = ({ obj, index, key, style }) => { + const menuActions = [reRunPipelineRun(obj), stopPipelineRun(obj), ...Kebab.factory.common]; + return ( + + + + + + + + + + + + - + - + + {obj.spec && obj.spec.trigger && obj.spec.trigger.type ? obj.spec.trigger.type : '-'} + + + + + + ); +}; + +export default PipelineRunRow; diff --git a/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunVisualization.tsx b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunVisualization.tsx new file mode 100644 index 00000000000..55c9561ae57 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelineruns/PipelineRunVisualization.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { K8sResourceKind, k8sGet } from '@console/internal/module/k8s'; +import { PipelineVisualizationGraph } from '../pipelines/PipelineVisualizationGraph'; +import { getPipelineTasks } from '../../utils/pipeline-utils'; + +import { PipelineModel } from '../../models'; + +export interface PipelineRunVisualizationProps { + pipelineRun: K8sResourceKind; +} + +export interface PipelineVisualizationRunState { + pipeline: K8sResourceKind; +} + +export class PipelineRunVisualization extends React.Component< + PipelineRunVisualizationProps, + PipelineVisualizationRunState +> { + constructor(props) { + super(props); + this.state = { pipeline: { apiVersion: '', metadata: {}, kind: 'PipelineRun' } }; + } + + componentDidMount() { + // eslint-disable-next-line promise/catch-or-return + k8sGet( + PipelineModel, + this.props.pipelineRun.spec.pipelineRef.name, + this.props.pipelineRun.metadata.namespace, + ).then((res) => { + this.setState({ + pipeline: res, + }); + }); + } + + render() { + return ( + + ); + } +} diff --git a/frontend/packages/dev-console/src/components/pipelineruns/pipelinerun-table.ts b/frontend/packages/dev-console/src/components/pipelineruns/pipelinerun-table.ts new file mode 100644 index 00000000000..2e76442c552 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelineruns/pipelinerun-table.ts @@ -0,0 +1,11 @@ +import { Kebab } from '@console/internal/components/utils'; + +export const tableColumnClasses = [ + 'col-lg-2 col-md-2 col-sm-4 col-xs-4', + 'col-lg-2 col-md-2 col-sm-4 col-xs-4', + 'col-lg-2 col-md-2 col-sm-2 col-xs-2', + 'col-lg-2 col-md-2 hidden-sm hidden-xs', + 'col-lg-2 col-md-2 hidden-sm hidden-xs', + 'col-lg-1 col-md-1 hidden-sm hidden-xs', + Kebab.columnClass, +]; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineAugmentRuns.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineAugmentRuns.tsx new file mode 100644 index 00000000000..95a1a86dd21 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineAugmentRuns.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Table } from '@console/internal/components/factory'; +import PipelineHeader from './PipelineHeader'; +import PipelineRow from './PipelineRow'; +import { augmentRunsToData, PropPipelineData, KeyedRuns } from '../../utils/pipeline-augment'; +import { PipelineModel } from '../../models'; + +export type PipelineAugmentRunsProps = { + data?: PropPipelineData[]; + propsReferenceForRuns?: string[]; +}; + +// Firehose injects a lot of props and some of those are considered the KeyedRuns +const PipelineAugmentRuns: React.FC = ({ + data, + propsReferenceForRuns, + ...props +}) => ( +
    +); + +export default PipelineAugmentRuns; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineDetails.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineDetails.tsx new file mode 100644 index 00000000000..9d2fce9a881 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineDetails.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { SectionHeading, ResourceSummary, ResourceLink } from '@console/internal/components/utils'; +import { referenceForModel } from '@console/internal/module/k8s'; +import { PipelineVisualization } from './PipelineVisualization'; +import { TaskModel } from '../../models'; + +const PipelineDetails = ({ obj: pipeline }) => ( +
    + + +
    +
    + +
    +
    + +
    + {pipeline.spec.tasks.map((task) => { + return ( + +
    Name: {task.name}
    +
    + Ref:{' '} + +
    +
    + ); + })} +
    +
    +
    +
    +); + +export default PipelineDetails; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineDetailsPage.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineDetailsPage.tsx new file mode 100644 index 00000000000..9779356640a --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineDetailsPage.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { DetailsPage, DetailsPageProps } from '@console/internal/components/factory'; +import { Kebab, navFactory } from '@console/internal/components/utils'; +import { k8sGet, k8sList } from '@console/internal/module/k8s'; +import PipelinEnvironmentComponent from './PipelineEnvironment'; +import PipelineDetails from './PipelineDetails'; +import PipelineRuns from './PipelineRuns'; +import { triggerPipeline, rerunPipeline } from '../../utils/pipeline-actions'; +import { getLatestRun } from '../../utils/pipeline-augment'; +import { PipelineRunModel, PipelineModel } from '../../models'; + +interface PipelineDetailsPageStates { + menuActions: Function[]; +} + +class PipelineDetailsPage extends React.Component { + constructor(props) { + super(props); + this.state = { menuActions: [] }; + } + + componentDidMount() { + // eslint-disable-next-line promise/catch-or-return + k8sGet(PipelineModel, this.props.name, this.props.namespace).then((res) => { + // eslint-disable-next-line promise/no-nesting, promise/catch-or-return + k8sList(PipelineRunModel, { + labelSelector: { 'tekton.dev/pipeline': res.metadata.name }, + }).then((listres) => { + this.setState({ + menuActions: [ + triggerPipeline(res, getLatestRun({ data: listres }, 'creationTimestamp'), 'pipelines'), + rerunPipeline(res, getLatestRun({ data: listres }, 'creationTimestamp'), 'pipelines'), + ...Kebab.factory.common, + ], + }); + }); + }); + } + + render() { + return ( + + ); + } +} + +export default PipelineDetailsPage; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineEnvironment.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineEnvironment.tsx new file mode 100644 index 00000000000..83318f1fcbb --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineEnvironment.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { AsyncComponent } from '@console/internal/components/utils'; + +const PipelineEnvironmentTab = (props) => ( + + import('@console/internal/components/environment.jsx').then((c) => c.EnvironmentPage) + } + {...props} + /> +); + +const envPath = ['spec', 'containers']; +const PipelinenvironmentComponent = (props) => ( + +); + +export default PipelinenvironmentComponent; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineHeader.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineHeader.tsx new file mode 100644 index 00000000000..24d2a0f2ea3 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineHeader.tsx @@ -0,0 +1,43 @@ +import { sortable } from '@patternfly/react-table'; +import { tableColumnClasses } from './pipeline-table'; + +const PipelineHeader = () => { + return [ + { + title: 'Name', + sortField: 'metadata.name', + transforms: [sortable], + props: { className: tableColumnClasses[0] }, + }, + { + title: 'Last Run', + sortField: 'lastRun.metadata.name', + transforms: [sortable], + props: { className: tableColumnClasses[1] }, + }, + { + title: 'Last Run Status', + sortField: 'latestRun.status.succeededCondition', + transforms: [sortable], + props: { className: tableColumnClasses[2] }, + }, + { + title: 'Task Status', + sortField: 'latestRun.status.completionTime', + transforms: [sortable], + props: { className: tableColumnClasses[3] }, + }, + { + title: 'Last Run Time', + sortField: 'latestRun.status.completionTime', + transforms: [sortable], + props: { className: tableColumnClasses[4] }, + }, + { + title: '', + props: { className: tableColumnClasses[5] }, + }, + ]; +}; + +export default PipelineHeader; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineList.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineList.tsx new file mode 100644 index 00000000000..7187a3da5fc --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineList.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { Firehose } from '@console/internal/components/utils'; +import { Table } from '@console/internal/components/factory'; +import PipelineHeader from './PipelineHeader'; +import PipelineRow from './PipelineRow'; +import PipelineAugmentRuns from './PipelineAugmentRuns'; +import { getResources, PropPipelineData, Resource } from '../../utils/pipeline-augment'; +import { PipelineModel } from '../../models'; + +export interface PipelineListProps { + data?: PropPipelineData[]; +} + +const PipelineList: React.FC = (props) => { + const { propsReferenceForRuns, resources }: Resource = getResources(props.data); + return resources && resources.length > 0 ? ( + + + + ) : ( +
    + ); +}; + +export default PipelineList; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineRow.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineRow.tsx new file mode 100644 index 00000000000..38ee085da9a --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineRow.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { TableRow, TableData } from '@console/internal/components/factory'; +import { + Kebab, + ResourceLink, + StatusIcon, + Timestamp, + ResourceKebab, +} from '@console/internal/components/utils'; +import { referenceForModel } from '@console/internal/module/k8s'; +import { pipelineFilterReducer } from '../../utils/pipeline-filter-reducer'; +import { Pipeline } from '../../utils/pipeline-augment'; +import { tableColumnClasses } from './pipeline-table'; +import { PipelineModel, PipelineRunModel } from '../../models'; +import { triggerPipeline, rerunPipeline } from '../../utils/pipeline-actions'; + +const pipelineReference = referenceForModel(PipelineModel); +const pipelinerunReference = referenceForModel(PipelineRunModel); + +interface PipelineRowProps { + obj: Pipeline; + index: number; + key?: string; + style: object; +} + +const PipelineRow: React.FC = ({ obj, index, key, style }) => { + const menuActions = [ + triggerPipeline(obj, obj.latestRun, ''), + rerunPipeline(obj, obj.latestRun, ''), + ...Kebab.factory.common, + ]; + return ( + + + + + + {obj.latestRun && obj.latestRun.metadata && obj.latestRun.metadata.name ? ( + + ) : ( + '-' + )} + + + + + - + + + + + + + + ); +}; + +export default PipelineRow; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineRuns.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineRuns.tsx new file mode 100644 index 00000000000..7f0d39c2a29 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineRuns.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { ListPage } from '@console/internal/components/factory'; +import PipelineRunsList from '../pipelineruns/PipelineRunList'; +import { + pipelineRunFilterReducer, + pipelineRunStatusFilter, +} from '../../utils/pipeline-filter-reducer'; +import { PipelineRunModel } from '../../models'; + +const filters = [ + { + type: 'pipelinerun-status', + selected: ['Succeeded'], + reducer: pipelineRunFilterReducer, + items: [ + { id: 'Succeeded', title: 'Complete' }, + { id: 'Failed', title: 'Failed' }, + { id: 'Running', title: 'Running' }, + ], + filter: pipelineRunStatusFilter, + }, +]; + +interface PipelineRunsProps { + obj: any; +} + +const PipelineRuns: React.FC = ({ obj }) => ( + +); + +export default PipelineRuns; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineVisualization.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualization.tsx new file mode 100644 index 00000000000..657f728af99 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualization.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { getPipelineTasks } from '../../utils/pipeline-utils'; +import { PipelineVisualizationGraph } from './PipelineVisualizationGraph'; + +export interface PipelineVisualizationProps { + pipeline?: K8sResourceKind; +} + +export const PipelineVisualization: React.FC = ({ pipeline }) => ( + +); diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationGraph.scss b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationGraph.scss new file mode 100644 index 00000000000..5adf7944de6 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationGraph.scss @@ -0,0 +1,81 @@ +$border-color: var(--pf-global--BorderColor--light-100); +$gutter: 1.8em; + +.odc-pipeline-vis-graph { + background: var(--pf-global--BackgroundColor--100); + padding: var(--pf-global--spacer--md); + margin: var(--pf-global--spacer--md) 0; + height: 20em; + overflow: auto; + position: relative; + -webkit-overflow-scrolling: touch; + + // reset + &__stage-column { + list-style: none; + padding: 0; + margin: 0; + } + + &__stages { + display: inline-flex; + } + + &__stage { + margin: 0 2em; + + &:not(:first-child) { + & .odc-pipeline-vis-task:first-child { + &::before { + content: ''; + position: absolute; + top: 1em; + + border-top: 1px solid $border-color; + width: $gutter * 1.7 + 1; + height: 0; + } + } + } + } + + &__stage.is-parallel { + & .odc-pipeline-vis-task { + &:first-child { + &::before, + &::after { + content: ''; + position: absolute; + top: 1em; + border-top: 1px solid $border-color; + width: $gutter * 1.7 + 1; + height: 0; + } + &::before { + left: 0; + transform: translateX(-100%); + } + &::after { + right: 0; + transform: translateX(100%); + } + } + } + &:last-child { + & .odc-pipeline-vis-task:first-child { + &::after { + content: ''; + width: $gutter / 2 + 0.05; + } + } + } + } + &__stage:not(.is-parallel) { + & .odc-pipeline-vis-task:first-child { + &::before { + left: 0; + transform: translateX(-100%); + } + } + } +} diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationGraph.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationGraph.tsx new file mode 100644 index 00000000000..6f0f46dc8ee --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationGraph.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import * as cx from 'classnames'; +import { ChevronCircleRightIcon } from '@patternfly/react-icons'; +import { PipelineVisualizationTask } from './PipelineVisualizationTask'; +import { PipelineVisualizationTaskItem } from '../../utils/pipeline-utils'; + +import './PipelineVisualizationGraph.scss'; + +export interface PipelineVisualizationGraphProps { + graph: PipelineVisualizationTaskItem[][]; + namespace: string; +} + +export const PipelineVisualizationGraph: React.FC = ({ + graph, + namespace, +}) => { + return ( +
    +
    +
    +
    + +
    +
    + {graph.map((stage) => { + return ( +
    1 })} + key={stage.map((t) => t.taskRef.name).join(',')} + > +
      + {stage.map((task) => { + return ( + + ); + })} +
    +
    + ); + })} +
    +
    + ); +}; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationStepList.scss b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationStepList.scss new file mode 100644 index 00000000000..0997a79c36e --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationStepList.scss @@ -0,0 +1,20 @@ +.odc-pipeline-vis-steps-list { + list-style: none; + padding: 0; + width: 10em; + &__item { + padding: 5px; + border-radius: 10px; + margin-bottom: 10px; + border: 1px solid var(--pf-global--BorderColor--light-100); + background: var(--pf-global--BackgroundColor--200); + color: var(--pf-global--Color--dark-100); + justify-content: center; + align-items: center; + display: flex; + text-align: center; + &:last-child { + margin-bottom: 0px; + } + } +} diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationStepList.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationStepList.tsx new file mode 100644 index 00000000000..946e7b896a4 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationStepList.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +import './PipelineVisualizationStepList.scss'; + +export interface PipelineVisualizationStepListProps { + steps: { name: string }[]; +} +export const PipelineVisualizationStepList: React.FC = ({ + steps, +}) => ( +
      + {steps.map((step) => { + return ( +
    • + {step.name} +
    • + ); + })} +
    +); diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationTask.scss b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationTask.scss new file mode 100644 index 00000000000..230aeb89603 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationTask.scss @@ -0,0 +1,116 @@ +$border-color: var(--pf-global--BorderColor--light-100); +$gutter: 1.8em; +.odc-pipeline-vis-task { + position: relative; + width: 13em; + height: 2em; + white-space: normal; + border: 1px solid $border-color; + border-radius: 5px; + background-color: var(--pf-global--BackgroundColor--200); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin-top: 1.5em; + padding: 0.5em; + &__content { + width: inherit; + display: flex; + position: relative; + top: 1px; + } + + &.is-done { + color: var(--pf-global--success-color--200); + } + + &.is-running { + color: var(--pf-global--primary-color--dark-100); + } + + &.is-error { + color: var(--pf-global--danger-color--100); + } + + &.is-idle { + color: var(--pf-global--disabled-color--100); + } + + &.is-input-node { + width: 2em; + height: 2em; + border-radius: 2em; + color: var(--pf-global--BackgroundColor--200); + background: $border-color; + &::after { + content: ''; + position: absolute; + transform: translateY(-100%); + left: 2em; + bottom: -1em; + } + } + + &:not(:first-child) { + margin-top: 3em; + } + + &__stepcount { + margin: 0 4px; + white-space: nowrap; + } + + &__status { + margin: 0 4px; + width: 1.5em; + flex-grow: 0; + flex-shrink: 0; + order: -1; + display: flex; + justify-content: center; + align-items: center; + font-size: 1.1em; + } + + &__title { + flex-grow: 1; + flex-shrink: 1; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + margin: 0px 4px; + &:hover { + cursor: pointer; + } + &.is-text-center { + text-align: center; + } + } + // Connect each task + &:not(:first-child) { + &::after, + &::before { + content: ''; + top: -4em; + position: absolute; + border-bottom: 1px solid $border-color; + width: $gutter / 2; + height: calc(5em + 1px); + } + + // Right connecting lines + &::after { + right: 0; + transform: translateX(100%); + border-right: 1px solid $border-color; + } + + // Left connecting lines + &::before { + left: 0; + transform: translateX(-100%); + border-left: 1px solid $border-color; + } + } +} diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationTask.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationTask.tsx new file mode 100644 index 00000000000..0b6a561f8df --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelineVisualizationTask.tsx @@ -0,0 +1,108 @@ +import * as React from 'react'; +import * as cx from 'classnames'; +import { Tooltip } from '@patternfly/react-core'; +import { + SyncAltIcon, + CheckCircleIcon, + ErrorCircleOIcon, + CircleIcon, +} from '@patternfly/react-icons'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { Firehose } from '@console/internal/components/utils'; +import { PipelineVisualizationStepList } from './PipelineVisualizationStepList'; +import { TaskStatusClassNameMap } from '../../utils/pipeline-utils'; + +import './PipelineVisualizationTask.scss'; + +interface TaskProps { + loaded?: boolean; + task?: { + data: K8sResourceKind; + }; + status?: { + reason: string; + duration: string; + }; + namespace: string; +} + +interface PipelineVisualizationTaskProp { + namespace: string; + task: { + taskRef: { + name: string; + }; + status?: { + reason: string; + duration: string; + }; + }; + taskRun?: string; +} + +interface StatusIconProps { + status: string; +} +export const StatusIcon: React.FC = ({ status }) => { + switch (status) { + case 'In Progress': + return ; + + case 'Succeeded': + return ; + + case 'Failed': + return ; + + default: + return ; + } +}; + +export const PipelineVisualizationTask: React.FC = (props) => { + return ( + + + + ); +}; +const TaskComponent: React.FC = (props) => { + const task = props.task.data; + const { status } = props; + + const getTaskStatusClass = (taskStatus = 'Idle') => { + return TaskStatusClassNameMap[taskStatus]; + }; + return ( +
  • + } + > +
    +
    + {task.metadata ? task.metadata.name : ''} +
    + {status && status.reason && ( +
    + +
    + )} + {status && status.duration && ( +
    ({status.duration})
    + )} +
    +
    +
  • + ); +}; diff --git a/frontend/packages/dev-console/src/components/pipelines/PipelinesPage.tsx b/frontend/packages/dev-console/src/components/pipelines/PipelinesPage.tsx new file mode 100644 index 00000000000..b6b3aa3ce0d --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/PipelinesPage.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { ListPage } from '@console/internal/components/factory'; +import PipelineList from './PipelineList'; +import { pipelineFilterReducer, pipelineStatusFilter } from '../../utils/pipeline-filter-reducer'; +import { PipelineModel } from '../../models'; + +const filters = [ + { + type: 'pipeline-status', + selected: ['Running', 'Failed', 'Complete'], + reducer: pipelineFilterReducer, + items: [ + { id: 'Running', title: 'Running' }, + { id: 'Failed', title: 'Failed' }, + { id: 'Complete', title: 'Complete' }, + ], + filter: pipelineStatusFilter, + }, +]; + +const PipelinesPage: React.FC = (props) => ( + +); + +export default PipelinesPage; diff --git a/frontend/packages/dev-console/src/components/pipelines/__mocks__/PipelineVisualizationTask.tsx b/frontend/packages/dev-console/src/components/pipelines/__mocks__/PipelineVisualizationTask.tsx new file mode 100644 index 00000000000..ef30a84db77 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/__mocks__/PipelineVisualizationTask.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { TaskStatusClassNameMap } from '../../../utils/pipeline-utils'; + +export const PipelineVisualizationTask = ({ task }) => ( +
  • + {task.name} + {task.status && task.status.reason && ( + {task.status.reason} + )} + {task.status && task.status.duration && {task.status.duration}} +
  • +); diff --git a/frontend/packages/dev-console/src/components/pipelines/__tests__/Pipeline.spec.tsx b/frontend/packages/dev-console/src/components/pipelines/__tests__/Pipeline.spec.tsx new file mode 100644 index 00000000000..3a602a22a0a --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/__tests__/Pipeline.spec.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { ListPage } from '@console/internal/components/factory'; +import PipelinePage from '../PipelinesPage'; +import PipelineRuns from '../PipelineRuns'; + +const pipelinePageProps = { + namespace: 'all-namespaces', +}; + +const pipelineRunProps = { + obj: { + metadata: { + name: 'pipeline-a', + }, + }, +}; + +const pipelineWrapper = shallow(); +const pipelineRunWrapper = shallow(); + +describe('Pipeline List', () => { + it('Renders a list', () => { + expect(pipelineWrapper.exists()).toBe(true); + expect(pipelineWrapper.find(ListPage).exists()); + }); + + it('List renders Pipeline resources', () => { + expect(pipelineWrapper.exists()).toBe(true); + expect(pipelineWrapper.find(ListPage).prop('kind')).toMatch('Pipeline'); + }); + + it('List renders Pipeline namespace with all-namespaces', () => { + expect(pipelineWrapper.find(ListPage).prop('namespace')).toBe('all-namespaces'); + }); + + it('List renders Pipeline with canCreate false', () => { + expect(pipelineWrapper.find(ListPage).prop('canCreate')).toBeFalsy(); + }); + + it('List renders Pipeline with default filter', () => { + expect(pipelineWrapper.find(ListPage).prop('rowFilters')[0].type).toEqual('pipeline-status'); + expect(pipelineWrapper.find(ListPage).prop('rowFilters')[0].items).toHaveLength(3); + }); +}); + +describe('Pipeline Run List', () => { + it('Renders a list', () => { + expect(pipelineRunWrapper.exists()).toBe(true); + expect(pipelineRunWrapper.find(ListPage).exists()); + }); + + it('List renders PipelineRun resources', () => { + expect(pipelineRunWrapper.exists()).toBe(true); + expect(pipelineRunWrapper.find(ListPage).prop('kind')).toMatch('PipelineRun'); + }); +}); diff --git a/frontend/packages/dev-console/src/components/pipelines/__tests__/PipelineVisualizationGraph.spec.tsx b/frontend/packages/dev-console/src/components/pipelines/__tests__/PipelineVisualizationGraph.spec.tsx new file mode 100644 index 00000000000..d0513cb85ec --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/__tests__/PipelineVisualizationGraph.spec.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import * as Renderer from 'react-test-renderer'; +import { PipelineVisualizationGraph } from '../PipelineVisualizationGraph'; +import { PipelineVisualizationProps } from '../PipelineVisualization'; +import { mockPipelineGraph } from './pipeline-visualization-test-data'; +import { getPipelineTasks } from '../../../utils/pipeline-utils'; +import { mockPipeline } from './pipeline-mock'; +import { mockPipelineRun } from './pipelinerun-mock'; + +jest.mock('react-dom', () => ({ + findDOMNode: () => ({}), + createPortal: (node) => node, +})); + +jest.mock('../PipelineVisualizationTask'); +describe('PipelineVisualizationGraph', () => { + const props = { + namespace: 'test', + graph: mockPipelineGraph, + }; + let wrapper: ShallowWrapper; + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders a Pipeline visualization graph', () => { + expect(wrapper.exists()).toBeTruthy(); + }); + + it('should contain right number of stages', () => { + const noOfStages = wrapper.find('.odc-pipeline-vis-graph__stage-column').length; + + expect(noOfStages).toEqual(mockPipelineGraph.length); + }); + + it('should match the previous pipeline snapshot', () => { + const tree = Renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should match the previous pipelineRun snapshot', () => { + const graph = getPipelineTasks(mockPipeline, mockPipelineRun); + const tree = Renderer.create( + , + ).toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/frontend/packages/dev-console/src/components/pipelines/__tests__/__snapshots__/PipelineVisualizationGraph.spec.tsx.snap b/frontend/packages/dev-console/src/components/pipelines/__tests__/__snapshots__/PipelineVisualizationGraph.spec.tsx.snap new file mode 100644 index 00000000000..bb03f0a5061 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/__tests__/__snapshots__/PipelineVisualizationGraph.spec.tsx.snap @@ -0,0 +1,197 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelineVisualizationGraph should match the previous pipeline snapshot 1`] = ` +
    +
    +
    +
    + + + +
    +
    +
    +
      +
    • + + start-app + +
    • +
    +
    +
    +
      +
    • + + test-app-1 + +
    • +
    • + + test-app-2 + +
    • +
    +
    +
    +
      +
    • + + build-image-1 + +
    • +
    • + + build-image-2 + +
    • +
    +
    +
    +
      +
    • + + deploy + +
    • +
    +
    +
    +
    +`; + +exports[`PipelineVisualizationGraph should match the previous pipelineRun snapshot 1`] = ` +
    +
    +
    +
    + + + +
    +
    +
    +
      +
    • + + build-skaffold-web + + + Succeeded + + + 32s + +
    • +
    +
    +
    +
      +
    • + + deploy-web + + + Succeeded + + + 32s + +
    • +
    +
    +
    +
    +`; diff --git a/frontend/packages/dev-console/src/components/pipelines/__tests__/pipeline-mock.ts b/frontend/packages/dev-console/src/components/pipelines/__tests__/pipeline-mock.ts new file mode 100644 index 00000000000..41808412786 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/__tests__/pipeline-mock.ts @@ -0,0 +1,94 @@ +export const mockPipeline = { + apiVersion: 'tekton.dev/v1alpha1', + kind: 'Pipeline', + metadata: { + annotations: { + 'kubectl.kubernetes.io/last-applied-configuration': + '{"apiVersion":"tekton.dev/v1alpha1","kind":"Pipeline","metadata":{"annotations":{},"name":"tutorial-pipeline","namespace":"tekton-pipelines"},"spec":{"resources":[{"name":"source-repo","type":"git"},{"name":"web-image","type":"image"}],"tasks":[{"name":"build-skaffold-web","params":[{"name":"pathToDockerFile","value":"Dockerfile"},{"name":"pathToContext","value":"/workspace/docker-source/examples/microservices/leeroy-web"}],"resources":{"inputs":[{"name":"docker-source","resource":"source-repo"}],"outputs":[{"name":"builtImage","resource":"web-image"}]},"taskRef":{"name":"build-docker-image-from-git-source"}},{"name":"deploy-web","params":[{"name":"path","value":"/workspace/source/examples/microservices/leeroy-web/kubernetes/deployment.yaml"},{"name":"yqArg","value":"-d1"},{"name":"yamlPathToImage","value":"spec.template.spec.containers[0].image"}],"resources":{"inputs":[{"name":"source","resource":"source-repo"},{"from":["build-skaffold-web"],"name":"image","resource":"web-image"}]},"taskRef":{"name":"deploy-using-kubectl"}}]}}\n', + }, + creationTimestamp: '2019-06-08T17:22:54Z', + generation: 1, + name: 'tutorial-pipeline', + namespace: 'tekton-pipelines', + resourceVersion: '517078', + selfLink: '/apis/tekton.dev/v1alpha1/namespaces/tekton-pipelines/pipelines/tutorial-pipeline', + uid: '0d4a9e56-8a12-11e9-8eab-52fdfc072182', + }, + spec: { + resources: [ + { + name: 'source-repo', + type: 'git', + }, + { + name: 'web-image', + type: 'image', + }, + ], + tasks: [ + { + name: 'build-skaffold-web', + params: [ + { + name: 'pathToDockerFile', + value: 'Dockerfile', + }, + { + name: 'pathToContext', + value: '/workspace/docker-source/examples/microservices/leeroy-web', + }, + ], + resources: { + inputs: [ + { + name: 'docker-source', + resource: 'source-repo', + }, + ], + outputs: [ + { + name: 'builtImage', + resource: 'web-image', + }, + ], + }, + taskRef: { + name: 'build-docker-image-from-git-source', + }, + }, + { + name: 'deploy-web', + params: [ + { + name: 'path', + value: '/workspace/source/examples/microservices/leeroy-web/kubernetes/deployment.yaml', + }, + { + name: 'yqArg', + value: '-d1', + }, + { + name: 'yamlPathToImage', + value: 'spec.template.spec.containers[0].image', + }, + ], + resources: { + inputs: [ + { + name: 'source', + resource: 'source-repo', + }, + { + from: ['build-skaffold-web'], + name: 'image', + resource: 'web-image', + }, + ], + }, + taskRef: { + name: 'deploy-using-kubectl', + }, + }, + ], + }, +}; diff --git a/frontend/packages/dev-console/src/components/pipelines/__tests__/pipeline-visualization-test-data.ts b/frontend/packages/dev-console/src/components/pipelines/__tests__/pipeline-visualization-test-data.ts new file mode 100644 index 00000000000..00f5d40497b --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/__tests__/pipeline-visualization-test-data.ts @@ -0,0 +1,133 @@ +import { PipelineVisualizationTaskItem } from '../../../utils/pipeline-utils'; + +export const mockPipelineGraph: PipelineVisualizationTaskItem[][] = [ + [ + { + name: 'start-app', + resources: { + inputs: [ + { + name: 'workspace-git', + resource: 'mapit-git', + }, + ], + outputs: [ + { + name: 'workspace-git', + resource: 'mapit-git', + }, + ], + }, + taskRef: { + name: 'start-app', + }, + }, + ], + [ + { + name: 'test-app-1', + resources: { + inputs: [ + { + from: ['start-app'], + name: 'workspace-git', + resource: 'mapit-git', + }, + ], + }, + taskRef: { + name: 'test-app-1', + }, + }, + { + name: 'test-app-2', + resources: { + inputs: [ + { + from: ['start-app'], + name: 'workspace-git', + resource: 'mapit-git', + }, + ], + }, + taskRef: { + name: 'test-app-2', + }, + }, + ], + [ + { + name: 'build-image-1', + params: [ + { + name: 'dockerfile', + value: 'Dockerfile.openjdk', + }, + { + name: 'verifyTLS', + value: 'false', + }, + ], + resources: { + inputs: [ + { + from: ['build-app'], + name: 'workspace-git', + resource: 'mapit-git', + }, + ], + outputs: [ + { + name: 'image', + resource: 'mapit-image', + }, + ], + }, + runAfter: ['test-app-1', 'test-app-2'], + taskRef: { + name: 'build-image-1', + }, + }, + { + name: 'build-image-2', + params: [ + { + name: 'dockerfile', + value: 'Dockerfile.openjdk', + }, + { + name: 'verifyTLS', + value: 'false', + }, + ], + resources: { + inputs: [ + { + from: ['build-app'], + name: 'workspace-git', + resource: 'mapit-git', + }, + ], + outputs: [ + { + name: 'image', + resource: 'mapit-image', + }, + ], + }, + runAfter: ['test-app-1', 'test-app-2'], + taskRef: { + name: 'build-image-2', + }, + }, + ], + [ + { + name: 'deploy', + runAfter: ['build-image-1', 'build-image-2'], + taskRef: { + name: 'openshift-cli-deploy-mapit', + }, + }, + ], +]; diff --git a/frontend/packages/dev-console/src/components/pipelines/__tests__/pipelinerun-mock.ts b/frontend/packages/dev-console/src/components/pipelines/__tests__/pipelinerun-mock.ts new file mode 100644 index 00000000000..b1c22af9cf9 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/__tests__/pipelinerun-mock.ts @@ -0,0 +1,170 @@ +export const mockPipelineRun = { + apiVersion: 'tekton.dev/v1alpha1', + kind: 'PipelineRun', + metadata: { + annotations: { + 'kubectl.kubernetes.io/last-applied-configuration': + '{"apiVersion":"tekton.dev/v1alpha1","kind":"PipelineRun","metadata":{"annotations":{},"name":"tutorial-pipeline-run-1","namespace":"tekton-pipelines"},"spec":{"pipelineRef":{"name":"tutorial-pipeline"},"resources":[{"name":"source-repo","resourceRef":{"name":"skaffold-git"}},{"name":"web-image","resourceRef":{"name":"skaffold-image-leeroy-web"}}],"trigger":{"type":"manual"}}}\n', + }, + selfLink: + '/apis/tekton.dev/v1alpha1/namespaces/tekton-pipelines/pipelineruns/tutorial-pipeline-run-1', + resourceVersion: '531347', + name: 'tutorial-pipeline-run-1', + uid: 'ebff21e8-8a1a-11e9-8eab-52fdfc072182', + creationTimestamp: '2019-06-08T18:26:24Z', + generation: 1, + namespace: 'tekton-pipelines', + labels: { + 'tekton.dev/pipeline': 'tutorial-pipeline', + }, + }, + spec: { + pipelineRef: { + name: 'tutorial-pipeline', + }, + resources: [ + { + name: 'source-repo', + resourceRef: { + name: 'skaffold-git', + }, + }, + { + name: 'web-image', + resourceRef: { + name: 'skaffold-image-leeroy-web', + }, + }, + ], + serviceAccount: '', + trigger: { + type: 'manual', + }, + }, + status: { + completionTime: '2019-06-08T18:27:28Z', + conditions: [ + { + lastTransitionTime: '2019-06-08T18:27:28Z', + message: 'All Tasks have completed executing', + reason: 'Succeeded', + status: 'True', + type: 'Succeeded', + }, + ], + startTime: '2019-06-08T18:26:24Z', + taskRuns: { + 'tutorial-pipeline-run-1-build-skaffold-web-s98hr': { + pipelineTaskName: 'build-skaffold-web', + status: { + completionTime: '2019-06-08T18:26:56Z', + conditions: [ + { + lastTransitionTime: '2019-06-08T18:26:56Z', + status: 'True', + type: 'Succeeded', + }, + ], + podName: 'tutorial-pipeline-run-1-build-skaffold-web-s98hr-pod-b0fc45', + startTime: '2019-06-08T18:26:24Z', + steps: [ + { + name: 'build-and-push', + terminated: { + containerID: + 'cri-o://dc7cdc11f878fe5d6c95ce7d3aa5a31b3586f3bbdb1fb001a0e13394f622aec2', + exitCode: 0, + finishedAt: '2019-06-08T18:26:55Z', + reason: 'Completed', + startedAt: '2019-06-08T18:26:55Z', + }, + }, + { + name: 'git-source-skaffold-git-prxcd', + terminated: { + containerID: + 'cri-o://3ba8f0645ad3160778d335bd11940ec105e8a26408abe00bf1ca7d48616e0d59', + exitCode: 0, + finishedAt: '2019-06-08T18:26:54Z', + reason: 'Completed', + startedAt: '2019-06-08T18:26:35Z', + }, + }, + { + name: 'nop', + terminated: { + containerID: + 'cri-o://64b61cac91394becb93b65e77c236d9d9c631905854a8dd16a1dbae4803faf8d', + exitCode: 0, + finishedAt: '2019-06-08T18:26:56Z', + reason: 'Completed', + startedAt: '2019-06-08T18:26:56Z', + }, + }, + ], + }, + }, + 'tutorial-pipeline-run-1-deploy-web-9sgh6': { + pipelineTaskName: 'deploy-web', + status: { + completionTime: '2019-06-08T18:27:28Z', + conditions: [ + { + lastTransitionTime: '2019-06-08T18:27:28Z', + status: 'True', + type: 'Succeeded', + }, + ], + podName: 'tutorial-pipeline-run-1-deploy-web-9sgh6-pod-a30845', + startTime: '2019-06-08T18:26:56Z', + steps: [ + { + name: 'git-source-skaffold-git-k997z', + terminated: { + containerID: + 'cri-o://354d7f3b3c543f98c6ac1fdb118777505a57d1c9d1a38520f4c82737c853053c', + exitCode: 0, + finishedAt: '2019-06-08T18:27:26Z', + reason: 'Completed', + startedAt: '2019-06-08T18:27:07Z', + }, + }, + { + name: 'post-deploy', + terminated: { + containerID: + 'cri-o://e0a755bcda299370d9886825cde7d27682644e8796a2f0d019d2d8819c068e07', + exitCode: 0, + finishedAt: '2019-06-08T18:27:27Z', + reason: 'Completed', + startedAt: '2019-06-08T18:27:25Z', + }, + }, + { + name: 'replace-image', + terminated: { + containerID: + 'cri-o://5596e313340dfeaf2b7fe53f7b13cfa2ddb293d731e5a185decf1defe838bada', + exitCode: 0, + finishedAt: '2019-06-08T18:27:26Z', + reason: 'Completed', + startedAt: '2019-06-08T18:27:15Z', + }, + }, + { + name: 'nop', + terminated: { + containerID: + 'cri-o://ae947df276fb75dbc59edcaa68f561674b8163f08156dd255a71405523a9d8d4', + exitCode: 0, + finishedAt: '2019-06-08T18:27:28Z', + reason: 'Completed', + startedAt: '2019-06-08T18:27:26Z', + }, + }, + ], + }, + }, + }, + }, +}; diff --git a/frontend/packages/dev-console/src/components/pipelines/pipeline-table.ts b/frontend/packages/dev-console/src/components/pipelines/pipeline-table.ts new file mode 100644 index 00000000000..43036dcef42 --- /dev/null +++ b/frontend/packages/dev-console/src/components/pipelines/pipeline-table.ts @@ -0,0 +1,10 @@ +import { Kebab } from '@console/internal/components/utils'; + +export const tableColumnClasses = [ + 'col-lg-2 col-md-2 col-sm-3 col-xs-5', + 'col-lg-2 col-md-2 col-sm-4 col-xs-5', + 'col-lg-2 col-md-2 col-sm-3 hidden-xs', + 'col-lg-2 col-md-2 hidden-sm hidden-xs', + 'col-lg-2 col-md-2 hidden-sm hidden-xs', + Kebab.columnClass, +]; diff --git a/frontend/packages/dev-console/src/components/source-to-image/ImageStreamInfo.tsx b/frontend/packages/dev-console/src/components/source-to-image/ImageStreamInfo.tsx new file mode 100644 index 00000000000..6145bd9fb09 --- /dev/null +++ b/frontend/packages/dev-console/src/components/source-to-image/ImageStreamInfo.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { getAnnotationTags } from '@console/internal/components/image-stream'; +import { ImageStreamIcon } from '@console/internal/components/catalog/catalog-item-icon'; +import { ExternalLink } from '@console/internal/components/utils'; +import { getSampleRepo } from '../../utils/imagestream-utils'; + +export type ImageStreamInfoProps = { + displayName: string; + tag: object; +}; + +const ImageStreamInfo: React.FC = ({ displayName, tag }) => { + const annotationTags = getAnnotationTags(tag); + const description = _.get(tag, 'annotations.description'); + const sampleRepo = getSampleRepo(tag); + + return ( +
    +
    + +
    +

    {displayName}

    + {annotationTags && ( +

    + {_.map(annotationTags, (annotationTag, i) => ( + + {annotationTag} + + ))} +

    + )} +
    +
    + {description &&

    {description}

    } + {sampleRepo && ( +

    + Sample repository: +

    + )} +
    + ); +}; + +export default ImageStreamInfo; diff --git a/frontend/packages/dev-console/src/components/source-to-image/SourceToImage.tsx b/frontend/packages/dev-console/src/components/source-to-image/SourceToImage.tsx new file mode 100644 index 00000000000..3d663ba5959 --- /dev/null +++ b/frontend/packages/dev-console/src/components/source-to-image/SourceToImage.tsx @@ -0,0 +1,403 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { connect } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { LoadingBox, LoadError } from '@console/internal/components/utils/status-box'; +import { + Dropdown, + history, + MsgBox, + NsDropdown, + ResourceName, +} from '@console/internal/components/utils'; +import { ImageStreamTagModel } from '@console/internal/models'; +import { ContainerPort, k8sGet, K8sResourceKind } from '@console/internal/module/k8s'; +import { getBuilderTagsSortedByVersion } from '@console/internal/components/image-stream'; +import { ButtonBar } from '@console/internal/components/utils/button-bar'; +import { getActivePerspective } from '@console/internal/reducers/ui'; +import { + getPorts, + getSampleRepo, + getSampleRef, + getSampleContextDir, + getTagDataWithDisplayName, +} from '../../utils/imagestream-utils'; +import ImageStreamInfo from './ImageStreamInfo'; +import { + createDeploymentConfig, + createImageStream, + createBuildConfig, + createService, + createRoute, +} from '../../utils/create-resource-utils'; +import AppNameSelector from '../dropdown/AppNameSelector'; +import SourceToImageResourceDetails from './SourceToImageResourceDetails'; + +const mapBuildSourceStateToProps = (state) => { + return { + activePerspective: getActivePerspective(state), + }; +}; + +class BuildSource extends React.Component< + BuildSourceStateProps & BuildSourceProps, + BuildSourceState +> { + constructor(props) { + super(props); + + const { preselectedNamespace: namespace = '' } = this.props; + this.state = { + tags: [], + namespace, + application: '', + selectedApplicationKey: '', + selectedTag: '', + name: '', + repository: '', + ref: '', + contextDir: '', + createRoute: false, + ports: [], + inProgress: false, + }; + } + + static getDerivedStateFromProps(props, state) { + if (_.isEmpty(props.obj.data)) { + return null; + } + const previousTag = state.selectedTag; + // Sort tags in reverse order by semver, falling back to a string comparison if not a valid version. + const tags = getBuilderTagsSortedByVersion(props.obj.data); + // Select the first tag if the current tag is missing or empty. + const selectedTag = + previousTag && _.includes(tags, previousTag) ? previousTag : _.get(_.head(tags), 'name'); + + return { tags, selectedTag }; + } + + componentDidUpdate(prevProps, prevState) { + if (prevState.selectedTag !== this.state.selectedTag) { + this.getImageStreamImage(); + } + } + + onNamespaceChange = (namespace: string) => { + this.setState({ namespace }); + }; + + onApplicationChange = (application: string, selectedKey: string) => { + this.setState({ application, selectedApplicationKey: selectedKey }); + }; + + onTagChange = (selectedTag: any) => { + this.setState({ selectedTag }, this.getImageStreamImage); + }; + + onNameChange: React.ReactEventHandler = (event) => { + this.setState({ name: event.currentTarget.value }); + }; + + onRepositoryChange: React.ReactEventHandler = (event) => { + // Reset ref and context dir if previously set from filling in a sample. + this.setState({ repository: event.currentTarget.value, ref: '', contextDir: '' }); + }; + + onCreateRouteChange: React.ReactEventHandler = (event) => { + this.setState({ createRoute: event.currentTarget.checked }); + }; + + fillSample: React.ReactEventHandler = () => { + const { + obj: { data: imageStream }, + } = this.props; + const { name: currentName, selectedTag } = this.state; + const tag = _.find(imageStream.spec.tags, { name: selectedTag }); + const repository = getSampleRepo(tag); + const ref = getSampleRef(tag); + const contextDir = getSampleContextDir(tag); + const name = currentName || imageStream.metadata.name; + this.setState({ name, repository, ref, contextDir }); + }; + + getImageStreamImage = () => { + const { selectedTag } = this.state; + if (!selectedTag) { + return; + } + + const { + obj: { data: imageStream }, + } = this.props; + const imageStreamTagName = `${imageStream.metadata.name}:${selectedTag}`; + this.setState({ inProgress: true }); + k8sGet(ImageStreamTagModel, imageStreamTagName, imageStream.metadata.namespace) + .then((imageStreamImage: K8sResourceKind) => { + const ports = getPorts(imageStreamImage); + this.setState({ ports, inProgress: false }); + return null; + }) + .catch((err) => this.setState({ error: err.message, inProgress: false })); + }; + + handleError = (err) => { + this.setState((state) => ({ + error: state.error ? `${state.error}; ${err.message}` : err.message, + })); + }; + + save = (event: React.FormEvent) => { + event.preventDefault(); + const { + activePerspective, + obj: { data: imageStream }, + } = this.props; + const { + name, + namespace, + application, + selectedTag, + repository, + createRoute: canCreateRoute, + ports, + ref, + contextDir, + } = this.state; + if (!name || !selectedTag || !namespace || !application || !repository) { + this.setState({ error: 'Please complete all required fields.' }); + return; + } + + const requests = [ + createDeploymentConfig(name, namespace, application, ports, imageStream), + createImageStream(name, namespace, application, imageStream), + createBuildConfig( + name, + namespace, + application, + repository, + ref, + contextDir, + selectedTag, + imageStream, + ), + ]; + + // Only create a service or route if the builder image has ports. + if (!_.isEmpty(ports)) { + requests.push(createService(name, namespace, application, ports, imageStream)); + if (canCreateRoute) { + requests.push(createRoute(name, namespace, application, ports, imageStream)); + } + } + + requests.forEach((r) => r.catch(this.handleError)); + this.setState({ inProgress: true, error: null }); + Promise.all(requests) + .then(() => { + this.setState({ inProgress: false }); + if (!this.state.error) { + switch (activePerspective) { + case 'dev': + history.push(`/dev/topology/ns/${this.state.namespace}`); + break; + default: + history.push(`/k8s/cluster/projects/${this.state.namespace}/workloads`); + } + } + }) + .catch(() => this.setState({ inProgress: false })); + }; + + render() { + const { obj } = this.props; + const { selectedTag, tags, ports } = this.state; + if (obj.loadError) { + return ( + + ); + } + + if (!obj.loaded) { + return ; + } + + const imageStream = obj.data; + if (_.isEmpty(tags)) { + return ( + + ); + } + + const [tag, displayName] = getTagDataWithDisplayName( + imageStream.spec.tags, + selectedTag, + imageStream.metadata.name, + ); + const sampleRepo = getSampleRepo(tag); + + const tagOptions = {}; + _.each( + tags, + ({ name }) => + (tagOptions[name] = ( + + )), + ); + return ( +
    +
    + + +
    +
    +
    +
    + + +
    + +
    + + +
    +
    + + +
    + Names the resources created for this application. +
    +
    +
    + + + {sampleRepo && ( +
    +