Skip to content

Commit

Permalink
feat: Rollouts UI List View Refresh (#3118)
Browse files Browse the repository at this point in the history
* feat: Rollouts UI Refresh

Signed-off-by: Philip Clark <[email protected]>

* add test for labels and annotations

Signed-off-by: Philip Clark <[email protected]>

* simplify regex

Signed-off-by: Philip Clark <[email protected]>

* make filters use OR logic

Signed-off-by: Philip Clark <[email protected]>

* add keyboard listener

Signed-off-by: Philip Clark <[email protected]>

* set default display mode to grid

Signed-off-by: Philip Clark <[email protected]>

* set strategy column to left justify

Signed-off-by: Philip Clark <[email protected]>

* add tooltips to view buttons

Signed-off-by: Philip Clark <[email protected]>

* consider unknown status rollouts as needing attention

Signed-off-by: Philip Clark <[email protected]>

* remove duplicate escape key listener

Signed-off-by: Philip Clark <[email protected]>

* group status filters together

Signed-off-by: Philip Clark <[email protected]>

* improve filter logic

Signed-off-by: Philip Clark <[email protected]>

* remove debug logging

Signed-off-by: Philip Clark <[email protected]>

* dont show help on escape

Signed-off-by: Philip Clark <[email protected]>

* properly remove url search params when disabled

Signed-off-by: Philip Clark <[email protected]>

* rename to RolloutGridWidget

Signed-off-by: Philip Clark <[email protected]>

* prevent help menu from loading while searching

Signed-off-by: Philip Clark <[email protected]>

---------

Signed-off-by: Philip Clark <[email protected]>
  • Loading branch information
phclark authored Nov 30, 2023
1 parent 33e0612 commit 8cae284
Show file tree
Hide file tree
Showing 30 changed files with 1,374 additions and 293 deletions.
23 changes: 23 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,29 @@ To run a subset of e2e tests, you need to specify the suite with `-run`, and the
E2E_TEST_OPTIONS="-run 'TestCanarySuite' -testify.m 'TestCanaryScaleDownOnAbortNoTrafficRouting'" make test-e2e
```

## Running the UI

If you'd like to run the UI locally, you first need a running Rollouts controller. This can be a locally running controller with a k3d cluster, as described above, or a controller running in a remote Kubernetes cluster.

In order for the local React app to communicate with the controller and Kubernetes API, run the following to open a port forward to the dashboard:
```bash
kubectl argo rollouts dashboard
```

Note that you can also build the API server and run this instead,

```
make plugin
./dist/kubectl-argo-rollouts dashboard
```

In another terminal, run the following to start the UI:
```bash
cd ui
yarn install
yarn start
```

## Controller architecture

Argo Rollouts is actually a collection of individual controllers
Expand Down
8 changes: 8 additions & 0 deletions pkg/kubectl-argo-rollouts/info/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,11 @@ func TestRolloutAborted(t *testing.T) {
assert.Equal(t, "Degraded", roInfo.Status)
assert.Equal(t, `RolloutAborted: metric "web" assessed Failed due to failed (1) > failureLimit (0)`, roInfo.Message)
}

func TestRolloutInfoMetadata(t *testing.T) {
rolloutObjs := testdata.NewCanaryRollout()
roInfo := NewRolloutInfo(rolloutObjs.Rollouts[0], rolloutObjs.ReplicaSets, rolloutObjs.Pods, rolloutObjs.Experiments, rolloutObjs.AnalysisRuns, nil)
assert.Equal(t, roInfo.ObjectMeta.Name, rolloutObjs.Rollouts[0].Name)
assert.Equal(t, roInfo.ObjectMeta.Annotations, rolloutObjs.Rollouts[0].Annotations)
assert.Equal(t, roInfo.ObjectMeta.Labels, rolloutObjs.Rollouts[0].Labels)
}
2 changes: 2 additions & 0 deletions pkg/kubectl-argo-rollouts/info/rollout_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func NewRolloutInfo(
ObjectMeta: &v1.ObjectMeta{
Name: ro.Name,
Namespace: ro.Namespace,
Labels: ro.Labels,
Annotations: ro.Annotations,
UID: ro.UID,
CreationTimestamp: ro.CreationTimestamp,
ResourceVersion: ro.ObjectMeta.ResourceVersion,
Expand Down
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"antd": "^5.4.2",
Expand Down
11 changes: 8 additions & 3 deletions ui/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import './App.scss';
import {NamespaceContext, RolloutAPI} from './shared/context/api';
import {Modal} from './components/modal/modal';
import {Rollout} from './components/rollout/rollout';
import {RolloutsList} from './components/rollouts-list/rollouts-list';
import {RolloutsHome} from './components/rollouts-home/rollouts-home';
import {Shortcut, Shortcuts} from './components/shortcuts/shortcuts';
import {ConfigProvider} from 'antd';
import {theme} from '../config/theme';
Expand All @@ -33,7 +33,12 @@ const Page = (props: {path: string; component: React.ReactNode; exact?: boolean;
pageHasShortcuts={!!props.shortcuts}
showHelp={() => {
if (props.shortcuts) {
setShowShortcuts(true);
setShowShortcuts(!showShortcuts);
}
}}
hideHelp={() => {
if (props.shortcuts) {
setShowShortcuts(false);
}
}}
/>
Expand Down Expand Up @@ -84,7 +89,7 @@ const App = () => {
<Page
exact
path='/:namespace?'
component={<RolloutsList />}
component={<RolloutsHome />}
shortcuts={[
{key: '/', description: 'Search'},
{key: 'TAB', description: 'Search, navigate search items'},
Expand Down
8 changes: 5 additions & 3 deletions ui/src/app/components/confirm-button/confirm-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react';
import {Button, Popconfirm, Tooltip} from 'antd';
import {ButtonProps} from 'antd/es/button/button';
import {useState} from 'react';
import { TooltipPlacement } from 'antd/es/tooltip';
import {TooltipPlacement} from 'antd/es/tooltip';

interface ConfirmButtonProps extends ButtonProps {
skipconfirm?: boolean;
Expand Down Expand Up @@ -51,7 +51,8 @@ export const ConfirmButton = (props: ConfirmButtonProps) => {
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}>
}}
>
<Popconfirm
title='Are you sure?'
open={open && !props.disabled}
Expand All @@ -60,7 +61,8 @@ export const ConfirmButton = (props: ConfirmButtonProps) => {
okText='Yes'
cancelText='No'
onOpenChange={handleOpenChange}
placement={props.placement || 'bottom'}>
placement={props.placement || 'bottom'}
>
<div>
<Tooltip title={props.tooltip}>
<Button {...buttonProps}>{props.children}</Button>
Expand Down
20 changes: 19 additions & 1 deletion ui/src/app/components/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';

import {useParams} from 'react-router';
import {Key, KeybindingContext} from 'react-keyhooks';
import {NamespaceContext, RolloutAPIContext} from '../../shared/context/api';

import './header.scss';
Expand All @@ -11,26 +12,43 @@ import {faBook, faKeyboard} from '@fortawesome/free-solid-svg-icons';

const Logo = () => <img src='assets/images/argo-icon-color-square.png' style={{width: '37px', height: '37px', margin: '0 12px'}} alt='Argo Logo' />;

export const Header = (props: {pageHasShortcuts: boolean; changeNamespace: (val: string) => void; showHelp: () => void}) => {
export const Header = (props: {pageHasShortcuts: boolean; changeNamespace: (val: string) => void; showHelp: () => void; hideHelp: () => void}) => {
const history = useHistory();
const namespaceInfo = React.useContext(NamespaceContext);
const {namespace} = useParams<{namespace: string}>();
const api = React.useContext(RolloutAPIContext);
const [version, setVersion] = React.useState('v?');
const [nsInput, setNsInput] = React.useState(namespaceInfo.namespace);
const {useKeybinding} = React.useContext(KeybindingContext);

useKeybinding([Key.SHIFT, Key.H],
() => {
props.showHelp();
return true;
},
true
);

useKeybinding(Key.ESCAPE, () => {
props.hideHelp();
return true;
});

React.useEffect(() => {
const getVersion = async () => {
const v = await api.rolloutServiceVersion();
setVersion(v.rolloutsVersion);
};
getVersion();
}, []);

React.useEffect(() => {
if (namespace && namespace != namespaceInfo.namespace) {
props.changeNamespace(namespace);
setNsInput(namespace);
}
}, []);

return (
<header className='rollouts-header'>
<Link to='/' className='rollouts-header__brand'>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/components/info-item/info-item.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
margin-right: 5px;
color: $argo-color-gray-8;
display: flex;
align-items: center;
align-items: left;
min-width: 0;

&--lightweight {
Expand Down
6 changes: 3 additions & 3 deletions ui/src/app/components/info-item/info-item.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import './info-item.scss';
import { Tooltip } from 'antd';
import {Tooltip} from 'antd';

export enum InfoItemKind {
Default = 'default',
Expand Down Expand Up @@ -40,7 +40,7 @@ export const InfoItem = (props: InfoItemProps) => {
/**
* Displays a right justified InfoItem (or multiple InfoItems) and a left justfied label
*/
export const InfoItemRow = (props: {label: string | React.ReactNode; items?: InfoItemProps | InfoItemProps[]; lightweight?: boolean}) => {
export const InfoItemRow = (props: {label: string | React.ReactNode; items?: InfoItemProps | InfoItemProps[]; lightweight?: boolean; style?: React.CSSProperties}) => {
let {label, items} = props;
let itemComponents = null;
if (!Array.isArray(items)) {
Expand All @@ -55,7 +55,7 @@ export const InfoItemRow = (props: {label: string | React.ReactNode; items?: Inf
<label>{label}</label>
</div>
)}
{props.items && <div className='info-item--row__container'>{itemComponents}</div>}
{props.items && <div className='info-item--row__container' style={props.style}>{itemComponents}</div>}
</div>
);
};
5 changes: 3 additions & 2 deletions ui/src/app/components/pods/pods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const ReplicaSets = (props: {replicaSets: RolloutReplicaSetInfo[]; showRe
<div key={rsInfo.objectMeta.uid} style={{marginBottom: '1em'}}>
<ReplicaSet rs={rsInfo} showRevision={props.showRevisions} />
</div>
)
),
)}
</div>
);
Expand Down Expand Up @@ -84,7 +84,8 @@ export const ReplicaSet = (props: {rs: RolloutReplicaSetInfo; showRevision?: boo
<span>
Scaledown in <Duration durationMs={time} />
</span>
}>
}
>
<InfoItem content={(<Duration durationMs={time} />) as any} icon='fa fa-clock'></InfoItem>
</Tooltip>
);
Expand Down
3 changes: 2 additions & 1 deletion ui/src/app/components/rollout-actions/rollout-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export const RolloutActionButton = (props: {action: RolloutAction; rollout: Roll
disabled={ap.disabled}
loading={loading}
tooltip={ap.tooltip}
icon={<FontAwesomeIcon icon={ap.icon} style={{marginRight: '5px'}} />}>
icon={<FontAwesomeIcon icon={ap.icon} style={{marginRight: '5px'}} />}
>
{props.action}
</ConfirmButton>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ $colWidth: ($WIDGET_WIDTH + (2 * $widgetPadding)) + $widgetMarginRight;
align-items: center;
margin-top: 1.5em;
z-index: 10 !important;
color: $argo-color-gray-7;
font-size: 14px;
}
}
}
}
134 changes: 134 additions & 0 deletions ui/src/app/components/rollout-grid-widget/rollout-grid-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as React from 'react';
import {Link} from 'react-router-dom';

import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCircleNotch, faRedoAlt} from '@fortawesome/free-solid-svg-icons';
import {IconDefinition} from '@fortawesome/fontawesome-svg-core';
import {faStar as faStarSolid} from '@fortawesome/free-solid-svg-icons';
import {faStar as faStarOutline} from '@fortawesome/free-regular-svg-icons/faStar';

import {Tooltip} from 'antd';

import {ParsePodStatus, PodStatus, ReplicaSets} from '../pods/pods';
import {RolloutInfo} from '../../../models/rollout/rollout';
import {useWatchRollout} from '../../shared/services/rollout';
import {useClickOutside} from '../../shared/utils/utils';
import {InfoItemKind, InfoItemRow} from '../info-item/info-item';
import {RolloutAction, RolloutActionButton} from '../rollout-actions/rollout-actions';
import {RolloutStatus, StatusIcon} from '../status-icon/status-icon';
import './rollout-grid-widget.scss';

export const isInProgress = (rollout: RolloutInfo): boolean => {
for (const rs of rollout.replicaSets || []) {
for (const p of rs.pods || []) {
const status = ParsePodStatus(p.status);
if (status === PodStatus.Pending) {
return true;
}
}
}
return false;
};

export const RolloutGridWidget = (props: {
rollout: RolloutInfo;
deselect: () => void;
selected?: boolean;
isFavorite: boolean;
onFavoriteChange: (rolloutName: string, isFavorite: boolean) => void;
}) => {
const [watching, subscribe] = React.useState(false);
let rollout = props.rollout;
useWatchRollout(props.rollout?.objectMeta?.name, watching, null, (r: RolloutInfo) => (rollout = r));
const ref = React.useRef(null);
useClickOutside(ref, props.deselect);

React.useEffect(() => {
if (watching) {
const to = setTimeout(() => {
if (!isInProgress(rollout)) {
subscribe(false);
}
}, 5000);
return () => clearTimeout(to);
}
}, [watching, rollout]);

return (
<Link
to={`/rollout/${rollout.objectMeta?.namespace}/${rollout.objectMeta?.name}`}
className={`rollouts-list__widget ${props.selected ? 'rollouts-list__widget--selected' : ''}`}
ref={ref}
>
<WidgetHeader
rollout={rollout}
refresh={() => {
subscribe(true);
setTimeout(() => {
subscribe(false);
}, 1000);
}}
isFavorite={props.isFavorite}
handleFavoriteChange={props.onFavoriteChange}
/>
<div className='rollouts-list__widget__body'>
<InfoItemRow
label={'Strategy'}
items={{content: rollout.strategy, icon: rollout.strategy === 'BlueGreen' ? 'fa-palette' : 'fa-dove', kind: rollout.strategy.toLowerCase() as InfoItemKind}}
/>
{(rollout.strategy || '').toLocaleLowerCase() === 'canary' && <InfoItemRow label={'Weight'} items={{content: rollout.setWeight, icon: 'fa-weight'}} />}
</div>
<ReplicaSets replicaSets={rollout.replicaSets} showRevisions />
<div className='rollouts-list__widget__message'>{rollout.message !== 'CanaryPauseStep' && rollout.message}</div>
<div className='rollouts-list__widget__actions'>
<RolloutActionButton action={RolloutAction.Restart} rollout={rollout} callback={() => subscribe(true)} indicateLoading />
<RolloutActionButton action={RolloutAction.Promote} rollout={rollout} callback={() => subscribe(true)} indicateLoading />
</div>
</Link>
);
};

const WidgetHeader = (props: {rollout: RolloutInfo; refresh: () => void; isFavorite: boolean; handleFavoriteChange: (rolloutName: string, isFavorite: boolean) => void}) => {
const {rollout} = props;
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
setTimeout(() => setLoading(false), 500);
}, [loading]);

const handleFavoriteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
props.handleFavoriteChange(rollout.objectMeta?.name, !props.isFavorite);
};

return (
<header>
{props.isFavorite ? (
<button onClick={handleFavoriteClick} style={{cursor: 'pointer'}}>
<FontAwesomeIcon icon={faStarSolid} size='lg' style={{marginRight: '10px'}} />
</button>
) : (
<button onClick={handleFavoriteClick} style={{cursor: 'pointer'}}>
<FontAwesomeIcon icon={faStarOutline as IconDefinition} size='lg' style={{marginRight: '10px'}} />
</button>
)}
{rollout.objectMeta?.name}
<span style={{marginLeft: 'auto', display: 'flex', alignItems: 'center'}}>
<Tooltip title='Refresh'>
<FontAwesomeIcon
icon={loading ? faCircleNotch : faRedoAlt}
spin={loading}
className={`rollouts-list__widget__refresh`}
style={{marginRight: '10px', fontSize: '14px'}}
onClick={(e) => {
props.refresh();
setLoading(true);
e.preventDefault();
}}
/>
</Tooltip>
<StatusIcon status={rollout.status as RolloutStatus} />
</span>
</header>
);
};
3 changes: 2 additions & 1 deletion ui/src/app/components/rollout/containers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export const ContainersWidget = (props: ContainersWidgetProps) => {
setError(true);
}
}
}}>
}}
>
{error ? 'ERROR' : 'SAVE'}
</ConfirmButton>
</div>
Expand Down
Loading

0 comments on commit 8cae284

Please sign in to comment.