From 889bb69608d24d7dbd96ec1e7855b925014cfa80 Mon Sep 17 00:00:00 2001 From: Sergey Smolnikov Date: Tue, 13 Apr 2021 14:58:09 +0200 Subject: [PATCH] 83862 jobs in web console (#276) * Labels for jobs renamed to pipeline jobs * Labels for jobs renamed to pipeline jobs * Added type to component-summary * Split list to components and jobs in job Artefacts * Split list to components and jobs in environment overview * Split list to components and jobs in environment overview * Cleanup * Added job logs * Added job logs * Added job list to component summary * Fixed unit-tests * Fixed scheduled job form * Added time and durations * Extracted common logic from active component overview * Cleanup and fixes * Cleanup and fixes * Cleanup and fixes * Shown scheduler port and payload * logs used env * extracted common logic * fixed layouts * fixed layouts * fixed names and tests --- proxy/server.dev.conf | 2 +- src/components/app-navbar/index.js | 2 +- src/components/app-overview/index.js | 4 +- .../component/active-component-secrets.js | 63 +++++ .../component/component-bread-crumb.js | 34 +++ src/components/component/component-ports.js | 29 ++ src/components/component/component-secrets.js | 39 +++ src/components/component/env-variables.js | 56 ++++ .../component/job-scheduler-details.js | 50 ++++ .../configure-application-github/index.js | 2 +- src/components/events-list/index.test.js | 2 +- src/components/job-overview/component-list.js | 26 ++ src/components/job-overview/index.js | 17 +- src/components/jobs-list/index.js | 2 +- .../active-component-overview.js | 257 +++--------------- .../page-active-component/default-alias.js | 36 +++ .../horizontal-scaling-summary.js | 44 +++ .../page-active-component/overview.js | 63 +++++ .../page-active-component/replica-list.js | 54 ++++ .../active-job-component-overview.js | 114 ++++++++ .../page-active-job-component/index.js | 39 +++ .../page-active-job-component/overview.js | 24 ++ .../scheduled-job-list.js | 51 ++++ src/components/page-application/index.js | 6 +- .../page-component/component-overview.js | 166 ----------- .../change-config-branch-form.js | 2 +- .../deployment-component-overview.js | 106 ++++++++ .../index.js | 16 +- .../deployment-job-component-overview.js | 111 ++++++++ .../page-deployment-job-component/index.js | 27 ++ .../page-deployment/deployment-bread-crumb.js | 27 ++ .../deployment-component-bread-crumb.js | 41 +++ .../deployment-component-list.js | 44 +++ .../deployment-job-component-list.js | 48 ++++ .../page-deployment/deployment-overview.js | 147 +++------- .../page-deployment/deployment-summary.js | 67 +++++ src/components/page-deployment/index.js | 10 +- .../promote-deployment-action.js | 42 +++ .../page-environment/component-list-item.js | 51 ++++ .../page-environment/component-list.js | 33 +++ .../page-environment/environment-overview.js | 30 +- src/components/page-environment/index.js | 5 + .../index.js | 27 +- .../{page-job => page-pipeline-job}/index.js | 6 +- .../index.js | 12 +- .../style.css | 0 src/components/page-replica/index.js | 51 +++- src/components/page-replica/use-poll-logs.js | 6 +- .../page-replica/use-select-replica.js | 3 +- src/components/page-scheduled-job/index.js | 143 ++++++++++ .../page-scheduled-job/use-poll-logs.js | 13 + .../use-select-scheduled-job.js | 31 +++ src/components/page-step/index.js | 5 +- src/components/scheduled-job-status/index.js | 21 ++ src/components/time/duration.js | 4 +- src/models/component-summary/index.js | 2 + src/models/component-summary/test-data.js | 2 + src/models/component-type/index.js | 28 ++ src/models/component-type/normaliser.js | 0 src/models/component/index.js | 8 + src/models/component/normaliser.js | 5 + src/models/job-summary/normaliser.js | 2 +- src/models/job/test-data.js | 2 + src/models/replica-summary/index.js | 2 + src/models/replica-summary/normaliser.js | 6 + src/models/replica-summary/test-data.js | 4 + src/models/scheduled-job-summary/index.js | 12 + .../scheduled-job-summary/normaliser.js | 18 ++ src/models/scheduled-job-summary/test-data.js | 46 ++++ src/routes.js | 3 + src/state/environment/index.js | 8 +- src/utils/routing.js | 22 +- src/utils/string.js | 2 + 73 files changed, 1884 insertions(+), 599 deletions(-) create mode 100644 src/components/component/active-component-secrets.js create mode 100644 src/components/component/component-bread-crumb.js create mode 100644 src/components/component/component-ports.js create mode 100644 src/components/component/component-secrets.js create mode 100644 src/components/component/env-variables.js create mode 100644 src/components/component/job-scheduler-details.js create mode 100644 src/components/job-overview/component-list.js create mode 100644 src/components/page-active-component/default-alias.js create mode 100644 src/components/page-active-component/horizontal-scaling-summary.js create mode 100644 src/components/page-active-component/overview.js create mode 100644 src/components/page-active-component/replica-list.js create mode 100644 src/components/page-active-job-component/active-job-component-overview.js create mode 100644 src/components/page-active-job-component/index.js create mode 100644 src/components/page-active-job-component/overview.js create mode 100644 src/components/page-active-job-component/scheduled-job-list.js delete mode 100644 src/components/page-component/component-overview.js create mode 100644 src/components/page-deployment-component/deployment-component-overview.js rename src/components/{page-component => page-deployment-component}/index.js (66%) create mode 100644 src/components/page-deployment-job-component/deployment-job-component-overview.js create mode 100644 src/components/page-deployment-job-component/index.js create mode 100644 src/components/page-deployment/deployment-bread-crumb.js create mode 100644 src/components/page-deployment/deployment-component-bread-crumb.js create mode 100644 src/components/page-deployment/deployment-component-list.js create mode 100644 src/components/page-deployment/deployment-job-component-list.js create mode 100644 src/components/page-deployment/deployment-summary.js create mode 100644 src/components/page-deployment/promote-deployment-action.js create mode 100644 src/components/page-environment/component-list-item.js create mode 100644 src/components/page-environment/component-list.js rename src/components/{page-job-new => page-pipeline-job-new}/index.js (77%) rename src/components/{page-job => page-pipeline-job}/index.js (73%) rename src/components/{page-jobs => page-pipeline-jobs}/index.js (89%) rename src/components/{page-jobs => page-pipeline-jobs}/style.css (100%) create mode 100644 src/components/page-scheduled-job/index.js create mode 100644 src/components/page-scheduled-job/use-poll-logs.js create mode 100644 src/components/page-scheduled-job/use-select-scheduled-job.js create mode 100644 src/components/scheduled-job-status/index.js create mode 100644 src/models/component-type/index.js create mode 100644 src/models/component-type/normaliser.js create mode 100644 src/models/scheduled-job-summary/index.js create mode 100644 src/models/scheduled-job-summary/normaliser.js create mode 100644 src/models/scheduled-job-summary/test-data.js diff --git a/proxy/server.dev.conf b/proxy/server.dev.conf index 1660572ef..2384336dc 100644 --- a/proxy/server.dev.conf +++ b/proxy/server.dev.conf @@ -6,7 +6,7 @@ server { error_page 500 502 503 504 /50x.html; location /api/ { - proxy_pass https://server-radix-api-qa.dev.radix.equinor.com; + proxy_pass https://server-radix-api-dev.dev.radix.equinor.com; proxy_set_header Authorization "Bearer $http_x_forwarded_access_token"; proxy_set_header x-forwarded-access-token ""; } diff --git a/src/components/app-navbar/index.js b/src/components/app-navbar/index.js index 58b3fad39..a9053965a 100644 --- a/src/components/app-navbar/index.js +++ b/src/components/app-navbar/index.js @@ -98,7 +98,7 @@ export class AppNavbar extends React.Component { /> 0 && ( -

Latest jobs

+

Latest pipeline jobs

diff --git a/src/components/component/active-component-secrets.js b/src/components/component/active-component-secrets.js new file mode 100644 index 000000000..c85bdc9a3 --- /dev/null +++ b/src/components/component/active-component-secrets.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import * as routing from '../../utils/routing'; +import SecretStatus from '../secret-status'; +import React from 'react'; +import { getEnvironment, getComponentSecret } from '../../state/environment'; +import { connect } from 'react-redux'; +import environmentModel from '../../models/environment'; + +const ActiveComponentSecrets = ({ + appName, + envName, + componentName, + secrets, + environment, +}) => { + return ( + +

Secrets

+ {secrets.length === 0 &&

This component uses no secrets

} + {secrets.length > 0 && ( +
    + {secrets.map((secretName) => { + let envSecret = getComponentSecret( + environment, + secretName, + componentName + ); + return ( +
  • + + {secretName} + {' '} + +
  • + ); + })} +
+ )} +
+ ); +}; + +ActiveComponentSecrets.propTypes = { + appName: PropTypes.string.isRequired, + envName: PropTypes.string.isRequired, + componentName: PropTypes.string.isRequired, + secrets: PropTypes.arrayOf(PropTypes.string), + environment: PropTypes.shape(environmentModel), +}; + +const mapStateToProps = (state) => ({ + environment: getEnvironment(state), +}); + +export default connect(mapStateToProps)(ActiveComponentSecrets); diff --git a/src/components/component/component-bread-crumb.js b/src/components/component/component-bread-crumb.js new file mode 100644 index 000000000..38ad47301 --- /dev/null +++ b/src/components/component/component-bread-crumb.js @@ -0,0 +1,34 @@ +import Breadcrumb from '../breadcrumb'; +import { routeWithParams } from '../../utils/string'; +import routes from '../../routes'; +import * as routing from '../../utils/routing'; +import EnvironmentBadge from '../environment-badge'; +import React from 'react'; +import PropTypes from 'prop-types'; + +const ComponentBreadCrumb = ({ appName, envName, componentName }) => { + return ( + , + to: routeWithParams(routes.appEnvironment, { + appName, + envName, + }), + }, + { label: componentName }, + ]} + /> + ); +}; + +ComponentBreadCrumb.propTypes = { + appName: PropTypes.string.isRequired, + envName: PropTypes.string.isRequired, + componentName: PropTypes.string.isRequired, +}; + +export default ComponentBreadCrumb; diff --git a/src/components/component/component-ports.js b/src/components/component/component-ports.js new file mode 100644 index 000000000..b2592206e --- /dev/null +++ b/src/components/component/component-ports.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import PortModel from '../../models/port'; + +const ComponentPorts = ({ ports }) => { + return ( + + {ports.length > 0 && ( + +

Open ports:

+
    + {ports.map((port) => ( +
  • + {port.port} ({port.name}) +
  • + ))} +
+
+ )} + {ports.length === 0 &&

No open ports

} +
+ ); +}; + +ComponentPorts.propTypes = { + ports: PropTypes.arrayOf(PropTypes.exact(PortModel)), +}; + +export default ComponentPorts; diff --git a/src/components/component/component-secrets.js b/src/components/component/component-secrets.js new file mode 100644 index 000000000..431c0fec0 --- /dev/null +++ b/src/components/component/component-secrets.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { getEnvironment } from '../../state/environment'; +import { connect } from 'react-redux'; +import { buildComponentTypeLabelMap } from '../../models/component-type'; +import Component from '../../models/component'; + +const ComponentSecrets = ({ component }) => { + let componentTypeTitle = component + ? buildComponentTypeLabelMap(component.type) + : ''; + return ( + +
+

Secrets

+ {component && component.secrets.length === 0 && ( +

This {componentTypeTitle.toLowerCase()} uses no secrets

+ )} + {component && component.secrets.length > 0 && ( +
    + {component.secrets.map((secret) => ( +
  • {secret}
  • + ))} +
+ )} +
+
+ ); +}; + +ComponentSecrets.propTypes = { + component: PropTypes.arrayOf(PropTypes.shape(Component)), +}; + +const mapStateToProps = (state) => ({ + environment: getEnvironment(state), +}); + +export default connect(mapStateToProps)(ComponentSecrets); diff --git a/src/components/component/env-variables.js b/src/components/component/env-variables.js new file mode 100644 index 000000000..4f48f55d3 --- /dev/null +++ b/src/components/component/env-variables.js @@ -0,0 +1,56 @@ +import React from 'react'; + +const EnvVariables = ({ component, includeRadixVars }) => { + let hasRadixVars = false; + const envVarNames = component && Object.keys(component.variables); + + const varList = envVarNames.map((varName) => { + const isRadixVar = varName.slice(0, 6) === 'RADIX_'; + hasRadixVars = includeRadixVars && (hasRadixVars || isRadixVar); + + if (!isRadixVar) { + return ( + +
{varName}
+
{(component && component.variables)[varName]}
+
+ ); + } + + if (includeRadixVars !== true) { + return ''; + } + + return ( + +
+ * {varName} +
+
+ {(component && component.variables)[varName]} +
+
+ ); + }); + + return ( + +

Environment variables

+ {envVarNames.length === 0 && ( +

This component uses no environment variables

+ )} + {envVarNames.length > 0 && ( +
+
{varList}
+ {hasRadixVars && ( +

+ * automatically added by Radix +

+ )} +
+ )} +
+ ); +}; + +export default EnvVariables; diff --git a/src/components/component/job-scheduler-details.js b/src/components/component/job-scheduler-details.js new file mode 100644 index 000000000..b8b0ceacf --- /dev/null +++ b/src/components/component/job-scheduler-details.js @@ -0,0 +1,50 @@ +import componentModel from '../../models/component'; +import EnvVariables from '../component/env-variables'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Alert from '../alert'; + +const JobSchedulerDetails = ({ component }) => { + return ( + +

Job Scheduler:

+
    +
  • + status {component.status} +
  • +
  • + port {component.schedulerPort} +
  • +
  • + URL{' '} + + http://{component.name}:{component.schedulerPort}/api/v1 + +
  • +
  • + payload{' '} + {component.scheduledJobPayloadPath && + component.scheduledJobPayloadPath.length > 0 && ( + {component.scheduledJobPayloadPath} + )} + {!component.scheduledJobPayloadPath || + (component.scheduledJobPayloadPath.length <= 0 && ( + is empty + ))} +
  • +
+ {component.status !== 'Consistent' && ( + + Job-scheduler has been manually stopped; please note that new + deployment will cause it to be restarted + + )} +
+ ); +}; + +EnvVariables.propTypes = { + component: PropTypes.shape(componentModel), +}; + +export default JobSchedulerDetails; diff --git a/src/components/configure-application-github/index.js b/src/components/configure-application-github/index.js index 2bbd0f644..74918d2e3 100644 --- a/src/components/configure-application-github/index.js +++ b/src/components/configure-application-github/index.js @@ -76,7 +76,7 @@ export const ConfigureApplicationGithub = (props) => { setSavedSharedSecret(saveState.data.sharedSecret); resetSaveState(); onDeployKeyChange(app.name); - }, [saveState, resetSaveState, onDeployKeyChange]); + }, [saveState, resetSaveState, onDeployKeyChange, app.name]); const saveDeployKeySetting = () => { saveFunc(); diff --git a/src/components/events-list/index.test.js b/src/components/events-list/index.test.js index 1ba45c02f..a3a143740 100644 --- a/src/components/events-list/index.test.js +++ b/src/components/events-list/index.test.js @@ -184,7 +184,7 @@ const unhealthyObsolete = { const failedNonPodEvent = { lastTimestamp: new Date('2019-12-22T14:38:36Z'), - involvedObjectKind: 'Job', + involvedObjectKind: 'Pipeline Job', involvedObjectNamespace: 'myapp-production', involvedObjectName: 'auth-74cb7c986', type: 'Warning', diff --git a/src/components/job-overview/component-list.js b/src/components/job-overview/component-list.js new file mode 100644 index 000000000..52d1670b9 --- /dev/null +++ b/src/components/job-overview/component-list.js @@ -0,0 +1,26 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ComponentItem from '../../models/component-summary'; +import { + buildComponentMap, + buildComponentTypeLabelMap, +} from '../../models/component-type'; + +export const ComponentList = ({ components }) => { + let compMap = buildComponentMap(components); + return Object.keys(compMap).map((compType) => + compMap[compType].map((component) => ( +

+ {buildComponentTypeLabelMap(compType)} {component.name} +

+ )) + ); +}; + +ComponentList.propTypes = { + appName: PropTypes.string.isRequired, + jobName: PropTypes.string.isRequired, + components: PropTypes.arrayOf(PropTypes.shape(ComponentItem)).isRequired, +}; + +export default ComponentList; diff --git a/src/components/job-overview/index.js b/src/components/job-overview/index.js index ff1410780..ebb6a265c 100644 --- a/src/components/job-overview/index.js +++ b/src/components/job-overview/index.js @@ -27,6 +27,7 @@ import { import routes from '../../routes'; import './style.css'; +import { ComponentList } from './component-list'; const getExecutionState = (status) => { if (status === jobStatuses.PENDING) { @@ -77,7 +78,10 @@ const JobOverview = (props) => { @@ -103,7 +107,7 @@ const JobOverview = (props) => {

Summary

- Job {job.status.toLowerCase()};{' '} + Pipeline Job {job.status.toLowerCase()};{' '} {getExecutionState(job.status)} pipeline{' '} {job.pipeline}

@@ -162,12 +166,9 @@ const JobOverview = (props) => {

))} - {job.components && - job.components.map((component) => ( -

- Component {component.name} -

- ))} + {job.components && ( + + )}
{job.steps && ( diff --git a/src/components/jobs-list/index.js b/src/components/jobs-list/index.js index 40515a393..819b92f2e 100644 --- a/src/components/jobs-list/index.js +++ b/src/components/jobs-list/index.js @@ -25,7 +25,7 @@ const noJobsIcon = ( export const JobsList = ({ appName, jobs, limit }) => (
{jobs.length === 0 && ( - + Push to GitHub to trigger a job )} diff --git a/src/components/page-active-component/active-component-overview.js b/src/components/page-active-component/active-component-overview.js index 46c672fd6..709441f83 100644 --- a/src/components/page-active-component/active-component-overview.js +++ b/src/components/page-active-component/active-component-overview.js @@ -1,74 +1,25 @@ import { connect } from 'react-redux'; -import { faLink } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import React from 'react'; -import Alert from '../alert'; -import Breadcrumb from '../breadcrumb'; -import DockerImage from '../docker-image'; -import EnvironmentBadge from '../environment-badge'; -import ReplicaStatus from '../replica-status'; -import SecretStatus from '../secret-status'; import AsyncResource from '../async-resource'; import Toolbar from './toolbar'; - import { getAppAlias } from '../../state/application'; -import { getComponent, getSecret } from '../../state/environment'; -import { routeWithParams, smallReplicaName } from '../../utils/string'; -import * as routing from '../../utils/routing'; +import { getComponent } from '../../state/environment'; import * as subscriptionActions from '../../state/subscriptions/action-creators'; import componentModel from '../../models/component'; -import routes from '../../routes'; - -const URL_VAR_NAME = 'RADIX_PUBLIC_DOMAIN_NAME'; - -const Vars = ({ envVarNames, component }) => { - let hasRadixVars = false; - - const varList = envVarNames.map((varName) => { - const isRadixVar = varName.slice(0, 6) === 'RADIX_'; - hasRadixVars = hasRadixVars || isRadixVar; - - if (isRadixVar) { - return ( - -
- * {varName} -
-
- {component.variables[varName]} -
-
- ); - } - - return ( - -
{varName}
-
{component.variables[varName]}
-
- ); - }); - - return ( -
-
{varList}
- {hasRadixVars && ( -

- * automatically added by Radix -

- )} -
- ); -}; +import EnvVariables from '../component/env-variables'; +import HorizontalScalingSummary from './horizontal-scaling-summary'; +import ComponentPorts from '../component/component-ports'; +import ReplicaList from './replica-list'; +import ComponentBreadCrumb from '../component/component-bread-crumb'; +import Overview from './overview'; +import ActiveComponentSecrets from '../component/active-component-secrets'; export class ActiveComponentOverview extends React.Component { componentDidMount() { this.props.subscribe(this.props.appName, this.props.envName); } - componentDidUpdate(prevProps) { const { appName, envName } = this.props; @@ -83,37 +34,13 @@ export class ActiveComponentOverview extends React.Component { } render() { - const { - appAlias, - appName, - envName, - componentName, - component, - getEnvSecret, - } = this.props; - - const envVarNames = component && Object.keys(component.variables); - - const isDefaultAlias = - appAlias && - appAlias.componentName === componentName && - appAlias.environmentName === envName; - + const { appAlias, appName, envName, componentName, component } = this.props; return ( - , - to: routeWithParams(routes.appEnvironment, { - appName, - envName, - }), - }, - { label: componentName }, - ]} +
-

Overview

-

- Component {component.name} -

- {component.status === 'Stopped' && ( - - Component has been manually stopped; please note that a - new deployment will cause it to be restarted unless you - set replicas of the component to{' '} - 0 in{' '} - - radixconfig.yaml - - - )} -

- Status {component.status} -

- {component.variables[URL_VAR_NAME] && ( -

- Publically available{' '} - - link - -

- )} - {isDefaultAlias && ( - - This component is the application{' '} - - default alias{' '} - - - - )} -

- Image -

- {component.ports.length > 0 && ( - -

Open ports:

-
    - {component.ports.map((port) => ( -
  • - {port.port} ({port.name}) -
  • - ))} -
-
- )} - {component.ports.length === 0 &&

No open ports

} -

Environment variables

- {envVarNames.length === 0 && ( -

This component uses no environment variables

- )} - {envVarNames.length > 0 && ( - - )} + + +
- {component.horizontalScalingSummary && ( - -

- Horizontal scaling -

-
-
Min replicas:
-
- {component.horizontalScalingSummary.minReplicas} -
-
Max replicas:
-
- {component.horizontalScalingSummary.maxReplicas} -
-
Current average CPU utilization:
-
- { - component.horizontalScalingSummary - .currentCPUUtilizationPercentage - } - % -
-
Target average CPU utilization:
-
- { - component.horizontalScalingSummary - .targetCPUUtilizationPercentage - } - % -
-
-
- )} -

Replicas

- {component.replicaList.map((replica) => ( -

- - {smallReplicaName(replica.name)}{' '} - - -

- ))} -

Secrets

- {component.secrets.length === 0 && ( -

This component uses no secrets

- )} - {component.secrets.length > 0 && ( -
    - {component.secrets.map((secretName) => ( -
  • - - {secretName} - {' '} - -
  • - ))} -
- )} + + +
@@ -284,7 +103,6 @@ ActiveComponentOverview.propTypes = { envName: PropTypes.string.isRequired, componentName: PropTypes.string.isRequired, component: PropTypes.shape(componentModel), - getEnvSecret: PropTypes.func.isRequired, subscribe: PropTypes.func.isRequired, unsubscribe: PropTypes.func.isRequired, }; @@ -292,7 +110,6 @@ ActiveComponentOverview.propTypes = { const mapStateToProps = (state, { componentName }) => ({ appAlias: getAppAlias(state), component: getComponent(state, componentName), - getEnvSecret: (secretName) => getSecret(state, componentName, secretName), }); const mapDispatchToProps = (dispatch) => ({ diff --git a/src/components/page-active-component/default-alias.js b/src/components/page-active-component/default-alias.js new file mode 100644 index 000000000..779ac2735 --- /dev/null +++ b/src/components/page-active-component/default-alias.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faLink } from '@fortawesome/free-solid-svg-icons'; +import PropTypes from 'prop-types'; + +const DefaultAlias = (props) => { + const { appAlias, envName, componentName } = props; + const isDefaultAlias = + appAlias && + appAlias.componentName === componentName && + appAlias.environmentName === envName; + return ( + + {isDefaultAlias && ( + + This component is the application{' '} + + default alias + + + )} + + ); +}; + +DefaultAlias.propTypes = { + appAlias: PropTypes.exact({ + componentName: PropTypes.string.isRequired, + environmentName: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + }), + envName: PropTypes.string.isRequired, + componentName: PropTypes.string.isRequired, +}; + +export default DefaultAlias; diff --git a/src/components/page-active-component/horizontal-scaling-summary.js b/src/components/page-active-component/horizontal-scaling-summary.js new file mode 100644 index 000000000..3c37fed0e --- /dev/null +++ b/src/components/page-active-component/horizontal-scaling-summary.js @@ -0,0 +1,44 @@ +import componentModel from '../../models/component'; +import PropTypes from 'prop-types'; +import React from 'react'; + +const HorizontalScalingSummary = (props) => { + const { component } = props; + return ( + + {component.horizontalScalingSummary && ( + +

Horizontal scaling

+
+
Min replicas:
+
{component.horizontalScalingSummary.minReplicas}
+
Max replicas:
+
{component.horizontalScalingSummary.maxReplicas}
+
Current average CPU utilization:
+
+ { + component.horizontalScalingSummary + .currentCPUUtilizationPercentage + } + % +
+
Target average CPU utilization:
+
+ { + component.horizontalScalingSummary + .targetCPUUtilizationPercentage + } + % +
+
+
+ )} +
+ ); +}; + +HorizontalScalingSummary.propTypes = { + component: PropTypes.shape(componentModel), +}; + +export default HorizontalScalingSummary; diff --git a/src/components/page-active-component/overview.js b/src/components/page-active-component/overview.js new file mode 100644 index 000000000..8ab1e6b0c --- /dev/null +++ b/src/components/page-active-component/overview.js @@ -0,0 +1,63 @@ +import Alert from '../alert'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faLink } from '@fortawesome/free-solid-svg-icons'; +import DefaultAlias from './default-alias'; +import DockerImage from '../docker-image'; +import React from 'react'; +import componentModel from '../../models/component'; +import PropTypes from 'prop-types'; + +const URL_VAR_NAME = 'RADIX_PUBLIC_DOMAIN_NAME'; + +const Overview = ({ appAlias, envName, component }) => { + return ( + +

Overview

+

+ Component {component.name} +

+ {component.status === 'Stopped' && ( + + Component has been manually stopped; please note that a new deployment + will cause it to be restarted unless you set replicas of + the component to 0 in{' '} + + radixconfig.yaml + + + )} +

+ Status {component.status} +

+ {component.variables[URL_VAR_NAME] && ( +

+ Publicly available{' '} + + link + +

+ )} + {appAlias && ( + + )} +

+ Image +

+
+ ); +}; + +Overview.propTypes = { + appAlias: PropTypes.exact({ + environmentName: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + }), + envName: PropTypes.string.isRequired, + component: PropTypes.shape(componentModel), +}; + +export default Overview; diff --git a/src/components/page-active-component/replica-list.js b/src/components/page-active-component/replica-list.js new file mode 100644 index 000000000..ef121b8ba --- /dev/null +++ b/src/components/page-active-component/replica-list.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import ReplicaSummaryModel from '../../models/replica-summary'; +import { Link } from 'react-router-dom'; +import * as routing from '../../utils/routing'; +import { smallReplicaName } from '../../utils/string'; +import ReplicaStatus from '../replica-status'; +import RelativeToNow from '../time/relative-to-now'; +import Duration from '../time/duration'; +import React, { useEffect, useState } from 'react'; + +const ReplicaList = ({ appName, envName, componentName, replicaList }) => { + const [now, setNow] = useState(new Date()); + useEffect(() => { + setNow(new Date()); + }, [replicaList]); + return ( + +

Replicas

+ {replicaList && + replicaList.map((replica) => ( +

+ + {smallReplicaName(replica.name)}{' '} + + +    Created{' '} + + + +     Duration{' '} + + + +

+ ))} +
+ ); +}; + +ReplicaList.propTypes = { + appName: PropTypes.string.isRequired, + envName: PropTypes.string.isRequired, + componentName: PropTypes.string.isRequired, + replicaList: PropTypes.arrayOf(PropTypes.exact(ReplicaSummaryModel)), +}; + +export default ReplicaList; diff --git a/src/components/page-active-job-component/active-job-component-overview.js b/src/components/page-active-job-component/active-job-component-overview.js new file mode 100644 index 000000000..718768fb8 --- /dev/null +++ b/src/components/page-active-job-component/active-job-component-overview.js @@ -0,0 +1,114 @@ +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import AsyncResource from '../async-resource'; +import { getComponent } from '../../state/environment'; +import * as subscriptionActions from '../../state/subscriptions/action-creators'; +import componentModel from '../../models/component'; +import EnvVariables from '../component/env-variables'; +import ComponentPorts from '../component/component-ports'; +import ComponentBreadCrumb from '../component/component-bread-crumb'; +import ScheduledJobList from './scheduled-job-list'; +import JobSchedulerDetails from '../component/job-scheduler-details'; +import Overview from './overview'; +import ActiveComponentSecrets from '../component/active-component-secrets'; + +export class ActiveScheduledJobOverview extends React.Component { + componentDidMount() { + this.props.subscribe(this.props.appName, this.props.envName); + } + componentDidUpdate(prevProps) { + const { appName, envName } = this.props; + + if (appName !== prevProps.appName || envName !== prevProps.envName) { + this.props.unsubscribe(prevProps.appName, prevProps.envName); + this.props.subscribe(appName, envName); + } + } + + componentWillUnmount() { + this.props.unsubscribe(this.props.appName, this.props.envName); + } + + render() { + const { appName, envName, jobComponentName, component } = this.props; + + return ( + + +
+ + {component && ( + +
+
+ + + + +
+
+ + +
+
+
+ )} +
+
+
+ ); + } +} + +ActiveScheduledJobOverview.propTypes = { + appName: PropTypes.string.isRequired, + envName: PropTypes.string.isRequired, + jobComponentName: PropTypes.string.isRequired, + component: PropTypes.shape(componentModel), + subscribe: PropTypes.func.isRequired, + unsubscribe: PropTypes.func.isRequired, +}; + +const mapStateToProps = (state, { jobComponentName }) => ({ + component: getComponent(state, jobComponentName), +}); + +const mapDispatchToProps = (dispatch) => ({ + subscribe: (appName, envName) => { + dispatch(subscriptionActions.subscribeEnvironment(appName, envName)); + dispatch(subscriptionActions.subscribeApplication(appName)); + }, + unsubscribe: (appName, envName) => { + dispatch(subscriptionActions.unsubscribeEnvironment(appName, envName)); + dispatch(subscriptionActions.unsubscribeApplication(appName)); + }, +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ActiveScheduledJobOverview); diff --git a/src/components/page-active-job-component/index.js b/src/components/page-active-job-component/index.js new file mode 100644 index 000000000..c4df5bdff --- /dev/null +++ b/src/components/page-active-job-component/index.js @@ -0,0 +1,39 @@ +import { Route } from 'react-router'; +import React from 'react'; + +import ActiveJobComponentOverview from './active-job-component-overview'; + +import DocumentTitle from '../document-title'; +import PageSecret from '../page-secret'; + +import { mapRouteParamsToProps } from '../../utils/routing'; +import routes from '../../routes'; +import PageScheduledJob from '../page-scheduled-job'; + +export const PageActiveJobComponent = ({ + appName, + envName, + jobComponentName, +}) => ( + + + ( + + )} + /> + + + +); + +export default mapRouteParamsToProps( + ['appName', 'envName', 'jobComponentName'], + PageActiveJobComponent +); diff --git a/src/components/page-active-job-component/overview.js b/src/components/page-active-job-component/overview.js new file mode 100644 index 000000000..8d5f10363 --- /dev/null +++ b/src/components/page-active-job-component/overview.js @@ -0,0 +1,24 @@ +import DockerImage from '../docker-image'; +import React from 'react'; +import componentModel from '../../models/component'; +import PropTypes from 'prop-types'; + +const Overview = ({ component }) => { + return ( + +

Overview

+

+ Job {component.name} +

+

+ Image +

+
+ ); +}; + +Overview.propTypes = { + component: PropTypes.shape(componentModel), +}; + +export default Overview; diff --git a/src/components/page-active-job-component/scheduled-job-list.js b/src/components/page-active-job-component/scheduled-job-list.js new file mode 100644 index 000000000..9c9a118be --- /dev/null +++ b/src/components/page-active-job-component/scheduled-job-list.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import * as routing from '../../utils/routing'; +import { smallScheduledJobName } from '../../utils/string'; +import ScheduledJobStatus from '../scheduled-job-status'; +import RelativeToNow from '../time/relative-to-now'; +import ScheduledJobSummaryModel from '../../models/scheduled-job-summary'; + +const ScheduledJobList = ({ + appName, + envName, + jobComponentName, + scheduledJobList, +}) => { + return ( + +

Scheduled Job

+ {scheduledJobList.map((scheduledJob) => ( +

+ + {smallScheduledJobName(scheduledJob.name)}{' '} + + +    Created{' '} + + + +

+ ))} +
+ ); +}; + +ScheduledJobList.propTypes = { + appName: PropTypes.string.isRequired, + envName: PropTypes.string.isRequired, + jobComponentName: PropTypes.string.isRequired, + scheduledJobList: PropTypes.arrayOf( + PropTypes.exact(ScheduledJobSummaryModel) + ), +}; + +export default ScheduledJobList; diff --git a/src/components/page-application/index.js b/src/components/page-application/index.js index 70ca178d2..613eeb61a 100644 --- a/src/components/page-application/index.js +++ b/src/components/page-application/index.js @@ -16,9 +16,9 @@ import PageEnvironment from '../page-environment'; import PageEnvironments from '../page-environments'; import PageBuildSecret from '../page-build-secret'; import PagePrivateImageHub from '../page-private-image-hub'; -import PageJob from '../page-job'; -import PageJobNew from '../page-job-new'; -import PageJobs from '../page-jobs'; +import PageJob from '../page-pipeline-job'; +import PageJobNew from '../page-pipeline-job-new'; +import PageJobs from '../page-pipeline-jobs'; import { mapRouteParamsToProps } from '../../utils/routing'; import routes from '../../routes'; diff --git a/src/components/page-component/component-overview.js b/src/components/page-component/component-overview.js deleted file mode 100644 index b940ef995..000000000 --- a/src/components/page-component/component-overview.js +++ /dev/null @@ -1,166 +0,0 @@ -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import React from 'react'; - -import Breadcrumb from '../breadcrumb'; -import DockerImage from '../docker-image'; -import AsyncResource from '../async-resource'; - -import { getDeployment } from '../../state/deployment'; -import { routeWithParams, smallDeploymentName } from '../../utils/string'; -import * as actionCreators from '../../state/subscriptions/action-creators'; -import routes from '../../routes'; - -/** - * Filter for removing Radix-injected variables; those should not be displayed - * in a deployment coponent (only in a running component under an environment) - */ -const filterRadixVariables = (() => { - const radixVarRegEx = /^RADIX_/; - return (varName) => !varName.match(radixVarRegEx); -})(); - -export class DeploymentOverview extends React.Component { - componentDidMount() { - this.props.subscribe(this.props.appName, this.props.deploymentName); - } - - componentDidUpdate(prevProps) { - const { appName, deploymentName } = this.props; - - if ( - appName !== prevProps.appName || - deploymentName !== prevProps.deploymentName - ) { - this.props.unsubscribe(prevProps.appName, prevProps.deploymentName); - this.props.subscribe(appName, deploymentName); - } - } - - componentWillUnmount() { - this.props.unsubscribe(this.props.appName, this.props.deploymentName); - } - - render() { - const { appName, componentName, deploymentName, deployment } = this.props; - const component = - deployment && - deployment.components && - deployment.components.find((comp) => comp.name === componentName); - const envVarNames = - component && - Object.keys(component.variables).filter(filterRadixVariables); - - return ( - - -
- - {deployment && ( - -
-

Overview

-

- Component {component.name} -

-

- Image -

- {component.ports.length > 0 && ( - -

Open ports:

-
    - {component.ports.map((port) => ( -
  • - {port.port} ({port.name}) -
  • - ))} -
-
- )} - {component.ports.length === 0 &&

No open ports

} -
-
-
-

Environment variables

- {envVarNames.length === 0 && ( -

This component uses no environment variables

- )} - {envVarNames.length > 0 && ( -
- {envVarNames.map((varName) => ( - -
{varName}
-
{component.variables[varName]}
-
- ))} -
- )} -
-
-

Secrets

- {component.secrets.length === 0 && ( -

This component uses no secrets

- )} - {component.secrets.length > 0 && ( -
    - {component.secrets.map((secret) => ( -
  • {secret}
  • - ))} -
- )} -
-
-
- )} -
-
-
- ); - } -} - -DeploymentOverview.propTypes = { - appName: PropTypes.string.isRequired, - componentName: PropTypes.string.isRequired, - deployment: PropTypes.object, - deploymentName: PropTypes.string.isRequired, - subscribe: PropTypes.func.isRequired, - unsubscribe: PropTypes.func.isRequired, -}; - -const mapStateToProps = (state) => ({ - deployment: getDeployment(state), -}); - -const mapDispatchToProps = (dispatch) => ({ - subscribe: (appName, deploymentName) => { - dispatch(actionCreators.subscribeDeployment(appName, deploymentName)); - }, - unsubscribe: (appName, deploymentName) => { - dispatch(actionCreators.unsubscribeDeployment(appName, deploymentName)); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(DeploymentOverview); diff --git a/src/components/page-configuration/change-config-branch-form.js b/src/components/page-configuration/change-config-branch-form.js index 41a76289d..864023d7d 100644 --- a/src/components/page-configuration/change-config-branch-form.js +++ b/src/components/page-configuration/change-config-branch-form.js @@ -75,7 +75,7 @@ export const ChangeConfigBranchForm = (props) => { - Jobs + Pipeline Jobs {' '} to verify that the build-deploy job runs to completion diff --git a/src/components/page-deployment-component/deployment-component-overview.js b/src/components/page-deployment-component/deployment-component-overview.js new file mode 100644 index 000000000..cdffe70f9 --- /dev/null +++ b/src/components/page-deployment-component/deployment-component-overview.js @@ -0,0 +1,106 @@ +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import React from 'react'; +import AsyncResource from '../async-resource'; +import { getDeployment } from '../../state/deployment'; +import * as actionCreators from '../../state/subscriptions/action-creators'; +import ComponentSecrets from '../component/component-secrets'; +import EnvVariables from '../component/env-variables'; +import DeploymentComponentBreadCrumb from '../page-deployment/deployment-component-bread-crumb'; +import ComponentPorts from '../component/component-ports'; +import Overview from '../page-active-component/overview'; + +export class DeploymentComponentOverview extends React.Component { + componentDidMount() { + this.props.subscribe(this.props.appName, this.props.deploymentName); + } + + componentDidUpdate(prevProps) { + const { appName, deploymentName } = this.props; + + if ( + appName !== prevProps.appName || + deploymentName !== prevProps.deploymentName + ) { + this.props.unsubscribe(prevProps.appName, prevProps.deploymentName); + this.props.subscribe(appName, deploymentName); + } + } + + componentWillUnmount() { + this.props.unsubscribe(this.props.appName, this.props.deploymentName); + } + + render() { + const { appName, deploymentName, componentName, deployment } = this.props; + const component = + deployment && + deployment.components && + deployment.components.find((comp) => comp.name === componentName); + return ( + + +
+ + {deployment && ( + +
+
+ + +
+ +
+
+
+ +
+
+
+ )} +
+
+
+ ); + } +} + +DeploymentComponentOverview.propTypes = { + appName: PropTypes.string.isRequired, + componentName: PropTypes.string.isRequired, + deployment: PropTypes.object, + deploymentName: PropTypes.string.isRequired, + subscribe: PropTypes.func.isRequired, + unsubscribe: PropTypes.func.isRequired, +}; + +const mapStateToProps = (state) => ({ + deployment: getDeployment(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + subscribe: (appName, deploymentName) => { + dispatch(actionCreators.subscribeDeployment(appName, deploymentName)); + }, + unsubscribe: (appName, deploymentName) => { + dispatch(actionCreators.unsubscribeDeployment(appName, deploymentName)); + }, +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DeploymentComponentOverview); diff --git a/src/components/page-component/index.js b/src/components/page-deployment-component/index.js similarity index 66% rename from src/components/page-component/index.js rename to src/components/page-deployment-component/index.js index 01dac8cf6..8f52f4e54 100644 --- a/src/components/page-component/index.js +++ b/src/components/page-deployment-component/index.js @@ -1,19 +1,21 @@ import React from 'react'; -import ComponentOverview from './component-overview'; - +import DeploymentComponentOverview from './deployment-component-overview'; import DocumentTitle from '../document-title'; - import { mapRouteParamsToProps } from '../../utils/routing'; -export const PageComponent = ({ appName, deploymentName, componentName }) => { +export const PageDeploymentComponent = ({ + appName, + deploymentName, + componentName, +}) => { return ( - ); @@ -21,5 +23,5 @@ export const PageComponent = ({ appName, deploymentName, componentName }) => { export default mapRouteParamsToProps( ['appName', 'deploymentName', 'componentName'], - PageComponent + PageDeploymentComponent ); diff --git a/src/components/page-deployment-job-component/deployment-job-component-overview.js b/src/components/page-deployment-job-component/deployment-job-component-overview.js new file mode 100644 index 000000000..76961f733 --- /dev/null +++ b/src/components/page-deployment-job-component/deployment-job-component-overview.js @@ -0,0 +1,111 @@ +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import React from 'react'; +import DockerImage from '../docker-image'; +import AsyncResource from '../async-resource'; +import { getDeployment } from '../../state/deployment'; +import * as actionCreators from '../../state/subscriptions/action-creators'; +import ComponentSecrets from '../component/component-secrets'; +import EnvVariables from '../component/env-variables'; +import DeploymentComponentBreadCrumb from '../page-deployment/deployment-component-bread-crumb'; +import ComponentPorts from '../component/component-ports'; +import JobSchedulerDetails from '../component/job-scheduler-details'; +import Overview from '../page-active-job-component/overview'; + +export class DeploymentJobComponentOverview extends React.Component { + componentDidMount() { + this.props.subscribe(this.props.appName, this.props.deploymentName); + } + + componentDidUpdate(prevProps) { + const { appName, deploymentName } = this.props; + + if ( + appName !== prevProps.appName || + deploymentName !== prevProps.deploymentName + ) { + this.props.unsubscribe(prevProps.appName, prevProps.deploymentName); + this.props.subscribe(appName, deploymentName); + } + } + + componentWillUnmount() { + this.props.unsubscribe(this.props.appName, this.props.deploymentName); + } + + render() { + const { + appName, + jobComponentName, + deploymentName, + deployment, + } = this.props; + const component = + deployment && + deployment.components && + deployment.components.find((comp) => comp.name === jobComponentName); + return ( + + +
+ + {deployment && component && ( + +
+
+ + + +
+ +
+
+
+ +
+
+
+ )} +
+
+
+ ); + } +} + +DeploymentJobComponentOverview.propTypes = { + appName: PropTypes.string.isRequired, + jobComponentName: PropTypes.string.isRequired, + deployment: PropTypes.object, + deploymentName: PropTypes.string.isRequired, + subscribe: PropTypes.func.isRequired, + unsubscribe: PropTypes.func.isRequired, +}; + +const mapStateToProps = (state) => ({ + deployment: getDeployment(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + subscribe: (appName, deploymentName) => { + dispatch(actionCreators.subscribeDeployment(appName, deploymentName)); + }, + unsubscribe: (appName, deploymentName) => { + dispatch(actionCreators.unsubscribeDeployment(appName, deploymentName)); + }, +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DeploymentJobComponentOverview); diff --git a/src/components/page-deployment-job-component/index.js b/src/components/page-deployment-job-component/index.js new file mode 100644 index 000000000..e39ec7c2e --- /dev/null +++ b/src/components/page-deployment-job-component/index.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import DeploymentJobComponentOverview from './deployment-job-component-overview'; +import DocumentTitle from '../document-title'; +import { mapRouteParamsToProps } from '../../utils/routing'; + +export const PageDeploymentJobComponent = ({ + appName, + deploymentName, + jobComponentName, +}) => { + return ( + + + + + ); +}; + +export default mapRouteParamsToProps( + ['appName', 'deploymentName', 'jobComponentName'], + PageDeploymentJobComponent +); diff --git a/src/components/page-deployment/deployment-bread-crumb.js b/src/components/page-deployment/deployment-bread-crumb.js new file mode 100644 index 000000000..7e410a17e --- /dev/null +++ b/src/components/page-deployment/deployment-bread-crumb.js @@ -0,0 +1,27 @@ +import Breadcrumb from '../breadcrumb'; +import { routeWithParams, smallDeploymentName } from '../../utils/string'; +import routes from '../../routes'; +import React from 'react'; +import PropTypes from 'prop-types'; + +const DeploymentBreadCrumb = ({ appName, deploymentName }) => { + return ( + + ); +}; + +DeploymentBreadCrumb.propTypes = { + appName: PropTypes.string.isRequired, + deploymentName: PropTypes.string.isRequired, +}; + +export default DeploymentBreadCrumb; diff --git a/src/components/page-deployment/deployment-component-bread-crumb.js b/src/components/page-deployment/deployment-component-bread-crumb.js new file mode 100644 index 000000000..77b35d69a --- /dev/null +++ b/src/components/page-deployment/deployment-component-bread-crumb.js @@ -0,0 +1,41 @@ +import Breadcrumb from '../breadcrumb'; +import { routeWithParams, smallDeploymentName } from '../../utils/string'; +import routes from '../../routes'; +import React from 'react'; +import PropTypes from 'prop-types'; + +const DeploymentComponentBreadCrumb = ({ + appName, + deploymentName, + componentName, +}) => { + return ( + + ); +}; + +DeploymentComponentBreadCrumb.propTypes = { + appName: PropTypes.string.isRequired, + deploymentName: PropTypes.string.isRequired, + componentName: PropTypes.string.isRequired, +}; + +export default DeploymentComponentBreadCrumb; diff --git a/src/components/page-deployment/deployment-component-list.js b/src/components/page-deployment/deployment-component-list.js new file mode 100644 index 000000000..770772961 --- /dev/null +++ b/src/components/page-deployment/deployment-component-list.js @@ -0,0 +1,44 @@ +import Component from '../../models/component'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { routeWithParams } from '../../utils/string'; +import routes from '../../routes'; +import DockerImage from '../docker-image'; +import React from 'react'; + +const DeploymentComponentList = ({ appName, deploymentName, components }) => { + return ( + + {components && ( +
+

Components

+ {components.map((component) => { + return ( +

+ + {component.name} + +
+ image +

+ ); + })} +
+ )} +
+ ); +}; + +DeploymentComponentList.propTypes = { + appName: PropTypes.string.isRequired, + deploymentName: PropTypes.string.isRequired, + components: PropTypes.arrayOf(PropTypes.shape(Component)), +}; + +export default DeploymentComponentList; diff --git a/src/components/page-deployment/deployment-job-component-list.js b/src/components/page-deployment/deployment-job-component-list.js new file mode 100644 index 000000000..311b656aa --- /dev/null +++ b/src/components/page-deployment/deployment-job-component-list.js @@ -0,0 +1,48 @@ +import Component from '../../models/component'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import { routeWithParams } from '../../utils/string'; +import routes from '../../routes'; +import DockerImage from '../docker-image'; +import React from 'react'; + +const DeploymentJobComponentList = ({ + appName, + deploymentName, + components, +}) => { + return ( + + {components && ( +
+

Jobs

+ {components.map((component) => { + return ( +

+ + {component.name} + +
+ image +

+ ); + })} +
+ )} +
+ ); +}; + +DeploymentJobComponentList.propTypes = { + appName: PropTypes.string.isRequired, + deploymentName: PropTypes.string.isRequired, + components: PropTypes.arrayOf(PropTypes.shape(Component)), +}; + +export default DeploymentJobComponentList; diff --git a/src/components/page-deployment/deployment-overview.js b/src/components/page-deployment/deployment-overview.js index 4f4c247fa..99bf02a04 100644 --- a/src/components/page-deployment/deployment-overview.js +++ b/src/components/page-deployment/deployment-overview.js @@ -1,30 +1,19 @@ import { connect } from 'react-redux'; import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import React from 'react'; - -import ActionsPage from '../actions-page'; import Alert from '../alert'; import AsyncResource from '../async-resource'; -import Breadcrumb from '../breadcrumb'; -import DockerImage from '../docker-image'; -import LinkButton from '../link-button'; -import RelativeToNow from '../time/relative-to-now'; - -import { - routeWithParams, - smallDeploymentName, - smallJobName, -} from '../../utils/string'; import { getDeployment } from '../../state/deployment'; import * as actionCreators from '../../state/subscriptions/action-creators'; import deploymentModel from '../../models/deployment'; -import configHandler from '../../utils/config'; -import { keys as configKeys } from '../../utils/config/keys'; - -import routes from '../../routes'; +import DeploymentSummary from './deployment-summary'; +import { buildComponentMap, componentType } from '../../models/component-type'; +import DeploymentComponentList from './deployment-component-list'; +import DeploymentJobComponentList from './deployment-job-component-list'; +import DeploymentBreadcrumb from '../page-deployment/deployment-bread-crumb'; +import PromoteDeploymentAction from './promote-deployment-action'; export class DeploymentOverview extends React.Component { componentDidMount() { @@ -49,38 +38,23 @@ export class DeploymentOverview extends React.Component { render() { const { appName, deploymentName, deployment } = this.props; + let componentMap; + if (deployment) { + componentMap = buildComponentMap(deployment.components); + } return ( - + - {deployment && - configHandler.getConfig(configKeys.FLAGS).enablePromotionPipeline && ( - - - Promote deployment… - - - )} -
+
-
-

Summary

-

- {!deployment.activeTo && ( - - Currently deployed on environment{' '} - - )} - {deployment.activeTo && ( - - Was deployed to environment{' '} - - )} - - {deployment.environment} - -

- {deployment.createdByJob && ( -

- Created by job{' '} - - {smallJobName(deployment.createdByJob)} - -

- )} -

- Active from{' '} - - - -

- {deployment.activeTo && ( -

- Active until{' '} - - - -

- )} -
-
-

Components

- {deployment.components && - deployment.components.map((component) => ( -

- - {component.name} - -
- image -

- ))} -
+ + +
)} diff --git a/src/components/page-deployment/deployment-summary.js b/src/components/page-deployment/deployment-summary.js new file mode 100644 index 000000000..d2f536a7c --- /dev/null +++ b/src/components/page-deployment/deployment-summary.js @@ -0,0 +1,67 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { routeWithParams, smallJobName } from '../../utils/string'; +import routes from '../../routes'; +import RelativeToNow from '../time/relative-to-now'; +import deploymentModel from '../../models/deployment'; +import PropTypes from 'prop-types'; + +const DeploymentSummary = ({ appName, deployment }) => { + return ( +
+

Summary

+

+ {!deployment.activeTo && ( + + Currently deployed on environment{' '} + + )} + {deployment.activeTo && ( + Was deployed to environment + )} + + {deployment.environment} + +

+ {deployment.createdByJob && ( +

+ Created by pipeline job{' '} + + {smallJobName(deployment.createdByJob)} + +

+ )} +

+ Active from{' '} + + + +

+ {deployment.activeTo && ( +

+ Active until{' '} + + + +

+ )} +
+ ); +}; + +DeploymentSummary.propTypes = { + appName: PropTypes.string.isRequired, + deployment: PropTypes.exact(deploymentModel), +}; + +export default DeploymentSummary; diff --git a/src/components/page-deployment/index.js b/src/components/page-deployment/index.js index 666bb2eb7..6184bc981 100644 --- a/src/components/page-deployment/index.js +++ b/src/components/page-deployment/index.js @@ -4,11 +4,11 @@ import React from 'react'; import DeploymentOverview from './deployment-overview'; import DocumentTitle from '../document-title'; -import PageComponent from '../page-component'; - import { mapRouteParamsToProps } from '../../utils/routing'; import { smallDeploymentName } from '../../utils/string'; import routes from '../../routes'; +import PageDeploymentComponent from '../page-deployment-component'; +import PageDeploymentJobComponent from '../page-deployment-job-component'; export const PageDeployment = ({ appName, deploymentName }) => { return ( @@ -26,7 +26,11 @@ export const PageDeployment = ({ appName, deploymentName }) => { /> )} /> - + + ); }; diff --git a/src/components/page-deployment/promote-deployment-action.js b/src/components/page-deployment/promote-deployment-action.js new file mode 100644 index 000000000..75a7ee768 --- /dev/null +++ b/src/components/page-deployment/promote-deployment-action.js @@ -0,0 +1,42 @@ +import deploymentModel from '../../models/deployment'; +import PropTypes from 'prop-types'; +import configHandler from '../../utils/config'; +import { keys as configKeys } from '../../utils/config/keys'; +import ActionsPage from '../actions-page'; +import LinkButton from '../link-button'; +import { routeWithParams } from '../../utils/string'; +import routes from '../../routes'; +import React from 'react'; + +const PromoteDeploymentAction = ({ appName, deploymentName, deployment }) => { + return ( + + {deployment && + configHandler.getConfig(configKeys.FLAGS).enablePromotionPipeline && ( + + + Promote deployment… + + + )} + + ); +}; + +PromoteDeploymentAction.propTypes = { + appName: PropTypes.string.isRequired, + deploymentName: PropTypes.string.isRequired, + deployment: PropTypes.exact(deploymentModel), +}; + +export default PromoteDeploymentAction; diff --git a/src/components/page-environment/component-list-item.js b/src/components/page-environment/component-list-item.js new file mode 100644 index 000000000..5fbdef102 --- /dev/null +++ b/src/components/page-environment/component-list-item.js @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ComponentItem from '../../models/component-summary'; +import { Link } from 'react-router-dom'; +import * as routing from '../../utils/routing'; +import ActiveComponentStatus from './active-component-status'; +import environmentModel from '../../models/environment'; +import { componentType } from '../../models/component-type'; + +export const ComponentListItem = ({ appName, environment, components }) => { + return components.map((component) => { + let activeComponentUrl = getActiveComponentUrl( + appName, + environment, + component + ); + return ( +

+ {component.name} + +

+ ); + }); +}; + +function getActiveComponentUrl(appName, environment, component) { + if (component.type === componentType.job) + return routing.getActiveJobComponentUrl( + appName, + environment.name, + component.name + ); + return routing.getActiveComponentUrl( + appName, + environment.name, + component.name + ); +} + +ComponentListItem.propTypes = { + appName: PropTypes.string.isRequired, + environment: PropTypes.shape(environmentModel), + components: PropTypes.arrayOf(PropTypes.shape(ComponentItem)).isRequired, +}; + +export default ComponentListItem; diff --git a/src/components/page-environment/component-list.js b/src/components/page-environment/component-list.js new file mode 100644 index 000000000..d4d4ebda6 --- /dev/null +++ b/src/components/page-environment/component-list.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ComponentItem from '../../models/component-summary'; +import { + buildComponentMap, + buildComponentTypeLabelPluralMap, +} from '../../models/component-type'; +import environmentModel from '../../models/environment'; +import ComponentListItem from './component-list-item'; + +export const ComponentList = ({ appName, environment, components }) => { + const compMap = buildComponentMap(components); + return Object.keys(compMap).map((componentType) => ( +
+

+ Active {buildComponentTypeLabelPluralMap(componentType)} +

+ +
+ )); +}; + +ComponentList.propTypes = { + appName: PropTypes.string.isRequired, + environment: PropTypes.shape(environmentModel), + components: PropTypes.arrayOf(PropTypes.shape(ComponentItem)).isRequired, +}; + +export default ComponentList; diff --git a/src/components/page-environment/environment-overview.js b/src/components/page-environment/environment-overview.js index 04a833853..5eed74cc0 100644 --- a/src/components/page-environment/environment-overview.js +++ b/src/components/page-environment/environment-overview.js @@ -5,7 +5,6 @@ import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import React from 'react'; -import ActiveComponentStatus from './active-component-status'; import Alert from '../alert'; import Button from '../button'; import LinkButton from '../link-button'; @@ -36,6 +35,7 @@ import { keys as configKeys } from '../../utils/config/keys'; import routes from '../../routes'; import './style.css'; +import ComponentList from './component-list'; const eventDateSorter = (a, b) => { if (a.lastTimestamp > b.lastTimestamp) { @@ -196,29 +196,11 @@ export class EnvironmentOverview extends React.Component { )} {deployment && ( -
-

Active components

- {deployment.components && - deployment.components.map((component) => ( -

- - {component.name}{' '} - - -

- ))} -
+ )}
diff --git a/src/components/page-environment/index.js b/src/components/page-environment/index.js index 1da0e7efb..a580d4506 100644 --- a/src/components/page-environment/index.js +++ b/src/components/page-environment/index.js @@ -5,6 +5,7 @@ import EnvironmentOverview from './environment-overview'; import DocumentTitle from '../document-title'; import PageActiveComponent from '../page-active-component'; +import PageActiveJobComponent from '../page-active-job-component'; import { mapRouteParamsToProps } from '../../utils/routing'; import routes from '../../routes'; @@ -21,6 +22,10 @@ export const PageEnvironment = ({ appName, envName }) => { )} /> + ); }; diff --git a/src/components/page-job-new/index.js b/src/components/page-pipeline-job-new/index.js similarity index 77% rename from src/components/page-job-new/index.js rename to src/components/page-pipeline-job-new/index.js index 4ee55cd36..a465ba07e 100644 --- a/src/components/page-job-new/index.js +++ b/src/components/page-pipeline-job-new/index.js @@ -17,7 +17,7 @@ import routes from '../../routes'; import jobActions from '../../state/job-creation/action-creators'; import { getCreationResult, getCreationState } from '../../state/job-creation'; -class PageJobNew extends React.Component { +class PagePipelineJobNew extends React.Component { componentWillUnmount() { this.props.resetCreate(); } @@ -27,20 +27,23 @@ class PageJobNew extends React.Component { return ( - +
-

New job

+

New pipeline job

- Jobs perform different actions in Radix. The pipeline of the job - defines what action to take, and it may require specific + Pipeline jobs perform different actions in Radix. The pipeline of + the job defines what action to take, and it may require specific parameters.

@@ -66,7 +69,7 @@ class PageJobNew extends React.Component { jobName: this.props.creationResult.name, })} > - Job + Pipeline Job ); @@ -76,14 +79,14 @@ class PageJobNew extends React.Component { appName: appName, })} > - jobs + Pipeline Jobs ); return (
- The job "{this.props.creationResult.name}" has been created + The pipeline job "{this.props.creationResult.name}" has been created

View {jobLink} or all {jobsLink} @@ -93,7 +96,7 @@ class PageJobNew extends React.Component { } } -PageJobNew.propTypes = { +PagePipelineJobNew.propTypes = { appName: PropTypes.string.isRequired, creationState: PropTypes.oneOf(Object.values(requestStates)).isRequired, resetCreate: PropTypes.func.isRequired, @@ -110,5 +113,5 @@ const mapDispatchToProps = (dispatch) => ({ export default mapRouteParamsToProps( ['appName'], - connect(mapStateToProps, mapDispatchToProps)(PageJobNew) + connect(mapStateToProps, mapDispatchToProps)(PagePipelineJobNew) ); diff --git a/src/components/page-job/index.js b/src/components/page-pipeline-job/index.js similarity index 73% rename from src/components/page-job/index.js rename to src/components/page-pipeline-job/index.js index 9dced18c3..9e28de456 100644 --- a/src/components/page-job/index.js +++ b/src/components/page-pipeline-job/index.js @@ -8,10 +8,10 @@ import PageStep from '../page-step'; import { mapRouteParamsToProps } from '../../utils/routing'; import routes from '../../routes'; -export const PageJob = ({ appName, jobName }) => { +export const PipelinePageJob = ({ appName, jobName }) => { return ( - + { ); }; -export default mapRouteParamsToProps(['appName', 'jobName'], PageJob); +export default mapRouteParamsToProps(['appName', 'jobName'], PipelinePageJob); diff --git a/src/components/page-jobs/index.js b/src/components/page-pipeline-jobs/index.js similarity index 89% rename from src/components/page-jobs/index.js rename to src/components/page-pipeline-jobs/index.js index 1113de59c..5d0999187 100644 --- a/src/components/page-jobs/index.js +++ b/src/components/page-pipeline-jobs/index.js @@ -18,7 +18,7 @@ import routes from '../../routes'; import './style.css'; -class PageJobs extends React.Component { +class PipelinePageJobs extends React.Component { componentDidMount() { const { subscribeJobs, appName, envName } = this.props; subscribeJobs(appName, envName); @@ -42,17 +42,17 @@ class PageJobs extends React.Component { const { appName, jobs } = this.props; return ( - +

- New Job… + New Pipeline Job… @@ -64,7 +64,7 @@ class PageJobs extends React.Component { } } -PageJobs.propTypes = { +PipelinePageJobs.propTypes = { appName: PropTypes.string.isRequired, jobs: PropTypes.arrayOf(PropTypes.shape(jobSummaryModel)).isRequired, }; @@ -82,5 +82,5 @@ const mapDispatchToProps = (dispatch) => ({ export default mapRouteParamsToProps( ['appName'], - connect(mapStateToProps, mapDispatchToProps)(PageJobs) + connect(mapStateToProps, mapDispatchToProps)(PipelinePageJobs) ); diff --git a/src/components/page-jobs/style.css b/src/components/page-pipeline-jobs/style.css similarity index 100% rename from src/components/page-jobs/style.css rename to src/components/page-pipeline-jobs/style.css diff --git a/src/components/page-replica/index.js b/src/components/page-replica/index.js index 1ee7037ff..df9340c78 100644 --- a/src/components/page-replica/index.js +++ b/src/components/page-replica/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import useGetEnvironment from '../page-environment/use-get-environment'; @@ -15,22 +15,18 @@ import routes from '../../routes'; import { mapRouteParamsToProps } from '../../utils/routing'; import { routeWithParams, smallReplicaName } from '../../utils/string'; import * as routing from '../../utils/routing'; +import RelativeToNow from '../time/relative-to-now'; +import Duration from '../time/duration'; const STATUS_OK = 'Running'; const PageReplica = (props) => { - const { - appName, - envName, - deploymentName, - componentName, - replicaName, - } = props; + const { appName, envName, componentName, replicaName } = props; const [getEnvironmentState] = useGetEnvironment(appName, envName); const [pollLogsState] = usePollLogs( appName, - deploymentName, + envName, componentName, replicaName ); @@ -39,8 +35,12 @@ const PageReplica = (props) => { componentName, replicaName ); - const replicaStatus = replica ? replica.replicaStatus : null; + const [now, setNow] = useState(new Date()); + useEffect(() => { + setNow(new Date()); + }, [pollLogsState]); const replicaLog = pollLogsState && pollLogsState.data; + const selectedReplica = replica; return ( @@ -76,15 +76,38 @@ const PageReplica = (props) => { Replica {smallReplicaName(replicaName)}, component {componentName}

+ {selectedReplica && ( +
+

+ Created{' '} + + + +

+

+ Duration{' '} + + + +

+
+ )}

- Status + Status

- {replicaStatus && replicaStatus.status !== STATUS_OK && ( + {selectedReplica && selectedReplica.status !== STATUS_OK && (

Status message is:

- {replica.statusMessage} + {selectedReplica.statusMessage}
)} + {selectedReplica && + selectedReplica.restartCount != NaN && + selectedReplica.restartCount > 0 && ( +

Restarted {selectedReplica.restartCount} times

+ )}

Log

{replicaLog && {replicaLog}} @@ -107,6 +130,6 @@ PageReplica.propTypes = { }; export default mapRouteParamsToProps( - ['appName', 'envName', 'deploymentName', 'componentName', 'replicaName'], + ['appName', 'envName', 'componentName', 'replicaName'], PageReplica ); diff --git a/src/components/page-replica/use-poll-logs.js b/src/components/page-replica/use-poll-logs.js index 02d0f8bee..34a11490c 100644 --- a/src/components/page-replica/use-poll-logs.js +++ b/src/components/page-replica/use-poll-logs.js @@ -1,11 +1,11 @@ import { usePollingPlain } from '../../effects'; -const usePollLogs = (appName, deploymentName, componentName, replicaName) => { +const usePollLogs = (appName, envName, componentName, replicaName) => { const encAppName = encodeURIComponent(appName); - const encDeployName = encodeURIComponent(deploymentName); + const encEnvName = encodeURIComponent(envName); const encComponentName = encodeURIComponent(componentName); const encReplicaName = encodeURIComponent(replicaName); - const path = `/applications/${encAppName}/deployments/${encDeployName}/components/${encComponentName}/replicas/${encReplicaName}/logs`; + const path = `/applications/${encAppName}/environments/${encEnvName}/components/${encComponentName}/replicas/${encReplicaName}/logs`; return usePollingPlain(path, 5000); }; diff --git a/src/components/page-replica/use-select-replica.js b/src/components/page-replica/use-select-replica.js index a8b4460f0..6e8bc03b9 100644 --- a/src/components/page-replica/use-select-replica.js +++ b/src/components/page-replica/use-select-replica.js @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import replicaSummaryNormaliser from '../../models/replica-summary/normaliser'; const useSelectReplica = (environment, componentName, replicaName) => { const [replica, setReplica] = useState(); @@ -15,7 +16,7 @@ const useSelectReplica = (environment, componentName, replicaName) => { component && component.replicaList ? component.replicaList.find((replica) => replica.name === replicaName) : null; - setReplica(selectedReplica); + setReplica(replicaSummaryNormaliser(selectedReplica)); }, [environment, componentName, replicaName]); return replica; diff --git a/src/components/page-scheduled-job/index.js b/src/components/page-scheduled-job/index.js new file mode 100644 index 000000000..427544d20 --- /dev/null +++ b/src/components/page-scheduled-job/index.js @@ -0,0 +1,143 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import useGetEnvironment from '../page-environment/use-get-environment'; +import usePollLogs from './use-poll-logs'; +import useSelectScheduledJob from './use-select-scheduled-job'; + +import Breadcrumb from '../breadcrumb'; +import Code from '../code'; +import EnvironmentBadge from '../environment-badge'; +import ScheduledJobStatus from '../scheduled-job-status'; +import AsyncResource from '../async-resource/simple-async-resource'; + +import routes from '../../routes'; +import { mapRouteParamsToProps } from '../../utils/routing'; +import { routeWithParams, smallScheduledJobName } from '../../utils/string'; +import * as routing from '../../utils/routing'; +import RelativeToNow from '../time/relative-to-now'; +import Duration from '../time/duration'; + +const PageScheduledJob = (props) => { + const { appName, envName, jobComponentName, scheduledJobName } = props; + + const [getEnvironmentState] = useGetEnvironment(appName, envName); + const [pollLogsState] = usePollLogs( + appName, + envName, + jobComponentName, + scheduledJobName + ); + const scheduledJob = useSelectScheduledJob( + getEnvironmentState.data, + jobComponentName, + scheduledJobName + ); + const scheduledJobStatus = scheduledJob ? scheduledJob.status : null; + const scheduledJobLog = pollLogsState && pollLogsState.data; + + return ( + + , + to: routeWithParams(routes.appEnvironment, { + appName, + envName, + }), + }, + { + to: routeWithParams(routes.appActiveJobComponent, { + appName, + envName, + jobComponentName, + }), + label: jobComponentName, + }, + { label: smallScheduledJobName(scheduledJobName) }, + ]} + /> +
+ + +
+
+

Overview

+

+ Scheduled job{' '} + {smallScheduledJobName(scheduledJobName)}, + job {jobComponentName} +

+ {scheduledJob && ( +
+

+ Created{' '} + + + +

+

+ Started{' '} + + + +

+

+ Ended{' '} + + + +

+

+ Duration{' '} + + + +

+
+ )} +

+ Status +

+ {scheduledJobLog && ( +

+

Log

+ + {scheduledJobLog && {scheduledJobLog}} + +

+ )} + {!scheduledJobLog &&

No logs

} +
+
+
+
+
+
+ ); +}; + +PageScheduledJob.propTypes = { + appName: PropTypes.string.isRequired, + jobComponentName: PropTypes.string.isRequired, + deploymentName: PropTypes.string, + envName: PropTypes.string.isRequired, + scheduledJobName: PropTypes.string.isRequired, +}; + +export default mapRouteParamsToProps( + ['appName', 'envName', 'jobComponentName', 'scheduledJobName'], + PageScheduledJob +); diff --git a/src/components/page-scheduled-job/use-poll-logs.js b/src/components/page-scheduled-job/use-poll-logs.js new file mode 100644 index 000000000..42bb0d600 --- /dev/null +++ b/src/components/page-scheduled-job/use-poll-logs.js @@ -0,0 +1,13 @@ +import { usePollingPlain } from '../../effects'; + +const usePollLogs = (appName, envName, jobComponentName, scheduledJobName) => { + const encAppName = encodeURIComponent(appName); + const encEnvName = encodeURIComponent(envName); + const encJobComponentName = encodeURIComponent(jobComponentName); + const encScheduledJobName = encodeURIComponent(scheduledJobName); + const path = `/applications/${encAppName}/environments/${encEnvName}/jobcomponents/${encJobComponentName}/scheduledjobs/${encScheduledJobName}/logs`; + + return usePollingPlain(path, 5000); +}; + +export default usePollLogs; diff --git a/src/components/page-scheduled-job/use-select-scheduled-job.js b/src/components/page-scheduled-job/use-select-scheduled-job.js new file mode 100644 index 000000000..a62604cac --- /dev/null +++ b/src/components/page-scheduled-job/use-select-scheduled-job.js @@ -0,0 +1,31 @@ +import { useState, useEffect } from 'react'; +import scheduledJobSummaryNormaliser from '../../models/scheduled-job-summary/normaliser'; + +const useSelectScheduledJob = ( + environment, + jobComponentName, + scheduledJobName +) => { + const [scheduledJob, setScheduledJob] = useState(); + + useEffect(() => { + const deployment = environment ? environment.activeDeployment : null; + + const component = + deployment && deployment.components + ? deployment.components.find((comp) => comp.name === jobComponentName) + : null; + + const selectedScheduledJob = + component && component.scheduledJobList + ? component.scheduledJobList.find( + (scheduledJob) => scheduledJob.name === scheduledJobName + ) + : null; + setScheduledJob(scheduledJobSummaryNormaliser(selectedScheduledJob)); + }, [environment, jobComponentName, scheduledJobName]); + + return scheduledJob; +}; + +export default useSelectScheduledJob; diff --git a/src/components/page-step/index.js b/src/components/page-step/index.js index 298fcf9fa..bd117abd5 100644 --- a/src/components/page-step/index.js +++ b/src/components/page-step/index.js @@ -53,7 +53,10 @@ export class PageStep extends React.Component { { + if (status === STATUS_FAIL) { + return ( + + Failing + + ); + } + + return {status}; +}; + +export default ScheduledJobStatus; diff --git a/src/components/time/duration.js b/src/components/time/duration.js index 2875cc388..06dbadbf6 100644 --- a/src/components/time/duration.js +++ b/src/components/time/duration.js @@ -6,7 +6,9 @@ export const Duration = ({ start, end, title }) => { if (!end) { return null; } - + if (end < start) { + end = start; + } return {differenceInWords(end, start)}; }; diff --git a/src/models/component-summary/index.js b/src/models/component-summary/index.js index f98ef12a8..715e5a5b3 100644 --- a/src/models/component-summary/index.js +++ b/src/models/component-summary/index.js @@ -1,6 +1,8 @@ import PropTypes from 'prop-types'; +import ComponentType from '../component-type'; export default Object.freeze({ image: PropTypes.string.isRequired, name: PropTypes.string.isRequired, + type: ComponentType.isRequired, }); diff --git a/src/models/component-summary/test-data.js b/src/models/component-summary/test-data.js index ddeeb2d3d..36802a846 100644 --- a/src/models/component-summary/test-data.js +++ b/src/models/component-summary/test-data.js @@ -3,11 +3,13 @@ export const testData = [ __testDescription: 'Normal component', name: 'component-a', image: 'an-image', + type: 'component', }, { __testDescription: 'Missing image', __testIsInvalidSample: true, name: 'component-b', + type: 'component', }, ]; diff --git a/src/models/component-type/index.js b/src/models/component-type/index.js new file mode 100644 index 000000000..e545ce828 --- /dev/null +++ b/src/models/component-type/index.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; + +export default PropTypes.oneOf(['component', 'job']); + +export const componentType = { + component: 'component', + job: 'job', +}; + +const componentTypeLabel = { component: 'Component', job: 'Job' }; +const componentTypeLabelPlural = { component: 'Components', job: 'Jobs' }; + +export const buildComponentTypeLabelMap = (type) => { + return componentTypeLabel['' + type]; +}; + +export const buildComponentTypeLabelPluralMap = (type) => { + return componentTypeLabelPlural['' + type]; +}; + +export const buildComponentMap = (components) => { + return components.reduce((componentMap, component) => { + let key = component.type; + componentMap[key] = componentMap[key] || []; + componentMap[key].push(component); + return componentMap; + }, {}); +}; diff --git a/src/models/component-type/normaliser.js b/src/models/component-type/normaliser.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/models/component/index.js b/src/models/component/index.js index dc2214b68..91ab05568 100644 --- a/src/models/component/index.js +++ b/src/models/component/index.js @@ -2,13 +2,21 @@ import PropTypes from 'prop-types'; import PortModel from '../port'; import ReplicaSummaryModel from '../replica-summary'; +import ScheduledJobSummaryModel from '../scheduled-job-summary'; +import ComponentType from '../component-type'; export default Object.freeze({ image: PropTypes.string.isRequired, name: PropTypes.string.isRequired, + type: ComponentType.isRequired, status: PropTypes.string.isRequired, ports: PropTypes.arrayOf(PropTypes.exact(PortModel)), + schedulerPort: PropTypes.string, + scheduledJobPayloadPath: PropTypes.string, replicaList: PropTypes.arrayOf(PropTypes.exact(ReplicaSummaryModel)), + scheduledJobList: PropTypes.arrayOf( + PropTypes.exact(ScheduledJobSummaryModel) + ), secrets: PropTypes.arrayOf(PropTypes.string), variables: PropTypes.objectOf(PropTypes.string), horizontalScalingSummary: PropTypes.objectOf(PropTypes.number), diff --git a/src/models/component/normaliser.js b/src/models/component/normaliser.js index 964e8513a..c8571f469 100644 --- a/src/models/component/normaliser.js +++ b/src/models/component/normaliser.js @@ -2,6 +2,7 @@ import pick from 'lodash/pick'; import portNormaliser from '../port/normaliser'; import replicaSummaryNormaliser from '../replica-summary/normaliser'; +import scheduledJobSummaryNormaliser from '../scheduled-job-summary/normaliser'; import model from '.'; @@ -19,6 +20,10 @@ export const normaliser = (props) => { ? component.replicaList.map(replicaSummaryNormaliser) : null; + component.scheduledJobList = component.scheduledJobList + ? component.scheduledJobList.map(scheduledJobSummaryNormaliser) + : null; + return Object.freeze(component); }; diff --git a/src/models/job-summary/normaliser.js b/src/models/job-summary/normaliser.js index a5c916362..d6313134f 100644 --- a/src/models/job-summary/normaliser.js +++ b/src/models/job-summary/normaliser.js @@ -3,7 +3,7 @@ import pick from 'lodash/pick'; import model from '.'; /** - * Create a Job Summary object + * Create a Pipeline Job Summary object */ export const normaliser = (props) => { const jobSummary = pick(props, Object.keys(model)); diff --git a/src/models/job/test-data.js b/src/models/job/test-data.js index f638dd026..1cb3c8f71 100644 --- a/src/models/job/test-data.js +++ b/src/models/job/test-data.js @@ -14,10 +14,12 @@ const normalComponents = Object.freeze([ { name: 'component-a', image: 'an-image', + type: 'component', }, { name: 'component-b', image: 'another-image', + type: 'component', }, ]); diff --git a/src/models/replica-summary/index.js b/src/models/replica-summary/index.js index 9642d48c9..6799a31a9 100644 --- a/src/models/replica-summary/index.js +++ b/src/models/replica-summary/index.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; export default Object.freeze({ name: PropTypes.string.isRequired, + created: PropTypes.instanceOf(Date).isRequired, status: PropTypes.oneOf([ 'Pending', 'Failing', @@ -9,5 +10,6 @@ export default Object.freeze({ 'Terminated', 'Starting', ]).isRequired, + restartCount: PropTypes.number, statusMessage: PropTypes.string, }); diff --git a/src/models/replica-summary/normaliser.js b/src/models/replica-summary/normaliser.js index 7ace1737e..bc245b6b1 100644 --- a/src/models/replica-summary/normaliser.js +++ b/src/models/replica-summary/normaliser.js @@ -6,9 +6,15 @@ import model from '.'; * Create a Replica object */ export const normaliser = (props) => { + if (props == null) { + return null; + } const replica = pick(props, Object.keys(model)); replica.status = props.replicaStatus.status; + replica.created = replica.created ? new Date(replica.created) : null; + let restartCount = parseInt(replica.restartCount); + replica.restartCount = restartCount === NaN ? 0 : restartCount; return Object.freeze(replica); }; diff --git a/src/models/replica-summary/test-data.js b/src/models/replica-summary/test-data.js index aeb294011..44d183528 100644 --- a/src/models/replica-summary/test-data.js +++ b/src/models/replica-summary/test-data.js @@ -3,23 +3,27 @@ export const testData = [ __testDescription: 'Running', name: 'a-replica', replicaStatus: { status: 'Running' }, + created: '2018-11-19T14:31:23Z', }, { __testDescription: 'Starting', name: 'b-replica', replicaStatus: { status: 'Pending' }, + created: '2018-11-19T14:31:23Z', }, { __testDescription: 'Failing', name: 'c-replica', replicaStatus: { status: 'Failing' }, statusMessage: 'Some error message', + created: '2018-11-19T14:31:23Z', }, { __testDescription: 'Wrong status', __testIsInvalidSample: true, name: 'd-replica', replicaStatus: { status: 'Waiting' }, + created: '2018-11-19T14:31:23Z', }, ]; diff --git a/src/models/scheduled-job-summary/index.js b/src/models/scheduled-job-summary/index.js new file mode 100644 index 000000000..d5f313e62 --- /dev/null +++ b/src/models/scheduled-job-summary/index.js @@ -0,0 +1,12 @@ +import PropTypes from 'prop-types'; +import ProgressStatusModel from '../progress-status'; +import ReplicaSummaryModel from '../replica-summary'; + +export default Object.freeze({ + created: PropTypes.instanceOf(Date), + ended: PropTypes.instanceOf(Date), + name: PropTypes.string.isRequired, + started: PropTypes.instanceOf(Date), + status: ProgressStatusModel.isRequired, + replicaList: PropTypes.arrayOf(PropTypes.exact(ReplicaSummaryModel)), +}); diff --git a/src/models/scheduled-job-summary/normaliser.js b/src/models/scheduled-job-summary/normaliser.js new file mode 100644 index 000000000..d6313134f --- /dev/null +++ b/src/models/scheduled-job-summary/normaliser.js @@ -0,0 +1,18 @@ +import pick from 'lodash/pick'; + +import model from '.'; + +/** + * Create a Pipeline Job Summary object + */ +export const normaliser = (props) => { + const jobSummary = pick(props, Object.keys(model)); + + jobSummary.started = jobSummary.started ? new Date(jobSummary.started) : null; + jobSummary.ended = jobSummary.ended ? new Date(jobSummary.ended) : null; + jobSummary.created = jobSummary.created ? new Date(jobSummary.created) : null; + + return Object.freeze(jobSummary); +}; + +export default normaliser; diff --git a/src/models/scheduled-job-summary/test-data.js b/src/models/scheduled-job-summary/test-data.js new file mode 100644 index 000000000..9acd3e536 --- /dev/null +++ b/src/models/scheduled-job-summary/test-data.js @@ -0,0 +1,46 @@ +const normalReplicas = Object.freeze([ + { + __testDescription: 'Running', + name: 'a-replica', + replicaStatus: { status: 'Running' }, + }, + { + __testDescription: 'Starting', + name: 'b-replica', + replicaStatus: { status: 'Pending' }, + }, + { + __testDescription: 'Failing', + name: 'c-replica', + replicaStatus: { status: 'Failing' }, + statusMessage: 'Some error message', + }, + { + __testDescription: 'Wrong status', + __testIsInvalidSample: true, + name: 'd-replica', + replicaStatus: { status: 'Waiting' }, + }, +]); + +export const testData = [ + { + __testDescription: 'Valid Running', + name: 'A Job', + created: '2018-11-19T14:31:23Z', + ended: '2018-11-19T14:32:23Z', + started: '2018-11-19T14:31:23Z', + status: 'Running', + replicaList: normalReplicas, + }, + { + __testDescription: 'Valid Running not ended', + name: 'A Job', + created: '2018-11-19T14:31:23Z', + started: '2018-11-19T14:31:23Z', + status: 'Running', + replicaList: normalReplicas, + }, +]; + +export default testData; diff --git a/src/routes.js b/src/routes.js index 8ff50d667..8a700f142 100644 --- a/src/routes.js +++ b/src/routes.js @@ -13,10 +13,12 @@ export const routes = { appPrivateImageHub: '/applications/:appName/config/imagehubs/:imageHubName', appBuildSecret: '/applications/:appName/config/buildsecrets/:secretName', appComponent: '/applications/:appName/deployments/:deploymentName/component/:componentName', + appJobComponent: '/applications/:appName/deployments/:deploymentName/jobcomponent/:jobComponentName', appDeployment: '/applications/:appName/deployments/:deploymentName', appDeployments: '/applications/:appName/deployments', appEnvironment: '/applications/:appName/envs/:envName', appActiveComponent: '/applications/:appName/envs/:envName/component/:componentName', + appActiveJobComponent: '/applications/:appName/envs/:envName/jobcomponent/:jobComponentName', appEnvComponent: '/applications/:appName/envs/:envName/component/:componentName', appEnvironments: '/applications/:appName/envs', appJob: '/applications/:appName/jobs/view/:jobName', @@ -25,6 +27,7 @@ export const routes = { appJobStep: '/applications/:appName/jobs/view/:jobName/steps/:stepName', appPod: '/applications/:appName/envs/:envName/component/:componentName/pod/:podName', appReplica: '/applications/:appName/envs/:envName/component/:componentName/replica/:replicaName', + appScheduledJob: '/applications/:appName/envs/:envName/jobcomponent/:jobComponentName/scheduledjob/:scheduledJobName', appSecret: '/applications/:appName/envs/:envName/component/:componentName/secret/:secretName', appCreate: '/applications/new', diff --git a/src/state/environment/index.js b/src/state/environment/index.js index efd852536..a36fc5844 100644 --- a/src/state/environment/index.js +++ b/src/state/environment/index.js @@ -119,9 +119,7 @@ export const getActiveDeploymentName = (state) => { return env.activeDeployment.name; }; -export const getSecret = (state, componentName, secretName) => { - const env = getEnvironment(state); - +export const getComponentSecret = (env, secretName, componentName) => { if (!env || !env.activeDeployment) { return null; } @@ -130,3 +128,7 @@ export const getSecret = (state, componentName, secretName) => { (secret) => secret.name === secretName && secret.component === componentName ); }; + +export const getSecret = (state, componentName, secretName) => { + return getComponentSecret(getEnvironment(state), secretName, componentName); +}; diff --git a/src/utils/routing.js b/src/utils/routing.js index c26fbfb14..9a08b3d73 100644 --- a/src/utils/routing.js +++ b/src/utils/routing.js @@ -18,7 +18,7 @@ import routes from '../routes'; * @todo Support object (key => function) as paramsToMap (like mapStateToProps) * * @param {string[]} paramsToMap List of URL parameters to inject as props - * @param {React.Component} Component Component to receive props + * @param {function(*)} Component Component to receive props */ export function mapRouteParamsToProps(paramsToMap, Component) { return withRouter((props) => { @@ -80,6 +80,13 @@ export const getActiveComponentUrl = (appName, envName, componentName) => componentName, }); +export const getActiveJobComponentUrl = (appName, envName, jobComponentName) => + routeWithParams(routes.appActiveJobComponent, { + appName, + envName, + jobComponentName, + }); + export const getReplicaUrl = (appName, envName, componentName, replicaName) => routeWithParams(routes.appReplica, { appName, @@ -88,6 +95,19 @@ export const getReplicaUrl = (appName, envName, componentName, replicaName) => replicaName, }); +export const getScheduledJobUrl = ( + appName, + envName, + jobComponentName, + scheduledJobName +) => + routeWithParams(routes.appScheduledJob, { + appName, + envName, + jobComponentName, + scheduledJobName, + }); + export const getSecretUrl = (appName, envName, componentName, secretName) => routeWithParams(routes.appSecret, { appName, diff --git a/src/utils/string.js b/src/utils/string.js index 144de4b0d..444ef14ae 100644 --- a/src/utils/string.js +++ b/src/utils/string.js @@ -69,3 +69,5 @@ export const smallDeploymentName = (() => { export const smallJobName = (jobName) => jobName.slice(-5); export const smallReplicaName = (replicaName) => replicaName.slice(-5); + +export const smallScheduledJobName = (scheduledJob) => scheduledJob.slice(-8);