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 && }
+
+
+
+ >
+ );
+ }
+}
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
+
+ .
+
+
+
+
+
+
Resize to
+
+
+
}
+ layout={({ input }) => <>{input}>}
+ touched={true}
+ onChange={() => {}}
+ />
+ instances
+
+
+
+
+ {!!errorMessage && (
+
+
+
+ )}
+
+
+
+ );
+}
+
+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
+
+ .
+
+
+
+
+
+
+
+
Resize to
+
+ }
+ layout={({ input }) => <>{input}>}
+ touched={true}
+ />
+
+
+
+ }
+ layout={({ input }) => <>{input}>}
+ touched={true}
+ />
+
+
+
+ }
+ layout={({ input }) => <>{input}>}
+ touched={true}
+ />
+
+
+
+ {!!errorMessage && (
+
+
+
+ )}
+
+
+
+ );
+}
+
+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}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+ }}
+ />
+ >
+ );
+}
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 (
+ * <>
+ *
+ *