Skip to content

Commit

Permalink
feat(core/managed): Application pause/resume functionality (#8342)
Browse files Browse the repository at this point in the history
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
vigneshm and mergify[bot] committed Jun 29, 2020
1 parent 638363d commit 996145a
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 1 deletion.
8 changes: 8 additions & 0 deletions app/scripts/modules/core/src/managed/Environments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ColumnHeader } from './ColumnHeader';
import { ArtifactsList } from './ArtifactsList';
import { EnvironmentsList } from './EnvironmentsList';
import { ArtifactDetail } from './ArtifactDetail';
import { EnvironmentsHeader } from './EnvironmentsHeader';

import styles from './Environments.module.css';

Expand Down Expand Up @@ -157,6 +158,13 @@ export function Environments({ app }: IEnvironmentsProps) {
show && (
<animated.div key={key} className={styles.environmentsPane} style={props}>
<ColumnHeader text="Environments" icon="environment" />
<EnvironmentsHeader
app={app}
resourceInfo={{
managed: resources.filter(r => !r.isPaused).length,
total: resources.length,
}}
/>
<EnvironmentsList application={app} {...{ environments, artifacts, resourcesById }} />
</animated.div>
),
Expand Down
29 changes: 29 additions & 0 deletions app/scripts/modules/core/src/managed/EnvironmentsHeader.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.EnvironmentsHeader {
background-color: var(--color-white);
border-radius: 3px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
margin: 0 3px 16px 3px;

// dropdown-toggle class is from the bootstrap library and needed here to increase the specificity of our rule
// to avoid bootstrap changing the background and text colors to default value.
.dropdown-toggle.dropdown-toggle-btn,
.dropdown-toggle.dropdown-toggle-btn:hover,
.open > .dropdown-toggle.dropdown-toggle-btn,
.open > .dropdown-toggle.dropdown-toggle-btn:hover,
.open > .dropdown-toggle.dropdown-toggle-btn:focus {
background-color: rgb(90, 95, 180);
color: var(--color-white);
}

.dropdown-menu > li {
cursor: pointer;

&:hover {
background-color: var(--color-alabaster);
}

a {
padding: 8px 20px;
}
}
}
64 changes: 64 additions & 0 deletions app/scripts/modules/core/src/managed/EnvironmentsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { Dropdown } from 'react-bootstrap';

import { Application } from '../application';
import { Illustration, IllustrationName } from '../presentation';
import { showToggleManagedResourceModal } from './ToggleManagedResourceForApplication';

import './EnvironmentsHeader.less';

interface IEnvironmentsHeaderProps {
app: Application;
resourceInfo: { managed: number; total: number };
}

export const ToggleManagedResourceAction = ({ app }: { app: Application }) => (
<li>
<a
onClick={() => {
showToggleManagedResourceModal({ application: app });
}}
>
{app.isManagementPaused ? 'Resume management' : 'Disable management'}
</a>
</li>
);

const environmentHeaderInfo: {
[key: string]: { description: string | null; icon: IllustrationName; title: (info: string) => string };
} = {
running: {
description: null,
icon: 'runManagement',
title: (info: string) => `Spinnaker is continuously managing ${info} resources.`,
},
paused: {
description: 'Any actions made will take effect once management is resumed.',
icon: 'disableManagement',
title: () => 'Management is disabled for this application.',
},
};

export const EnvironmentsHeader = ({ app, resourceInfo: { managed, total } }: IEnvironmentsHeaderProps) => {
const { description, icon, title } = environmentHeaderInfo[app.isManagementPaused ? 'paused' : 'running'];

return (
<div className="EnvironmentsHeader">
<div className="flex-container-h sp-padding-l">
<div style={{ minWidth: 145 }}>
<Illustration name={icon} />
</div>
<div className="flex-container-v sp-padding-xl-top sp-margin-m-left sp-margin-m-top">
<div className="heading-3 bold">{title(managed === total ? `${total}` : `${managed}/${total}`)}</div>
{description && <div className="sp-margin-s-top">{description}</div>}
<Dropdown id="application-actions" className="sp-margin-l-top">
<Dropdown.Toggle className="dropdown-toggle-btn">Application Actions</Dropdown.Toggle>
<Dropdown.Menu className="dropdown-menu">
<ToggleManagedResourceAction app={app} />
</Dropdown.Menu>
</Dropdown>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { memo, useState } from 'react';
import ReactGA from 'react-ga';

import { Application } from '../application';
import { Button } from './Button';
import {
Illustration,
IllustrationName,
IModalComponentProps,
ModalBody,
ModalFooter,
ModalHeader,
showModal,
ValidationMessage,
} from '../presentation';
import { ManagedWriter } from './ManagedWriter';

const logClick = (label: string, application: string) =>
ReactGA.event({
category: 'Environments - toggle application management modal',
action: `${label} clicked`,
label: application,
});

export interface IToggleManagedResourceForApplicationModalProps extends IModalComponentProps {
application: Application;
}

interface Error {
data: { error: string; message: string };
}

export const MDRunningStateDescription = () => (
<>
<p>
<span className="bold">
Careful! You’re about to stop Spinnaker from managing all resources in your application.
</span>
This feature should only be used if management is not working properly and manual intervention is required.{' '}
<a href="https://www.spinnaker.io/guides/user/managed-delivery" target="_blank">
Check our documentation for more information
</a>
.
</p>
<p>
Need to rollback?{' '}
<a href="https://www.spinnaker.io/guides/user/managed-delivery/pinning/" target="_blank">
Try pinning a version instead
</a>
.
</p>
</>
);

export const MDPausedStateDescription = () => {
return (
<p>
You’re about to resume management for this application. The latest good version approved for deployment will be
deployed to each environment, and any configuration changes made while disabled will take effect.
</p>
);
};

const modalViewInfo = {
paused: {
actionToBeTaken: 'Resume',
description: <MDPausedStateDescription />,
icon: 'runManagement' as IllustrationName,
},
running: {
actionToBeTaken: 'Disable',
description: <MDRunningStateDescription />,
icon: 'disableManagement' as IllustrationName,
},
};

export const showToggleManagedResourceModal = (props: IToggleManagedResourceForApplicationModalProps) =>
showModal(ToggleManagedResourceForApplicationModal, props, { maxWidth: 800 });

export const ToggleManagedResourceForApplicationModal = memo(
({ application, closeModal, dismissModal }: IToggleManagedResourceForApplicationModalProps) => {
const [isSubmitting, setSubmitting] = useState<boolean>(false);
const [actionError, setActionError] = useState<{ error: string; message: string } | null>(null);
const dataSource = application.getDataSource('environments');

const { actionToBeTaken, description, icon } = modalViewInfo[application.isManagementPaused ? 'paused' : 'running'];

const handleActionInitiation = () => {
setActionError(null);
setSubmitting(true);
logClick(`${actionToBeTaken} Management`, application.name);
const call = application.isManagementPaused
? ManagedWriter.resumeApplicationManagement(application.name)
: ManagedWriter.pauseApplicationManagement(application.name);
call
.then(() => dataSource.refresh(true).catch(() => null))
.then(() => {
closeModal();
})
.catch((error: Error) => {
setActionError(error.data);
setSubmitting(false);
});
};
return (
<>
<ModalHeader>{actionToBeTaken} management</ModalHeader>
<ModalBody>
<div className="flex-container-h sp-padding-xl-yaxis">
<div style={{ minWidth: 145 }}>
<Illustration name={icon} />
</div>
<div className="sp-padding-xl">
<div>{description}</div>
{actionError && (
<div className="sp-margin-xl-top">
<ValidationMessage
type="error"
message={
<span className="flex-container-v">
<span className="text-bold">Something went wrong:</span>
{actionError.error && <span className="text-semibold">{actionError.error}</span>}
{actionError.message && <span>{actionError.message}</span>}
</span>
}
/>
</div>
)}
</div>
</div>
</ModalBody>
<ModalFooter
primaryActions={
<div className="flex-container-h sp-group-margin-s-xaxis">
<Button onClick={dismissModal}>Cancel</Button>
<Button appearance="primary" disabled={isSubmitting} onClick={handleActionInitiation}>
{actionToBeTaken}
</Button>
</div>
}
/>
</>
);
},
);
3 changes: 2 additions & 1 deletion app/scripts/modules/core/src/managed/managed.dataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ module(MANAGED_RESOURCES_DATA_SOURCE, []).run([
};

const addEnvironments = (
_application: Application,
application: Application,
data: IManagedApplicationSummary<'resources' | 'artifacts' | 'environments'>,
) => {
application.isManagementPaused = data.applicationPaused;
return $q.when(data);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { ReactComponent as announcement } from './vectors/announcement.svg';
import { ReactComponent as disableManagement } from './vectors/disableManagement.svg';
import { ReactComponent as markArtifactVersionAsBad } from './vectors/markArtifactVersionAsBad.svg';
import { ReactComponent as pinArtifactVersion } from './vectors/pinArtifactVersion.svg';
import { ReactComponent as runManagement } from './vectors/runManagement.svg';
import { ReactComponent as unpinArtifactVersion } from './vectors/unpinArtifactVersion.svg';

export const illustrationsByName = {
announcement,
disableManagement,
markArtifactVersionAsBad,
pinArtifactVersion,
runManagement,
unpinArtifactVersion,
} as const;

0 comments on commit 996145a

Please sign in to comment.