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

feat: Rollouts UI List view refresh #3118

Merged
merged 21 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading