Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle manually scaled components #1077

Merged
merged 15 commits into from
Sep 12, 2024
4 changes: 2 additions & 2 deletions proxy/server.dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ server {
proxy_read_timeout 10;

location /api/ {
# proxy_pass https://server-radix-api-qa.dev.radix.equinor.com;
proxy_pass http://172.19.0.1:3002;
proxy_pass https://server-radix-api-qa.dev.radix.equinor.com;
# proxy_pass http://172.19.0.1:3002;
proxy_set_header Authorization "Bearer $http_x_forwarded_access_token";
proxy_set_header x-forwarded-access-token "";
}
Expand Down
122 changes: 0 additions & 122 deletions src/components/component/toolbar.tsx

This file was deleted.

5 changes: 3 additions & 2 deletions src/components/global-top-nav/styled-toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,16 @@ export function successToast<T>(

export function handlePromiseWithToast<TArgs extends Array<unknown>, TReturn>(
fn: (...args: TArgs) => TReturn,
successContent = 'Saved'
successContent = 'Saved',
errorContent = 'Error while saving'
) {
return async (...args: TArgs): Promise<Awaited<TReturn> | undefined> => {
try {
const ret = await fn(...args);
successToast(successContent);
return ret;
} catch (e) {
errorToast(`Error while saving. ${getFetchErrorMessage(e)}`);
errorToast(`${errorContent}. ${getFetchErrorMessage(e)}`);
return undefined;
}
};
Expand Down
26 changes: 19 additions & 7 deletions src/components/page-active-component/active-component-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,27 @@ import { getEnvsUrl } from '../../utils/routing';
import AsyncResource from '../async-resource/async-resource';
import { Breadcrumb } from '../breadcrumb';
import { ActiveComponentSecrets } from '../component/secrets/active-component-secrets';
import { Toolbar } from '../component/toolbar';
import { EnvironmentVariables } from '../environment-variables';

import { routeWithParams } from '../../utils/string';
import './style.css';
import { ActiveComponentToolbar } from './active-component-toolbar';
import { ComponentStatus } from './component-status';

export const ActiveComponentOverview: FunctionComponent<{
appName: string;
envName: string;
componentName: string;
}> = ({ appName, envName, componentName }) => {
const { data: application, refetch } = useGetApplicationQuery(
const { data: application } = useGetApplicationQuery(
{ appName },
{ skip: !appName, pollingInterval }
);
const { data: environment, ...envState } = useGetEnvironmentQuery(
const {
data: environment,
refetch,
...envState
} = useGetEnvironmentQuery(
{ appName, envName },
{ skip: !appName || !envName, pollingInterval }
);
Expand Down Expand Up @@ -68,14 +73,21 @@ export const ActiveComponentOverview: FunctionComponent<{
<AsyncResource asyncState={envState}>
{component && (
<>
<Toolbar
<ActiveComponentToolbar
component={component}
appName={appName}
envName={envName}
component={component}
startEnabled
stopEnabled
refetch={refetch}
/>

<ComponentStatus
appName={appName}
envName={envName}
componentName={componentName}
refetch={refetch}
component={component}
/>

<Overview
appAlias={appAlias}
dnsAliases={componentDNSAliases}
Expand Down
175 changes: 175 additions & 0 deletions src/components/page-active-component/active-component-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {
Button,
CircularProgress,
Dialog,
Slider,
Typography,
} from '@equinor/eds-core-react';
import { useState } from 'react';
import {
type Component,
useRestartComponentMutation,
useScaleComponentMutation,
useStopComponentMutation,
} from '../../store/radix-api';
import { handlePromiseWithToast } from '../global-top-nav/styled-toaster';
import './style.css';
import { useDurationInterval } from '../../effects/use-interval';

type Props = {
component: Component;
appName: string;
envName: string;
refetch: () => unknown;
};

export function ActiveComponentToolbar({
component,
appName,
envName,
refetch,
}: Props) {
const [scaleTrigger, scaleState] = useScaleComponentMutation();
const [stopTrigger, stopState] = useStopComponentMutation();
const [restartTrigger, restartState] = useRestartComponentMutation();
const startRefetch = useDurationInterval(2_000, 10_000, refetch);

const isStopped = component?.status === 'Stopped';
const isWorking =
restartState.isLoading ||
scaleState.isLoading ||
stopState.isLoading ||
component?.status === 'Reconciling' ||
component?.status === 'Restarting';

const onStop = handlePromiseWithToast(
async () => {
await stopTrigger({
appName,
envName,
componentName: component.name,
}).unwrap();
startRefetch();
},
'Stopping component',
'Failed to stop component'
);

const onRestart = handlePromiseWithToast(
async () => {
await restartTrigger({
appName,
envName,
componentName: component.name,
}).unwrap();
startRefetch();
},
'Restarting component',
'Failed to restart component'
);

const onScale = handlePromiseWithToast(
async (replicas: number) => {
await scaleTrigger({
appName,
envName,
componentName: component.name,
replicas: replicas.toFixed(),
}).unwrap();
startRefetch();
},
'Scaling component',
'Failed to scale component'
);
return (
<>
<div className="grid grid--gap-small">
<div className="grid grid--gap-small grid--auto-columns">
<ScaleButtonPopup
onScale={onScale}
disabled={scaleState.isLoading}
currentReplicas={
component.replicasOverride ?? component.replicaList.length
}
/>
<Button
disabled={isStopped || stopState.isLoading}
variant="outlined"
onClick={onStop}
>
Stop
</Button>

<Button
disabled={isStopped || restartState.isLoading}
variant="outlined"
onClick={onRestart}
>
Restart
</Button>
{isWorking && <CircularProgress size={32} />}
</div>
</div>
</>
);
}

type ScaleProps = {
disabled: boolean;
currentReplicas: number;
onScale: (replicas: number) => unknown;
};

function ScaleButtonPopup({ disabled, currentReplicas, onScale }: ScaleProps) {
const [replicas, setReplicas] = useState<number | null>(null);
const [visibleScrim, setVisibleScrim] = useState<boolean>(false);
const current = replicas ?? currentReplicas;

const onLocalScale = async () => {
await onScale(current);
setVisibleScrim(false);
setReplicas(null);
};

return (
<div>
<Button onClick={() => setVisibleScrim(true)}>Scale</Button>

<Dialog
title={'Scale Component'}
open={!!visibleScrim}
onClose={() => setVisibleScrim(false)}
isDismissable
style={{ width: '400px' }}
>
<Dialog.Header>
<Dialog.Title>Scale Component</Dialog.Title>
</Dialog.Header>
<Dialog.Content>
<Typography>
This will disable any automatic scaling until manual scaling is
reset.
</Typography>
<Slider
value={current}
min={0}
max={20}
onChange={(_, values) => setReplicas(values[0])}
aria-labelledby="simple-slider"
/>
</Dialog.Content>

<Dialog.Actions>
<div className={'scale-component-popup-actions-wrapper'}>
<Button disabled={disabled} onClick={onLocalScale}>
{current === 0 ? 'Stop component' : `Scale to ${current}`}
</Button>
<Button variant="outlined" onClick={() => setVisibleScrim(false)}>
Cancel
</Button>
</div>
</Dialog.Actions>
</Dialog>
</div>
);
}
Loading