diff --git a/app/scripts/modules/titus/src/domain/ITitusServerGroup.ts b/app/scripts/modules/titus/src/domain/ITitusServerGroup.ts index 7823e44680a..bab0147e2cb 100644 --- a/app/scripts/modules/titus/src/domain/ITitusServerGroup.ts +++ b/app/scripts/modules/titus/src/domain/ITitusServerGroup.ts @@ -10,6 +10,7 @@ export interface ITitusServerGroup extends IServerGroup { image?: ITitusImage; scalingPolicies?: ITitusPolicy[]; targetGroups?: string[]; + capacityGroup?: string; } export interface ITitusImage { diff --git a/app/scripts/modules/titus/src/serverGroup/details/TitusCapacityDetailsSection.tsx b/app/scripts/modules/titus/src/serverGroup/details/TitusCapacityDetailsSection.tsx new file mode 100644 index 00000000000..6c7f2e21513 --- /dev/null +++ b/app/scripts/modules/titus/src/serverGroup/details/TitusCapacityDetailsSection.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; + +import { ReactModal, Application, Overridable } from '@spinnaker/core'; +import { ITitusServerGroup } from 'titus/domain'; +import { ITitusResizeServerGroupModalProps, TitusResizeServerGroupModal } from './resize/TitusResizeServerGroupModal'; + +interface ICapacityDetailsSectionProps { + app: Application; + serverGroup: ITitusServerGroup; +} + +export const TitusSimpleMinMaxDesired = ({ serverGroup }: ICapacityDetailsSectionProps) => ( + <> +
Min/Max
+
{serverGroup.capacity.desired}
+
Current
+
{serverGroup.instances.length}
+ +); + +export const TitusAdvancedMinMaxDesired = ({ serverGroup }: ICapacityDetailsSectionProps) => ( + <> +
Min
+
{serverGroup.capacity.min}
+
Desired
+
{serverGroup.capacity.desired}
+
Max
+
{serverGroup.capacity.max}
+
Current
+
{serverGroup.instances.length}
+ +); + +export const TitusCapacityGroup = ({ serverGroup }: ICapacityDetailsSectionProps) => ( + <> +
Cap. Group
+
{serverGroup.capacityGroup}
+ +); + +@Overridable('titus.serverGroup.CapacityDetailsSection') +export class TitusCapacityDetailsSection extends React.Component { + public render(): JSX.Element { + const { serverGroup, app: application } = this.props; + const isSimpleMode = serverGroup.capacity.min === serverGroup.capacity.max; + const resizeServerGroup = () => + ReactModal.show(TitusResizeServerGroupModal, { serverGroup, application }); + + return ( + <> +
+ {isSimpleMode ? : } + {serverGroup.capacityGroup && } +
+ +
+ + Resize Server Group + +
+ + ); + } +} diff --git a/app/scripts/modules/titus/src/serverGroup/details/capacityDetailsSection.component.ts b/app/scripts/modules/titus/src/serverGroup/details/capacityDetailsSection.component.ts new file mode 100644 index 00000000000..240a6d3cfab --- /dev/null +++ b/app/scripts/modules/titus/src/serverGroup/details/capacityDetailsSection.component.ts @@ -0,0 +1,10 @@ +import { module } from 'angular'; +import { react2angular } from 'react2angular'; + +import { TitusCapacityDetailsSection } from './TitusCapacityDetailsSection'; + +export const TITUS_SERVERGROUP_DETAILS_CAPACITYDETAILSSECTION = 'titus.servergroup.details.capacitydetailssection'; +module(TITUS_SERVERGROUP_DETAILS_CAPACITYDETAILSSECTION, []).component( + 'titusCapacityDetailsSection', + react2angular(TitusCapacityDetailsSection, ['serverGroup', 'app']), +); diff --git a/app/scripts/modules/titus/src/serverGroup/details/resize/TitusResizeServerGroupModal.tsx b/app/scripts/modules/titus/src/serverGroup/details/resize/TitusResizeServerGroupModal.tsx new file mode 100644 index 00000000000..401304b1f89 --- /dev/null +++ b/app/scripts/modules/titus/src/serverGroup/details/resize/TitusResizeServerGroupModal.tsx @@ -0,0 +1,305 @@ +import * as React from 'react'; +import { Modal } from 'react-bootstrap'; +import { Form, Formik, FormikContext } from 'formik'; +import { ITitusServerGroup } from 'titus/domain'; +import { + Application, + FormikFormField, + ICapacity, + IModalComponentProps, + MinMaxDesiredChanges, + ModalClose, + NgReact, + NumberInput, + PlatformHealthOverride, + ReactInjector, + UserVerification, + ValidationMessage, +} from '@spinnaker/core'; +import { useTaskMonitor } from 'titus/serverGroup/details/resize/useTaskMonitor'; + +const { useState, useEffect, useMemo } = React; + +export interface ITitusResizeServerGroupModalProps extends IModalComponentProps { + application: Application; + serverGroup: ITitusServerGroup; +} + +interface ITitusResizeServerGroupCommand { + capacity: ICapacity; + serverGroupName: string; + instances: number; + interestingHealthProviderNames: string[]; + region: string; +} + +function surfacedErrorMessage(formik: FormikContext) { + const capacityErrors = formik.errors.capacity || ({} as any); + const { min, max, desired } = capacityErrors; + return [min, max, desired].find(x => !!x); +} + +function SimpleMode({ formik, serverGroup, toggleMode }: IAdvancedModeProps) { + useEffect(() => { + formik.setFieldValue('capacity.min', formik.values.capacity.desired); + formik.setFieldValue('capacity.max', formik.values.capacity.desired); + }, [formik.values.capacity.desired]); + + const errorMessage = surfacedErrorMessage(formik); + + return ( +
+

Sets min, max, and desired instance counts to the same value.

+ +

+ To allow autoscaling, use the{' '} + + Advanced Mode + + . +

+ +
+
Current size
+
+
+ +
instances
+
+
+
+ +
+
Resize to
+
+
+ } + layout={({ input }) => <>{input}} + touched={true} + onChange={() => {}} + /> +
instances
+
+
+
+ + {!!errorMessage && ( +
+ +
+ )} + +
+
Changes
+
+ +
+
+
+ ); +} + +interface IAdvancedModeProps { + formik: FormikContext; + serverGroup: ITitusServerGroup; + toggleMode: () => void; +} + +function AdvancedMode({ formik, serverGroup, toggleMode }: IAdvancedModeProps) { + const { min, max } = formik.values.capacity || ({} as any); + + const DisabledNumberField = ({ value }: { value: string | number }) => ( +
+ +
+ ); + const errorMessage = surfacedErrorMessage(formik); + + return ( +
+

Sets up auto-scaling for this server group.

+

+ To disable auto-scaling, use the{' '} + + Simple Mode + + . +

+ +
+
Min
+
Max
+
Desired
+
+ +
+
Current
+ + + +
+ +
+
Resize to
+
+ } + layout={({ input }) => <>{input}} + touched={true} + /> +
+ +
+ } + layout={({ input }) => <>{input}} + touched={true} + /> +
+ +
+ } + layout={({ input }) => <>{input}} + touched={true} + /> +
+
+ + {!!errorMessage && ( +
+ +
+ )} + +
+
Changes
+
+ +
+
+
+ ); +} + +function validateResizeCommand(values: ITitusResizeServerGroupCommand) { + const { min, max, desired } = values.capacity; + const capacityErrors = {} as any; + + // try to only show one error message at a time + if (min > max) { + capacityErrors.min = capacityErrors.max = 'Min cannot be larger than Max'; + } else if (desired < min) { + capacityErrors.desired = capacityErrors.min = 'Desired cannot be smaller than Min'; + } else if (desired > max) { + capacityErrors.desired = capacityErrors.max = 'Desired cannot be larger than Max'; + } + + if (Object.keys(capacityErrors).length) { + return { capacity: capacityErrors }; + } + + return {}; +} + +export function TitusResizeServerGroupModal(props: ITitusResizeServerGroupModalProps) { + const { TaskMonitorWrapper } = NgReact; + const { serverGroup, application, dismissModal } = props; + + const initialAdvancedMode = useMemo(() => { + const { min, max, desired } = serverGroup.capacity; + return desired !== max || desired !== min; + }, []); + const [advancedMode, setAdvancedMode] = useState(initialAdvancedMode); + + const platformHealthOnlyShowOverride = + application.attributes && application.attributes.platformHealthOnlyShowOverride; + const [verified, setVerified] = useState(); + + const taskMonitor = useTaskMonitor( + { + application, + title: `Resizing ${serverGroup.name}`, + onTaskComplete: () => application.getDataSource('serverGroups').refresh(true), + }, + dismissModal, + ); + const submit = (command: ITitusResizeServerGroupCommand) => + taskMonitor.submit(() => ReactInjector.serverGroupWriter.resizeServerGroup(serverGroup, application, command)); + + const initialValues = { capacity: serverGroup.capacity } as ITitusResizeServerGroupCommand; + + return ( + <> + + + + initialValues={initialValues} + validate={validateResizeCommand} + onSubmit={submit} + render={formik => { + return ( + <> + +

Resize {serverGroup.name}

+
+ + + + +
+ {advancedMode ? ( + setAdvancedMode(false)} /> + ) : ( + setAdvancedMode(true)} /> + )} + + {platformHealthOnlyShowOverride && ( + + formik.setFieldValue('interestingHealthProviderNames', names ? names : undefined) + } + /> + )} + +
+ + + + + + + + + + ); + }} + /> + + ); +} diff --git a/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.controller.js b/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.controller.js deleted file mode 100644 index d585eb6c945..00000000000 --- a/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.controller.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -const angular = require('angular'); - -import { SERVER_GROUP_WRITER, TaskMonitor } from '@spinnaker/core'; - -module.exports = angular - .module('spinnaker.titus.serverGroup.details.resize.controller', [SERVER_GROUP_WRITER]) - .controller('titusResizeServerGroupCtrl', [ - '$scope', - '$uibModalInstance', - 'serverGroupWriter', - 'application', - 'serverGroup', - function($scope, $uibModalInstance, serverGroupWriter, application, serverGroup) { - $scope.serverGroup = serverGroup; - $scope.currentSize = { - min: serverGroup.capacity.min, - max: serverGroup.capacity.max, - desired: serverGroup.capacity.desired, - newSize: null, - }; - - $scope.verification = {}; - - $scope.command = angular.copy($scope.currentSize); - $scope.command.advancedMode = serverGroup.capacity.min !== serverGroup.capacity.max; - - if (application && application.attributes) { - $scope.command.platformHealthOnlyShowOverride = application.attributes.platformHealthOnlyShowOverride; - } - - this.isValid = function() { - var command = $scope.command; - if (!$scope.verification.verified) { - return false; - } - return command.advancedMode - ? command.min <= command.max && command.desired >= command.min && command.desired <= command.max - : command.newSize !== null; - }; - - $scope.taskMonitor = new TaskMonitor({ - application: application, - title: 'Resizing ' + serverGroup.name, - modalInstance: $uibModalInstance, - }); - - this.resize = function() { - if (!this.isValid()) { - return; - } - var capacity = { min: $scope.command.min, max: $scope.command.max, desired: $scope.command.desired }; - if (!$scope.command.advancedMode) { - capacity = { min: $scope.command.newSize, max: $scope.command.newSize, desired: $scope.command.newSize }; - } - - var submitMethod = function() { - return serverGroupWriter.resizeServerGroup(serverGroup, application, { - capacity: capacity, - serverGroupName: serverGroup.name, - instances: capacity.desired, - interestingHealthProviderNames: $scope.command.interestingHealthProviderNames, - region: serverGroup.region, - }); - }; - - $scope.taskMonitor.submit(submitMethod); - }; - - this.cancel = function() { - $uibModalInstance.dismiss(); - }; - }, - ]); diff --git a/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.controller.spec.js b/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.controller.spec.js deleted file mode 100644 index 1a8adf78a9a..00000000000 --- a/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.controller.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -describe('Controller: titusResizeServerGroupCtrl', function() { - //NOTE: This is only testing the controllers dependencies. Please add more tests. - - var controller; - var scope; - - beforeEach(window.module(require('./resizeServerGroup.controller').name)); - - beforeEach( - window.inject(function($rootScope, $controller) { - scope = $rootScope.$new(); - controller = $controller('titusResizeServerGroupCtrl', { - $scope: scope, - $uibModalInstance: { result: { then: angular.noop } }, - application: {}, - serverGroup: { - capacity: { - min: 0, - max: 0, - desired: 0, - }, - }, - }); - }), - ); - - it('should instantiate the controller', function() { - expect(controller).toBeDefined(); - }); -}); diff --git a/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.html b/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.html deleted file mode 100644 index 34c77bae3da..00000000000 --- a/app/scripts/modules/titus/src/serverGroup/details/resize/resizeServerGroup.html +++ /dev/null @@ -1,120 +0,0 @@ -
- -
- - - - -
-
- - -
-
- - -
-
diff --git a/app/scripts/modules/titus/src/serverGroup/details/resize/useTaskMonitor.ts b/app/scripts/modules/titus/src/serverGroup/details/resize/useTaskMonitor.ts new file mode 100644 index 00000000000..e196f4e01d3 --- /dev/null +++ b/app/scripts/modules/titus/src/serverGroup/details/resize/useTaskMonitor.ts @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; +import { ITaskMonitorConfig, TaskMonitor } from '@spinnaker/core'; + +/** + * React hook that returns a TaskMonitor + * + * @param config a ITaskMonitorConfig + * @param dismissModal a function that closes the modal enclosing the task monitor + * + * Example: + * + * function MyComponent(props) { + * const { application, serverGroup } = props; + * const title = `Resize ${serverGroup.name}`; + * const taskMonitor = useTaskMonitor({ application, title }); + * + * return ( + * <> + * + *
taskMonitor.submit(() => API.runSomeTask())}> + * + * ) + * } + * + */ +export const useTaskMonitor = (config: ITaskMonitorConfig, dismissModal: () => void) => { + const modalInstance = TaskMonitor.modalInstanceEmulation(() => dismissModal()); + return useMemo(() => new TaskMonitor({ modalInstance, ...config }), [config.application, config.title]); +}; diff --git a/app/scripts/modules/titus/src/serverGroup/details/serverGroupDetails.html b/app/scripts/modules/titus/src/serverGroup/details/serverGroupDetails.html index 7c6d865272d..5fb880123c6 100644 --- a/app/scripts/modules/titus/src/serverGroup/details/serverGroupDetails.html +++ b/app/scripts/modules/titus/src/serverGroup/details/serverGroupDetails.html @@ -99,29 +99,10 @@

-
-
Desired
-
{{serverGroup.capacity.desired}}
-
Current
-
{{serverGroup.instances.length}}
-
Cap. Group
-
{{serverGroup.capacityGroup}}
-
-
-
Min
-
{{serverGroup.capacity.min}}
-
Desired
-
{{serverGroup.capacity.desired}}
-
Max
-
{{serverGroup.capacity.max}}
-
Current
-
{{serverGroup.instances.length}}
-
Cap. Group
-
{{serverGroup.capacityGroup}}
-
- +
diff --git a/app/scripts/modules/titus/src/serverGroup/details/serverGroupDetails.titus.controller.js b/app/scripts/modules/titus/src/serverGroup/details/serverGroupDetails.titus.controller.js index 43d558f4cf9..f8d33d92ff5 100644 --- a/app/scripts/modules/titus/src/serverGroup/details/serverGroupDetails.titus.controller.js +++ b/app/scripts/modules/titus/src/serverGroup/details/serverGroupDetails.titus.controller.js @@ -29,7 +29,6 @@ module.exports = angular CONFIRMATION_MODAL_SERVICE, DISRUPTION_BUDGET_DETAILS_SECTION, SERVER_GROUP_WRITER, - require('./resize/resizeServerGroup.controller').name, require('./rollback/rollbackServerGroup.controller').name, SCALING_POLICY_MODULE, TITUS_SECURITY_GROUPS_DETAILS, @@ -341,21 +340,6 @@ module.exports = angular confirmationModalService.confirm(confirmationModalParams); }; - this.resizeServerGroup = function resizeServerGroup() { - $uibModal.open({ - templateUrl: require('./resize/resizeServerGroup.html'), - controller: 'titusResizeServerGroupCtrl as ctrl', - resolve: { - serverGroup: function() { - return $scope.serverGroup; - }, - application: function() { - return application; - }, - }, - }); - }; - this.cloneServerGroup = function cloneServerGroup() { TitusReactInjector.titusServerGroupCommandBuilder .buildServerGroupCommandFromExisting(application, $scope.serverGroup) diff --git a/app/scripts/modules/titus/src/titus.module.ts b/app/scripts/modules/titus/src/titus.module.ts index 7c1a508fc87..1126e1ef8e8 100644 --- a/app/scripts/modules/titus/src/titus.module.ts +++ b/app/scripts/modules/titus/src/titus.module.ts @@ -5,6 +5,7 @@ import { CloudProviderRegistry, DeploymentStrategyRegistry } from '@spinnaker/co import { AmazonLoadBalancersTag } from '@spinnaker/amazon'; import { TITUS_MIGRATION_CONFIG_COMPONENT } from './migration/titusMigrationConfig.component'; +import { TITUS_SERVERGROUP_DETAILS_CAPACITYDETAILSSECTION } from './serverGroup/details/capacityDetailsSection.component'; import './validation/ApplicationNameValidator'; import './help/titus.help'; import { TITUS_REACT_MODULE } from './reactShims/titus.react.module'; @@ -39,6 +40,7 @@ module(TITUS_MODULE, [ require('./pipeline/stages/disableCluster/titusDisableClusterStage').name, require('./pipeline/stages/shrinkCluster/titusShrinkClusterStage').name, require('./pipeline/stages/scaleDownCluster/titusScaleDownClusterStage').name, + TITUS_SERVERGROUP_DETAILS_CAPACITYDETAILSSECTION, TITUS_MIGRATION_CONFIG_COMPONENT, ]).config(() => { CloudProviderRegistry.registerProvider('titus', {