From 4024109e6f3a000cb6c5246f3ec57c1a3c0cc50b Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Wed, 18 Oct 2023 19:13:27 -0400 Subject: [PATCH 01/17] feat: Rollouts UI Refresh Signed-off-by: Philip Clark --- docs/CONTRIBUTING.md | 29 ++ .../info/rollout_info.go | 2 + ui/package.json | 1 + .../confirm-button/confirm-button.tsx | 8 +- ui/src/app/components/header/header.tsx | 9 + ui/src/app/components/pods/pods.tsx | 5 +- .../rollout-actions/rollout-actions.tsx | 3 +- .../rollout-widget.scss} | 4 +- .../rollout-widget/rollout-widget.tsx | 134 +++++++++ ui/src/app/components/rollout/containers.tsx | 3 +- ui/src/app/components/rollout/revision.tsx | 15 +- ui/src/app/components/rollout/rollout.tsx | 7 +- .../rollouts-grid/rollouts-grid.scss | 8 + .../rollouts-grid/rollouts-grid.tsx | 95 ++++++ .../rollouts-home/rollouts-home.scss | 182 ++++++++++++ .../rollouts-home/rollouts-home.tsx | 167 +++++++++++ .../rollouts-list/rollouts-list.tsx | 258 ----------------- .../rollouts-table/rollouts-table.scss | 18 ++ .../rollouts-table/rollouts-table.tsx | 272 ++++++++++++++++++ .../rollouts-toolbar/rollouts-toolbar.scss | 47 +++ .../rollouts-toolbar/rollouts-toolbar.tsx | 230 +++++++++++++++ .../components/status-count/status-count.scss | 31 ++ .../components/status-count/status-count.tsx | 16 ++ .../components/status-icon/status-icon.tsx | 34 ++- ui/yarn.lock | 12 + 25 files changed, 1306 insertions(+), 284 deletions(-) rename ui/src/app/components/{rollouts-list/rollouts-list.scss => rollout-widget/rollout-widget.scss} (98%) create mode 100644 ui/src/app/components/rollout-widget/rollout-widget.tsx create mode 100644 ui/src/app/components/rollouts-grid/rollouts-grid.scss create mode 100644 ui/src/app/components/rollouts-grid/rollouts-grid.tsx create mode 100644 ui/src/app/components/rollouts-home/rollouts-home.scss create mode 100644 ui/src/app/components/rollouts-home/rollouts-home.tsx delete mode 100644 ui/src/app/components/rollouts-list/rollouts-list.tsx create mode 100644 ui/src/app/components/rollouts-table/rollouts-table.scss create mode 100644 ui/src/app/components/rollouts-table/rollouts-table.tsx create mode 100644 ui/src/app/components/rollouts-toolbar/rollouts-toolbar.scss create mode 100644 ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx create mode 100644 ui/src/app/components/status-count/status-count.scss create mode 100644 ui/src/app/components/status-count/status-count.tsx diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index a37ad29fc0..510bac78af 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -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 @@ -178,6 +201,12 @@ make start-e2e E2E_INSTANCE_ID='' ``` +6. Working on CRDs? While editing them directly works when you are finding the shape of things you want, the final CRDs are autogenerated. Make sure to regenerate them before submitting PRs. They are controlled by the relevant annotations in the types file: + +eg: Analysis Templates are controlled by annotations in `pkg/apis/rollouts/v1alpha1/analysis_types.go`. + +Refer to https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html and https://book.kubebuilder.io/reference/markers/crd-validation.html for more info on annotations you can use. + ## Running Local Containers You may need to run containers locally, so here's how: diff --git a/pkg/kubectl-argo-rollouts/info/rollout_info.go b/pkg/kubectl-argo-rollouts/info/rollout_info.go index 59ee3f076a..8afc8cf02f 100644 --- a/pkg/kubectl-argo-rollouts/info/rollout_info.go +++ b/pkg/kubectl-argo-rollouts/info/rollout_info.go @@ -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, diff --git a/ui/package.json b/ui/package.json index fac0a758f3..46c0e34666 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/app/components/confirm-button/confirm-button.tsx b/ui/src/app/components/confirm-button/confirm-button.tsx index 4dd4f37e7c..48ce3fba77 100644 --- a/ui/src/app/components/confirm-button/confirm-button.tsx +++ b/ui/src/app/components/confirm-button/confirm-button.tsx @@ -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; @@ -51,7 +51,8 @@ export const ConfirmButton = (props: ConfirmButtonProps) => { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - }}> + }} + > { okText='Yes' cancelText='No' onOpenChange={handleOpenChange} - placement={props.placement || 'bottom'}> + placement={props.placement || 'bottom'} + >
diff --git a/ui/src/app/components/header/header.tsx b/ui/src/app/components/header/header.tsx index e267822108..e33b3c826f 100644 --- a/ui/src/app/components/header/header.tsx +++ b/ui/src/app/components/header/header.tsx @@ -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'; @@ -18,6 +19,12 @@ export const Header = (props: {pageHasShortcuts: boolean; changeNamespace: (val: 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; + }); + React.useEffect(() => { const getVersion = async () => { const v = await api.rolloutServiceVersion(); @@ -25,12 +32,14 @@ export const Header = (props: {pageHasShortcuts: boolean; changeNamespace: (val: }; getVersion(); }, []); + React.useEffect(() => { if (namespace && namespace != namespaceInfo.namespace) { props.changeNamespace(namespace); setNsInput(namespace); } }, []); + return (
diff --git a/ui/src/app/components/pods/pods.tsx b/ui/src/app/components/pods/pods.tsx index c3e5fecb32..1107a8444d 100644 --- a/ui/src/app/components/pods/pods.tsx +++ b/ui/src/app/components/pods/pods.tsx @@ -55,7 +55,7 @@ export const ReplicaSets = (props: {replicaSets: RolloutReplicaSetInfo[]; showRe
- ) + ), )}
); @@ -84,7 +84,8 @@ export const ReplicaSet = (props: {rs: RolloutReplicaSetInfo; showRevision?: boo Scaledown in - }> + } + > ) as any} icon='fa fa-clock'> ); diff --git a/ui/src/app/components/rollout-actions/rollout-actions.tsx b/ui/src/app/components/rollout-actions/rollout-actions.tsx index 94a4b289f2..fd2ff83f7d 100644 --- a/ui/src/app/components/rollout-actions/rollout-actions.tsx +++ b/ui/src/app/components/rollout-actions/rollout-actions.tsx @@ -107,7 +107,8 @@ export const RolloutActionButton = (props: {action: RolloutAction; rollout: Roll disabled={ap.disabled} loading={loading} tooltip={ap.tooltip} - icon={}> + icon={} + > {props.action} ); diff --git a/ui/src/app/components/rollouts-list/rollouts-list.scss b/ui/src/app/components/rollout-widget/rollout-widget.scss similarity index 98% rename from ui/src/app/components/rollouts-list/rollouts-list.scss rename to ui/src/app/components/rollout-widget/rollout-widget.scss index f0ec5fdcc7..68f92bf93e 100644 --- a/ui/src/app/components/rollouts-list/rollouts-list.scss +++ b/ui/src/app/components/rollout-widget/rollout-widget.scss @@ -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; } } -} +} \ No newline at end of file diff --git a/ui/src/app/components/rollout-widget/rollout-widget.tsx b/ui/src/app/components/rollout-widget/rollout-widget.tsx new file mode 100644 index 0000000000..8832ddfcc7 --- /dev/null +++ b/ui/src/app/components/rollout-widget/rollout-widget.tsx @@ -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-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 RolloutWidget = (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 ( + + { + subscribe(true); + setTimeout(() => { + subscribe(false); + }, 1000); + }} + isFavorite={props.isFavorite} + handleFavoriteChange={props.onFavoriteChange} + /> +
+ + {(rollout.strategy || '').toLocaleLowerCase() === 'canary' && } +
+ +
{rollout.message !== 'CanaryPauseStep' && rollout.message}
+
+ subscribe(true)} indicateLoading /> + subscribe(true)} indicateLoading /> +
+ + ); +}; + +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) => { + e.stopPropagation(); + e.preventDefault(); + props.handleFavoriteChange(rollout.objectMeta?.name, !props.isFavorite); + }; + + return ( +
+ {props.isFavorite ? ( + + ) : ( + + )} + {rollout.objectMeta?.name} + + + { + props.refresh(); + setLoading(true); + e.preventDefault(); + }} + /> + + + +
+ ); +}; diff --git a/ui/src/app/components/rollout/containers.tsx b/ui/src/app/components/rollout/containers.tsx index c69b30658b..552ac29a93 100644 --- a/ui/src/app/components/rollout/containers.tsx +++ b/ui/src/app/components/rollout/containers.tsx @@ -63,7 +63,8 @@ export const ContainersWidget = (props: ContainersWidgetProps) => { setError(true); } } - }}> + }} + > {error ? 'ERROR' : 'SAVE'} diff --git a/ui/src/app/components/rollout/revision.tsx b/ui/src/app/components/rollout/revision.tsx index e2fcd11526..bd7f7410ce 100644 --- a/ui/src/app/components/rollout/revision.tsx +++ b/ui/src/app/components/rollout/revision.tsx @@ -68,7 +68,8 @@ export const RevisionWidget = (props: RevisionWidgetProps) => { onClick={() => props.rollback(Number(revision.number))} type='default' icon={} - style={{fontSize: '13px', marginRight: '10px'}}> + style={{fontSize: '13px', marginRight: '10px'}} + > Rollback )} @@ -123,11 +124,13 @@ const AnalysisRunWidget = (props: {analysisRuns: RolloutAnalysisRunInfo[]}) => { {ar.status} - }> + } + >
+ }`} + > @@ -201,7 +204,8 @@ const AnalysisRunWidget = (props: {analysisRuns: RolloutAnalysisRunInfo[]}) => { )} ); - })}> + })} + >
@@ -264,7 +268,8 @@ const AnalysisRunWidget = (props: {analysisRuns: RolloutAnalysisRunInfo[]}) => { )} ); - })}> + })} + > diff --git a/ui/src/app/components/rollout/rollout.tsx b/ui/src/app/components/rollout/rollout.tsx index 91b6c0a9d8..317d223d2d 100644 --- a/ui/src/app/components/rollout/rollout.tsx +++ b/ui/src/app/components/rollout/rollout.tsx @@ -332,7 +332,8 @@ const Step = (props: {step: GithubComArgoprojArgoRolloutsPkgApisRolloutsV1alpha1 (props.step.setMirrorRoute && openMirror) ? 'steps__step-title--experiment' : '' - }`}> + }`} + > {icon && } {content} {unit} {props.step.setCanaryScale && ( @@ -457,7 +458,7 @@ const WidgetItemSetMirror = ({value}: {value: GithubComArgoprojArgoRolloutsPkgAp {index} - Path ({stringMatcherType})
{stringMatcherValue}
- + , ); } if (val.method != null) { @@ -479,7 +480,7 @@ const WidgetItemSetMirror = ({value}: {value: GithubComArgoprojArgoRolloutsPkgAp {index} - Method ({stringMatcherType})
{stringMatcherValue}
- + , ); } return fragments; diff --git a/ui/src/app/components/rollouts-grid/rollouts-grid.scss b/ui/src/app/components/rollouts-grid/rollouts-grid.scss new file mode 100644 index 0000000000..4f6e1dc22f --- /dev/null +++ b/ui/src/app/components/rollouts-grid/rollouts-grid.scss @@ -0,0 +1,8 @@ +@import 'node_modules/argo-ui/v2/styles/colors'; + +.rollouts-grid { + display: flex; + box-sizing: border-box; + flex-wrap: wrap; + padding-top: 20px; +} diff --git a/ui/src/app/components/rollouts-grid/rollouts-grid.tsx b/ui/src/app/components/rollouts-grid/rollouts-grid.tsx new file mode 100644 index 0000000000..04d2ed57d5 --- /dev/null +++ b/ui/src/app/components/rollouts-grid/rollouts-grid.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import {useHistory} from 'react-router-dom'; +import {Key, KeybindingContext, useNav} from 'react-keyhooks'; +import {RolloutInfo} from '../../../models/rollout/rollout'; +import {RolloutWidget} from '../rollout-widget/rollout-widget'; +import './rollouts-grid.scss'; + +export const RolloutsGrid = ({ + rollouts, + onFavoriteChange, + favorites, +}: { + rollouts: RolloutInfo[]; + onFavoriteChange: (rolloutName: string, isFavorite: boolean) => void; + favorites: {[key: string]: boolean}; +}) => { + const [itemsPerRow, setItemsPerRow] = React.useState(0); + const rolloutsGridRef = React.useRef(null); + + const handleFavoriteChange = (rolloutName: string, isFavorite: boolean) => { + onFavoriteChange(rolloutName, isFavorite); + }; + + const orderedRollouts = rollouts + .map((rollout) => { + return { + ...rollout, + key: rollout.objectMeta?.uid, + favorite: favorites[rollout.objectMeta?.name] || false, + }; + }) + .sort((a, b) => { + if (a.favorite && !b.favorite) { + return -1; + } else if (!a.favorite && b.favorite) { + return 1; + } else { + return 0; + } + }); + + // Calculate the number of items per row for keyboard navigation + React.useEffect(() => { + const rolloutsGrid = rolloutsGridRef.current; + + const updateItemsPerRow = () => { + if (rolloutsGrid) { + const containerWidth = rolloutsGrid.clientWidth; + const widgetWidth = parseInt(getComputedStyle(document.querySelector('.rollouts-list__widget')).getPropertyValue('width'), 10); + const widgetPadding = parseInt(getComputedStyle(document.querySelector('.rollouts-list__widget')).getPropertyValue('padding'), 10); + const itemsPerRowValue = Math.floor(containerWidth / (widgetWidth + widgetPadding * 2)); + setItemsPerRow(itemsPerRowValue); + } + }; + + updateItemsPerRow(); + + window.addEventListener('resize', updateItemsPerRow); + + return () => { + window.removeEventListener('resize', updateItemsPerRow); + }; + }, []); + + const history = useHistory(); + const [pos, nav, reset] = useNav(orderedRollouts.length); + const {useKeybinding} = React.useContext(KeybindingContext); + + useKeybinding(Key.RIGHT, () => nav(1)); + useKeybinding(Key.LEFT, () => nav(-1)); + useKeybinding(Key.UP, () => nav(-itemsPerRow)); + useKeybinding(Key.DOWN, () => nav(itemsPerRow)); + useKeybinding(Key.ENTER, () => { + if (pos !== undefined) { + history.push(`/rollout/${orderedRollouts[pos].objectMeta?.name}`); + return true; + } + return false; + }); + + return ( +
+ {orderedRollouts.map((rollout, i) => ( + reset()} + isFavorite={favorites[rollout.objectMeta?.name] || false} + onFavoriteChange={handleFavoriteChange} + /> + ))} +
+ ); +}; diff --git a/ui/src/app/components/rollouts-home/rollouts-home.scss b/ui/src/app/components/rollouts-home/rollouts-home.scss new file mode 100644 index 0000000000..575c8350f6 --- /dev/null +++ b/ui/src/app/components/rollouts-home/rollouts-home.scss @@ -0,0 +1,182 @@ +@import 'node_modules/argo-ui/v2/styles/colors'; + +$WIDGET_WIDTH: 400px; + +$widgetPadding: 17px; +$widgetMarginRight: 20px; +$colWidth: ($WIDGET_WIDTH + (2 * $widgetPadding)) + $widgetMarginRight; + +.rollouts-home { + height: 100vh; + display: flex; + flex-direction: column; +} + +.rollouts-list { + display: flex; + box-sizing: border-box; + flex-wrap: wrap; + + &__search-container { + width: 50% !important; + margin: 0 auto; + } + + &__search { + width: 100%; + font-size: 15px; + } + + &__rollouts-container { + padding: 20px; + display: flex; + flex-wrap: wrap; + + width: 3 * $colWidth; + margin: 0 auto; + + @media screen and (max-width: (3 * $colWidth)) { + width: 2 * $colWidth; + margin: 0 auto; + } + + @media screen and (max-width: (2 * $colWidth)) { + width: $colWidth; + + .rollouts-list__widget { + margin: 0 inherit; + width: 100%; + } + + .rollouts-list__search-container { + width: 100% !important; + } + } + } + + &__empty-message { + padding-top: 70px; + width: 50%; + margin: 0 auto; + color: $argo-color-gray-7; + h1 { + margin-bottom: 1em; + text-align: center; + } + div { + line-height: 1.5em; + } + pre { + overflow: scroll; + cursor: pointer; + line-height: 2em; + font-size: 15px; + padding: 3px 5px; + color: $argo-color-gray-8; + margin: 0.5em 0; + background-color: white; + } + a { + color: $sea; + border-bottom: 1px solid $sea; + } + + &--dark { + color: $shine; + a { + color: $sky; + border-color: $sky; + } + pre { + background-color: $space; + color: $shine; + } + } + + @media screen and (max-width: (2 * $colWidth)) { + width: 80%; + } + } + + &__toolbar { + width: 100%; + padding: 1em 0; + background-color: white; + border-bottom: 1px solid white; + + &--dark { + border-bottom: 1px solid $silver-lining; + background-color: $space; + } + } + + &__widget { + position: relative; + box-sizing: border-box; + padding: 17px; + font-size: 14px; + margin: 0 10px; + color: $argo-color-gray-7; + width: $WIDGET_WIDTH; + height: max-content; + flex-shrink: 0; + margin-bottom: 1.5em; + border-radius: 5px; + background-color: white; + box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.05); + border: 1px solid $argo-color-gray-4; + z-index: 0; + + &:hover, + &--selected { + border-color: $argo-running-color; + } + + &__pods { + margin-bottom: 1em; + } + + &--dark { + color: $dull-shine; + border-color: $silver-lining; + box-shadow: 1px 2px 3px 1px $space; + background: none; + } + + &__refresh { + &:hover { + color: $argo-running-color; + } + } + + &__body { + margin-bottom: 1em; + padding-bottom: 0.75em; + border-bottom: 1px solid $argo-color-gray-4; + &--dark { + border-bottom: 1px solid $silver-lining; + } + } + + header { + color: $argo-color-gray-8; + display: flex; + align-items: center; + font-weight: 600; + font-size: 20px; + margin-bottom: 1em; + } + + &--dark header { + color: $shine; + border-bottom: 1px solid $silver-lining; + } + &__actions { + position: relative; + display: flex; + align-items: center; + margin-top: 1.5em; + z-index: 10 !important; + } + } +} diff --git a/ui/src/app/components/rollouts-home/rollouts-home.tsx b/ui/src/app/components/rollouts-home/rollouts-home.tsx new file mode 100644 index 0000000000..c9847b5ff9 --- /dev/null +++ b/ui/src/app/components/rollouts-home/rollouts-home.tsx @@ -0,0 +1,167 @@ +import * as React from 'react'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faCircleNotch} from '@fortawesome/free-solid-svg-icons'; + +import {NamespaceContext} from '../../shared/context/api'; +import {useWatchRollouts} from '../../shared/services/rollout'; +import {RolloutsToolbar, defaultDisplayMode, Filters} from '../rollouts-toolbar/rollouts-toolbar'; +import {RolloutsTable} from '../rollouts-table/rollouts-table'; +import {RolloutsGrid} from '../rollouts-grid/rollouts-grid'; +import './rollouts-home.scss'; + +export const RolloutsHome = () => { + const rolloutsList = useWatchRollouts(); + const rollouts = rolloutsList.items; + const loading = rolloutsList.loading; + const namespaceCtx = React.useContext(NamespaceContext); + + const [filters, setFilters] = React.useState({ + showRequiresAttention: false, + showFavorites: false, + name: '', + displayMode: defaultDisplayMode, + status: { + progressing: false, + degraded: false, + paused: false, + healthy: false, + }, + }); + + const handleFilterChange = (newFilters: Filters) => { + setFilters(newFilters); + }; + + const [favorites, setFavorites] = React.useState(() => { + const favoritesStr = localStorage.getItem('rolloutsFavorites'); + return favoritesStr ? JSON.parse(favoritesStr) : {}; + }); + + const handleFavoriteChange = (rolloutName: string, isFavorite: boolean) => { + const newFavorites = {...favorites}; + if (isFavorite) { + newFavorites[rolloutName] = true; + } else { + delete newFavorites[rolloutName]; + } + setFavorites(newFavorites); + localStorage.setItem('rolloutsFavorites', JSON.stringify(newFavorites)); + }; + + const filteredRollouts = React.useMemo(() => { + return rollouts.filter((r) => { + if (filters.showFavorites && !favorites[r.objectMeta.name]) { + return false; + } + if (filters.showRequiresAttention && r.status !== 'Degraded' && r.status !== 'Paused' && r.message !== 'CanaryPauseStep') { + return false; + } + if (Object.values(filters.status).some((value) => value === true) && !filters.status[r.status]) { + return false; + } + let nameMatches = false; + for (let term of filters.name.split(',').map((t) => t.trim())) { + if (term === '') continue; // Skip empty terms + + if (term.includes(':')) { + // Filter by label + const [key, value] = term.split(':'); + if (value.startsWith('"') && value.endsWith('"')) { + const exactValue = value.substring(1, value.length - 1); + if (r.objectMeta.labels && r.objectMeta.labels[key] && r.objectMeta.labels[key] === exactValue) { + nameMatches = true; + break; + } + } else if (r.objectMeta.labels && r.objectMeta.labels[key] && r.objectMeta.labels[key].includes(value)) { + nameMatches = true; + break; + } + } else { + // Filter by name + const isNegated = term.startsWith('!'); + term = term.replace(/^!/, ''); + + const isExact = term.startsWith('"') && term.endsWith('"'); + term = term.replace(/^"|"$/g, ''); + + if (isExact) { + if (isNegated) { + if (r.objectMeta.name !== term) { + nameMatches = true; + continue; + } + } else { + if (r.objectMeta.name === term) { + nameMatches = true; + break; + } + } + } else { + if (isNegated) { + if (!r.objectMeta.name.includes(term)) { + nameMatches = true; + break; + } + } else { + if (r.objectMeta.name.includes(term)) { + nameMatches = true; + break; + } + } + } + } + } + if (filters.name != '' && !nameMatches) return false; + return true; + }); + }, [rollouts, filters, favorites]); + + return ( +
+ +
+ {loading ? ( +
+ + Loading... +
+ ) : (rollouts || []).length > 0 ? ( + + {filters.displayMode === 'table' && } + {filters.displayMode !== 'table' && } + + ) : ( + + )} +
+
+ ); +}; + +const EmptyMessage = (props: {namespace: string}) => { + const CodeLine = (props: {children: string}) => { + return
 navigator.clipboard.writeText(props.children)}>{props.children}
; + }; + return ( +
+

No Rollouts to display!

+
+
Make sure you are running the API server in the correct namespace. Your current namespace is:
+
+ {props.namespace} +
+
+
+ To create a new Rollout and Service, run + kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/rollout.yaml + kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/service.yaml + or follow the{' '} + + Getting Started guide + + . +
+
+ ); +}; diff --git a/ui/src/app/components/rollouts-list/rollouts-list.tsx b/ui/src/app/components/rollouts-list/rollouts-list.tsx deleted file mode 100644 index e8c9a4dc5f..0000000000 --- a/ui/src/app/components/rollouts-list/rollouts-list.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import * as React from 'react'; -import {Key, KeybindingContext, useNav} from 'react-keyhooks'; -import {Link, useHistory} from 'react-router-dom'; -import {RolloutInfo} from '../../../models/rollout/rollout'; -import {NamespaceContext} from '../../shared/context/api'; -import {useWatchRollout, useWatchRollouts} from '../../shared/services/rollout'; -import {useClickOutside} from '../../shared/utils/utils'; -import {ParsePodStatus, PodStatus, ReplicaSets} from '../pods/pods'; -import {RolloutAction, RolloutActionButton} from '../rollout-actions/rollout-actions'; -import {RolloutStatus, StatusIcon} from '../status-icon/status-icon'; -import './rollouts-list.scss'; -import {AutoComplete, Tooltip} from 'antd'; -import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; -import {faCircleNotch, faRedoAlt} from '@fortawesome/free-solid-svg-icons'; -import {InfoItemKind, InfoItemRow} from '../info-item/info-item'; - -const useRolloutNames = (rollouts: RolloutInfo[]) => { - const parseNames = (rl: RolloutInfo[]) => - (rl || []).map((r) => { - const name = r.objectMeta?.name || ''; - return { - label: name, - value: name, - }; - }); - - const [rolloutNames, setRolloutNames] = React.useState(parseNames(rollouts)); - React.useEffect(() => { - setRolloutNames(parseNames(rollouts)); - }, [rollouts]); - - return rolloutNames; -}; - -export const RolloutsList = () => { - const rolloutsList = useWatchRollouts(); - const rollouts = rolloutsList.items; - const loading = rolloutsList.loading; - const [filteredRollouts, setFilteredRollouts] = React.useState(rollouts); - const [pos, nav, reset] = useNav(filteredRollouts.length); - const [searchString, setSearchString] = React.useState(''); - const searchParam = new URLSearchParams(window.location.search).get('q'); - React.useEffect(() => { - if (searchParam && searchParam != searchString) { - setSearchString(searchParam); - } - }, []); - - const searchRef = React.useRef(null); - - React.useEffect(() => { - if (searchRef.current) { - // or, if Input component in your ref, then use input property like: - // searchRef.current.input.focus(); - searchRef.current.focus(); - } - }, [searchRef]); - - const {useKeybinding} = React.useContext(KeybindingContext); - - useKeybinding(Key.RIGHT, () => nav(1)); - useKeybinding(Key.LEFT, () => nav(-1)); - useKeybinding(Key.ESCAPE, () => { - reset(); - if (searchString && searchString !== '') { - setSearchString(''); - return true; - } else { - return false; - } - }); - - const rolloutNames = useRolloutNames(rollouts); - const history = useHistory(); - - useKeybinding(Key.SLASH, () => { - if (!searchString) { - if (searchRef) { - searchRef.current.focus(); - } - return true; - } - return false; - }); - - useKeybinding(Key.ENTER, () => { - if (pos > -1) { - history.push(`/rollout/${filteredRollouts[pos].objectMeta?.name}`); - return true; - } - return false; - }); - - React.useEffect(() => { - const filtered = (rollouts || []).filter((r) => (r.objectMeta?.name || '').includes(searchString)); - if ((filtered || []).length > 0) { - setFilteredRollouts(filtered); - } - if (searchString) { - history.replace(`/${namespaceCtx.namespace}?q=${searchString}`); - } else { - history.replace(`/${namespaceCtx.namespace}`); - } - }, [searchString, rollouts]); - - const namespaceCtx = React.useContext(NamespaceContext); - - return ( -
- {loading ? ( -
- - Loading... -
- ) : (rollouts || []).length > 0 ? ( - -
-
- history.push(`/rollout/${namespaceCtx.namespace}/${val}`)} - options={rolloutNames} - onChange={(val) => setSearchString(val)} - value={searchString} - ref={searchRef} - /> -
-
-
- {(filteredRollouts.sort((a, b) => (a.objectMeta.name < b.objectMeta.name ? -1 : 1)) || []).map((rollout, i) => ( - reset()} /> - ))} -
-
- ) : ( - - )} -
- ); -}; - -const EmptyMessage = (props: {namespace: string}) => { - const CodeLine = (props: {children: string}) => { - return
 navigator.clipboard.writeText(props.children)}>{props.children}
; - }; - return ( -
-

No Rollouts to display!

-
-
Make sure you are running the API server in the correct namespace. Your current namespace is:
-
- {props.namespace} -
-
-
- To create a new Rollout and Service, run - kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/rollout.yaml - kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/master/docs/getting-started/basic/service.yaml - or follow the{' '} - - Getting Started guide - - . -
-
- ); -}; - -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 RolloutWidget = (props: {rollout: RolloutInfo; deselect: () => void; selected?: boolean}) => { - 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 ( - - { - subscribe(true); - setTimeout(() => { - subscribe(false); - }, 1000); - }} - /> -
- - {(rollout.strategy || '').toLocaleLowerCase() === 'canary' && } -
- {(rollout.replicaSets || []).length < 1 && } - -
- subscribe(true)} indicateLoading /> - subscribe(true)} indicateLoading /> -
- - ); -}; - -const WidgetHeader = (props: {rollout: RolloutInfo; refresh: () => void}) => { - const {rollout} = props; - const [loading, setLoading] = React.useState(false); - React.useEffect(() => { - setTimeout(() => setLoading(false), 500); - }, [loading]); - return ( -
- {rollout.objectMeta?.name} - - - { - props.refresh(); - setLoading(true); - e.preventDefault(); - }} - /> - - - -
- ); -}; diff --git a/ui/src/app/components/rollouts-table/rollouts-table.scss b/ui/src/app/components/rollouts-table/rollouts-table.scss new file mode 100644 index 0000000000..bd93df799a --- /dev/null +++ b/ui/src/app/components/rollouts-table/rollouts-table.scss @@ -0,0 +1,18 @@ +@import 'node_modules/argo-ui/v2/styles/colors'; + +.rollouts-table { + width: 100%; +} + +.rollouts-table__row__selected { + background-color: $argo-color-gray-2; + } + +.rollouts-table_widget_actions { + display: flex; + flex-wrap: wrap; +} + +.rollouts-table_widget_actions_button { + margin-top: 10px; +} diff --git a/ui/src/app/components/rollouts-table/rollouts-table.tsx b/ui/src/app/components/rollouts-table/rollouts-table.tsx new file mode 100644 index 0000000000..4525214caf --- /dev/null +++ b/ui/src/app/components/rollouts-table/rollouts-table.tsx @@ -0,0 +1,272 @@ +import * as React from 'react'; +import {useHistory} from 'react-router-dom'; +import {Tooltip, Table, TablePaginationConfig} from 'antd'; +import {Key, KeybindingContext} from 'react-keyhooks'; +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +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 {RolloutAction, RolloutActionButton} from '../rollout-actions/rollout-actions'; +import {RolloutStatus, StatusIcon} from '../status-icon/status-icon'; +import {ReplicaSetStatus, ReplicaSetStatusIcon} from '../status-icon/status-icon'; +import {RolloutInfo} from '../../../models/rollout/rollout'; +import {InfoItemKind, InfoItemRow} from '../info-item/info-item'; +import './rollouts-table.scss'; + +export const RolloutsTable = ({ + rollouts, + onFavoriteChange, + favorites, +}: { + rollouts: RolloutInfo[]; + onFavoriteChange: (rolloutName: string, isFavorite: boolean) => void; + favorites: {[key: string]: boolean}; +}) => { + const tableRef = React.useRef(null); + + const handleFavoriteChange = (rolloutName: string, isFavorite: boolean) => { + onFavoriteChange(rolloutName, isFavorite); + }; + const data = rollouts + .map((rollout) => { + return { + ...rollout, + key: rollout.objectMeta?.uid, + favorite: favorites[rollout.objectMeta?.name] || false, + }; + }) + .sort((a, b) => { + if (a.favorite && !b.favorite) { + return -1; + } else if (!a.favorite && b.favorite) { + return 1; + } else { + return 0; + } + }); + + const columns = [ + { + dataIndex: 'favorite', + key: 'favorite', + render: (favorite: boolean, rollout: RolloutInfo) => { + return favorite ? ( + + ) : ( + + ); + }, + width: 50, + }, + { + title: 'Name', + dataIndex: 'objectMeta', + key: 'name', + width: 300, + render: (objectMeta: {name?: string}) => objectMeta.name, + sorter: (a: any, b: any) => a.objectMeta.name.localeCompare(b.objectMeta.name), + }, + { + title: 'Strategy', + dataIndex: 'strategy', + key: 'strategy', + align: 'left' as const, + sorter: (a: any, b: any) => a.strategy.localeCompare(b.strategy), + render: (strategy: string) => { + return ( + + ); + }, + }, + { + title: 'Step', + dataIndex: 'step', + key: 'step', + render: (text: any, record: {step?: string}) => record.step || '-', + sorter: (a: any, b: any) => { + if (a.step === undefined) { + return -1; + } + if (b.step === undefined) { + return 1; + } else return a.step.localeCompare(b.step); + }, + }, + { + title: 'Weight', + dataIndex: 'setWeight', + key: 'weight', + render: (text: any, record: {setWeight?: number}) => record.setWeight || '-', + sorter: (a: any, b: any) => a.setWeight - b.setWeight, + }, + { + title: 'ReplicaSets', + key: 'replicasets', + width: 200, + sorter: (a: RolloutInfo, b: RolloutInfo) => a.desired - b.desired, + render: (rollout: RolloutInfo) => { + const stableReplicaSets = rollout.replicaSets?.filter((rs) => rs.stable); + const canaryReplicaSets = rollout.replicaSets?.filter((rs) => rs.canary); + const previewReplicaSets = rollout.replicaSets?.filter((rs) => rs.preview); + return ( +
+ {stableReplicaSets?.length > 0 && ( +
+ Stable:{' '} + {stableReplicaSets.map((rs) => ( + + + Rev {rs.revision} ({rs.available}/{rs.replicas}) + + + ))} +
+ )} + {canaryReplicaSets?.length > 0 && ( +
+ Canary:{' '} + {canaryReplicaSets.map((rs) => ( + + + Rev {rs.revision} ({rs.available}/{rs.replicas}) + + + ))} +
+ )} + {previewReplicaSets?.length > 0 && ( +
+ Preview:{' '} + {previewReplicaSets.map((rs) => ( + + + Rev {rs.revision} ({rs.available}/{rs.replicas}) + + + ))} +
+ )} +
+ ); + }, + }, + { + title: 'Status', + sorter: (a: any, b: any) => a.status.localeCompare(b.status), + render: (record: {message?: string; status?: string}) => { + return ( +
+ + {record.status} + +
+ ); + }, + }, + { + title: 'Actions', + dataIndex: 'actions', + key: 'actions', + render: (text: any, rollout: {objectMeta?: {name?: string}}) => { + return ( +
+
+ {}} indicateLoading /> +
+
+ {}} indicateLoading /> +
+
+ {}} indicateLoading /> +
+
+ {}} indicateLoading /> +
+
+ ); + }, + }, + ]; + + const history = useHistory(); + const [selectedRow, setSelectedRow] = React.useState(undefined); + const {useKeybinding} = React.useContext(KeybindingContext); + useKeybinding(Key.UP, () => { + if (selectedRow === undefined) { + setSelectedRow(itemsPerPage - 1); + return true; + } else if (selectedRow > 0) { + setSelectedRow(selectedRow - 1); + return true; + } + return false; + }); + useKeybinding(Key.DOWN, () => { + if (selectedRow === undefined) { + setSelectedRow(0); + return true; + } else if (selectedRow < itemsPerPage - 1) { + setSelectedRow(selectedRow + 1); + return true; + } + return false; + }); + useKeybinding(Key.ENTER, () => { + if (selectedRow !== undefined) { + history.push(`/rollout/${data[selectedRow].objectMeta?.name}`); + return true; + } + return false; + }); + useKeybinding(Key.ESCAPE, () => { + setSelectedRow(undefined); + return false; // let the toolbar handle clearing the search bar + }); + + const [itemsPerPage, setItemsPerPage] = React.useState(10); + const handlePaginationChange = (pagination: TablePaginationConfig) => { + setItemsPerPage(pagination.pageSize); + }; + + return ( + ({ + className: selectedRow === index ? 'rollouts-table__row__selected' : '', + onClick: () => { + history.push(`/rollout/${record.objectMeta?.name}`); + }, + style: {cursor: 'pointer'}, + })} + pagination={ + { + pageSize: itemsPerPage, + onChange: handlePaginationChange, + } as TablePaginationConfig + } + ref={tableRef} + rowClassName='rollouts-table__row' + rowKey={(_, index) => index} + style={{width: '100%', padding: '20px 20px'}} + /> + ); +}; diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.scss b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.scss new file mode 100644 index 0000000000..5acb36c0dc --- /dev/null +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.scss @@ -0,0 +1,47 @@ +@import 'node_modules/argo-ui/v2/styles/colors'; + +.rollouts-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 10px; + border-bottom: 1px solid #fff; + background-color: #fff; +} + +.rollouts-toolbar_requires-attention-checkbox { + flex: 2; + padding-left: 20px; +} + +.rollouts-toolbar_search-container { + min-width: 300px; + padding-left: 20px; + padding-right: 20px; +} + +.rollouts-toolbar_display-modes { + margin-left: auto; +} + +.rollouts-toolbar_mode-button { + color: #989898; + border: none; + padding: 5px; + cursor: pointer; + transition: background-color 0.3s ease; + font-size: 24px; // increase the font-size to make the icon larger +} + +.rollouts-toolbar_mode-button:hover { + background-color: #b2b2b2; +} + +.rollouts-toolbar_mode-button.active { + color: #000000; +} + +.rollouts-toolbar_status-button { + cursor: pointer; + transition: background-color 0.3s ease; +} diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx new file mode 100644 index 0000000000..c3df4a2112 --- /dev/null +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx @@ -0,0 +1,230 @@ +import * as React from 'react'; + +import {Key, KeybindingContext} from 'react-keyhooks'; +import {useHistory, useLocation} from 'react-router-dom'; + +import {AutoComplete} from 'antd'; +import {Tooltip} from 'antd'; + +import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import {faTableList, faTableCellsLarge} from '@fortawesome/free-solid-svg-icons'; + +import {RolloutInfo} from '../../../models/rollout/rollout'; +import {StatusCount} from '../status-count/status-count'; +import './rollouts-toolbar.scss'; + +export type Filters = { + showRequiresAttention: boolean; + showFavorites?: boolean; + name: string; + displayMode?: string; + status: { + [key: string]: boolean; + }; +}; + +interface StatusCount { + [key: string]: number; +} + +export const defaultDisplayMode = 'table'; + +export const RolloutsToolbar = ({ + rollouts, + favorites, + onFilterChange, +}: { + rollouts: RolloutInfo[]; + favorites: {[key: string]: boolean}; + onFilterChange: (filters: Filters) => void; +}) => { + const history = useHistory(); + const location = useLocation(); + const searchParams = new URLSearchParams(window.location.search); + const [filters, setFilters] = React.useState({ + showRequiresAttention: searchParams.get('showRequiresAttention') === 'true', + showFavorites: searchParams.get('showFavorites') === 'true', + name: searchParams.get('name') || '', + displayMode: searchParams.get('displayMode') || defaultDisplayMode, + status: { + Progressing: searchParams.get('Progressing') === 'true', + Degraded: searchParams.get('Degraded') === 'true', + Paused: searchParams.get('Paused') === 'true', + Healthy: searchParams.get('Healthy') === 'true', + }, + }); + // Ensure that the filters are updated when the URL changes + onFilterChange(filters); + + const handleFilterChange = (newFilters: Filters) => { + setFilters(newFilters); + onFilterChange(newFilters); + }; + + const handleNameFilterChange = (value: string) => { + const newFilters = { + ...filters, + name: value, + }; + const searchParams = new URLSearchParams(location.search); + if (value) { + searchParams.set('name', value); + } else { + searchParams.delete('name'); + } + history.push({search: searchParams.toString()}); + handleFilterChange(newFilters); + }; + + const handleShowRequiresAttentionChange = (event: React.MouseEvent) => { + const newFilters = { + ...filters, + showRequiresAttention: !filters.showRequiresAttention, + }; + const searchParams = new URLSearchParams(location.search); + if (!filters.showRequiresAttention) { + searchParams.set('showRequiresAttention', 'true'); + } else { + searchParams.delete('showRequiresAttention'); + } + history.push({search: searchParams.toString()}); + handleFilterChange(newFilters); + }; + + const handleShowFavoritesChange = (event: React.MouseEvent) => { + const newFilters = { + ...filters, + showFavorites: !filters.showFavorites, + }; + const searchParams = new URLSearchParams(location.search); + if (!filters.showFavorites) { + searchParams.set('showFavorites', 'true'); + } else { + searchParams.delete('showFavorites'); + } + history.push({search: searchParams.toString()}); + handleFilterChange(newFilters); + }; + + const handleDisplayModeChange = (event: React.MouseEvent) => { + const newFilters = { + ...filters, + displayMode: event.currentTarget.id, + }; + const searchParams = new URLSearchParams(location.search); + if (event.currentTarget.id !== defaultDisplayMode) { + searchParams.set('displayMode', event.currentTarget.id); + } else { + searchParams.delete('displayMode'); + } + history.push({search: searchParams.toString()}); + handleFilterChange(newFilters); + }; + + const handleStatusFilterChange = (event: React.MouseEvent) => { + const newFilters = { + ...filters, + status: { + ...filters.status, + [event.currentTarget.id]: !filters.status[event.currentTarget.id], + }, + }; + const searchParams = new URLSearchParams(location.search); + if (event.currentTarget.id) { + searchParams.set(event.currentTarget.id, 'true'); + } else { + searchParams.delete(event.currentTarget.id); + } + history.push({search: searchParams.toString()}); + handleFilterChange(newFilters); + }; + + const statusCounts: StatusCount = React.useMemo(() => { + const counts: StatusCount = { + Progressing: 0, + Degraded: 0, + Paused: 0, + Healthy: 0, + }; + rollouts.forEach((r) => { + counts[r.status]++; + }); + + return counts; + }, [rollouts]); + + const needsAttentionCount: number = React.useMemo(() => { + const pausedRollouts = rollouts.filter((r) => r.status === 'Paused' && r.message !== 'CanaryPauseStep'); + return statusCounts['Degraded'] + pausedRollouts.length; + }, [rollouts, statusCounts]); + + const favoriteCount: number = React.useMemo(() => { + return rollouts.filter((r) => favorites[r.objectMeta.name]).length; + }, [rollouts, favorites]); + + const searchRef = React.useRef(null); + const {useKeybinding} = React.useContext(KeybindingContext); + useKeybinding(Key.SLASH, () => { + if (searchRef) { + searchRef.current.focus(); + return true; + } + return false; + }); + useKeybinding(Key.ESCAPE, () => { + if (filters.name !== '') { + handleNameFilterChange(''); + searchRef.current.blur(); + return true; + } else { + return false; + } + }); + + return ( +
+
+ + + + + + + {Object.keys(statusCounts).map((status: string) => { + return ( + + + + ); + })} +
+
+ + +
+ + + +
+ ); +}; diff --git a/ui/src/app/components/status-count/status-count.scss b/ui/src/app/components/status-count/status-count.scss new file mode 100644 index 0000000000..9dec534d27 --- /dev/null +++ b/ui/src/app/components/status-count/status-count.scss @@ -0,0 +1,31 @@ +@import 'node_modules/argo-ui/v2/styles/colors'; + +.status-count { + display: flex; + align-items: center; + border: 1px solid $argo-color-gray-4; + border-radius: 5px; + padding: 2px; + margin: 1px; + + &__icon { + font-size: 15px; + color: $argo-color-gray-8; + margin: 5px; + + text-align: center; + flex: 0 0 auto; + } + + &__count { + font-size: 15px; + font-weight: 500; + color: $argo-color-gray-8; + margin-right: 5px; + text-align: right; + flex: 1; + } +} +.status-count.active { + background-color: $argo-color-teal-2; +} diff --git a/ui/src/app/components/status-count/status-count.tsx b/ui/src/app/components/status-count/status-count.tsx new file mode 100644 index 0000000000..83cea4f5ac --- /dev/null +++ b/ui/src/app/components/status-count/status-count.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; + +import {RolloutStatus, StatusIcon} from '../status-icon/status-icon'; + +import './status-count.scss'; + +export const StatusCount = ({status, count, defaultIcon = 'fa-exclamation-circle', active = false}: {status: String; count: Number; defaultIcon?: String; active?: boolean}) => { + return ( +
+
+ +
+
{count}
+
+ ); +}; diff --git a/ui/src/app/components/status-icon/status-icon.tsx b/ui/src/app/components/status-icon/status-icon.tsx index 257dc50567..5da619a357 100644 --- a/ui/src/app/components/status-icon/status-icon.tsx +++ b/ui/src/app/components/status-icon/status-icon.tsx @@ -9,9 +9,11 @@ export enum RolloutStatus { Healthy = 'Healthy', } -export const StatusIcon = (props: {status: RolloutStatus}): JSX.Element => { +export const StatusIcon = (props: {status: RolloutStatus; showTooltip?: boolean; defaultIcon?: String}): JSX.Element => { let icon, className; let spin = false; + const showTooltip = props.showTooltip ?? true; + const defaultIcon = props.defaultIcon ?? 'fa-question-circle'; const {status} = props; switch (status) { case 'Progressing': { @@ -36,14 +38,19 @@ export const StatusIcon = (props: {status: RolloutStatus}): JSX.Element => { break; } default: { - icon = 'fa-question-circle'; + icon = defaultIcon; className = 'unknown'; } } return ( - - - + + {showTooltip && ( + + + + )} + {!showTooltip && } + ); }; @@ -55,9 +62,11 @@ export enum ReplicaSetStatus { Progressing = 'Progressing', } -export const ReplicaSetStatusIcon = (props: {status: ReplicaSetStatus}) => { +export const ReplicaSetStatusIcon = (props: {status: ReplicaSetStatus; showTooltip?: boolean; defaultIcon?: String}) => { let icon, className; let spin = false; + const showTooltip = props.showTooltip ?? true; + const defaultIcon = props.defaultIcon ?? 'fa-question-circle'; const {status} = props; switch (status) { case 'Healthy': @@ -83,13 +92,18 @@ export const ReplicaSetStatusIcon = (props: {status: ReplicaSetStatus}) => { break; } default: { - icon = 'fa-question-circle'; + icon = defaultIcon; className = 'unknown'; } } return ( - - - + + {showTooltip && ( + + + + )} + {!showTooltip && } + ); }; diff --git a/ui/yarn.lock b/ui/yarn.lock index 29f5446d37..af1a13d826 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1326,6 +1326,11 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b" integrity sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ== +"@fortawesome/fontawesome-common-types@6.4.2": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" + integrity sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA== + "@fortawesome/fontawesome-free@^5.8.1": version "5.15.4" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5" @@ -1338,6 +1343,13 @@ dependencies: "@fortawesome/fontawesome-common-types" "6.4.0" +"@fortawesome/free-regular-svg-icons@^6.4.0": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz#aee79ed76ce5dd04931352f9d83700761b8b1b25" + integrity sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q== + dependencies: + "@fortawesome/fontawesome-common-types" "6.4.2" + "@fortawesome/free-solid-svg-icons@^6.4.0": version "6.4.0" resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.0.tgz#48c0e790847fa56299e2f26b82b39663b8ad7119" From 925aab6cd40ed87e9dd369c847e3f700a3f8b671 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Thu, 19 Oct 2023 08:54:15 -0400 Subject: [PATCH 02/17] add test for labels and annotations Signed-off-by: Philip Clark --- pkg/kubectl-argo-rollouts/info/info_test.go | 8 ++++++++ ui/src/app/App.tsx | 6 +++--- ui/src/app/components/info-item/info-item.tsx | 2 +- ui/src/app/index.tsx | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pkg/kubectl-argo-rollouts/info/info_test.go b/pkg/kubectl-argo-rollouts/info/info_test.go index d7bbe161fd..ab28b48637 100644 --- a/pkg/kubectl-argo-rollouts/info/info_test.go +++ b/pkg/kubectl-argo-rollouts/info/info_test.go @@ -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) +} diff --git a/ui/src/app/App.tsx b/ui/src/app/App.tsx index 60ba5419c6..b1c022a70c 100644 --- a/ui/src/app/App.tsx +++ b/ui/src/app/App.tsx @@ -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'; @@ -33,7 +33,7 @@ const Page = (props: {path: string; component: React.ReactNode; exact?: boolean; pageHasShortcuts={!!props.shortcuts} showHelp={() => { if (props.shortcuts) { - setShowShortcuts(true); + setShowShortcuts(!showShortcuts); } }} /> @@ -84,7 +84,7 @@ const App = () => { } + component={} shortcuts={[ {key: '/', description: 'Search'}, {key: 'TAB', description: 'Search, navigate search items'}, diff --git a/ui/src/app/components/info-item/info-item.tsx b/ui/src/app/components/info-item/info-item.tsx index 7e7bf8e617..ab64712885 100644 --- a/ui/src/app/components/info-item/info-item.tsx +++ b/ui/src/app/components/info-item/info-item.tsx @@ -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', diff --git a/ui/src/app/index.tsx b/ui/src/app/index.tsx index 3bc5d04223..56521a1185 100644 --- a/ui/src/app/index.tsx +++ b/ui/src/app/index.tsx @@ -6,5 +6,5 @@ ReactDOM.render( , - document.getElementById('root') + document.getElementById('root'), ); From 9181c1264bf6090322a423fa7e11a7005c4734c0 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Thu, 19 Oct 2023 09:01:12 -0400 Subject: [PATCH 03/17] simplify regex Signed-off-by: Philip Clark --- ui/src/app/components/rollouts-home/rollouts-home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/components/rollouts-home/rollouts-home.tsx b/ui/src/app/components/rollouts-home/rollouts-home.tsx index c9847b5ff9..dfb0008f68 100644 --- a/ui/src/app/components/rollouts-home/rollouts-home.tsx +++ b/ui/src/app/components/rollouts-home/rollouts-home.tsx @@ -83,7 +83,7 @@ export const RolloutsHome = () => { term = term.replace(/^!/, ''); const isExact = term.startsWith('"') && term.endsWith('"'); - term = term.replace(/^"|"$/g, ''); + term = term.replace(/"/g, ''); if (isExact) { if (isNegated) { From 678cad848fdb4a4c1513cf5c3cc10521ae40d98a Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Fri, 20 Oct 2023 10:25:46 -0400 Subject: [PATCH 04/17] make filters use OR logic Signed-off-by: Philip Clark --- .../rollouts-home/rollouts-home.tsx | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/ui/src/app/components/rollouts-home/rollouts-home.tsx b/ui/src/app/components/rollouts-home/rollouts-home.tsx index dfb0008f68..5e11c94ea5 100644 --- a/ui/src/app/components/rollouts-home/rollouts-home.tsx +++ b/ui/src/app/components/rollouts-home/rollouts-home.tsx @@ -51,16 +51,26 @@ export const RolloutsHome = () => { const filteredRollouts = React.useMemo(() => { return rollouts.filter((r) => { - if (filters.showFavorites && !favorites[r.objectMeta.name]) { - return false; + // If no filters are set, show all rollouts + if (filters.name === '' && !filters.showFavorites && !filters.showRequiresAttention && !Object.values(filters.status).some((value) => value === true)) { + return true; } - if (filters.showRequiresAttention && r.status !== 'Degraded' && r.status !== 'Paused' && r.message !== 'CanaryPauseStep') { - return false; + + let favoritesMatches = false; + let requiresAttentionMatches = false; + let statusMatches = false; + let nameMatches = false; + + if (filters.showFavorites && favorites[r.objectMeta.name]) { + favoritesMatches = true; } - if (Object.values(filters.status).some((value) => value === true) && !filters.status[r.status]) { - return false; + if (filters.showRequiresAttention && (r.status === 'Degraded' || (r.status === 'Paused' && r.message !== 'CanaryPauseStep'))) { + requiresAttentionMatches = true; } - let nameMatches = false; + if (Object.values(filters.status).some((value) => value === true) && filters.status[r.status]) { + statusMatches = true; + } + for (let term of filters.name.split(',').map((t) => t.trim())) { if (term === '') continue; // Skip empty terms @@ -112,8 +122,12 @@ export const RolloutsHome = () => { } } } - if (filters.name != '' && !nameMatches) return false; - return true; + + if (favoritesMatches || requiresAttentionMatches || statusMatches || nameMatches) { + return true; + } else { + return false; + } }); }, [rollouts, filters, favorites]); From 6869f5d0140a5be16d8e41eb28fe22628c1bc5ce Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Mon, 30 Oct 2023 14:15:43 -0400 Subject: [PATCH 05/17] add keyboard listener Signed-off-by: Philip Clark --- ui/src/app/components/rollouts-home/rollouts-home.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/app/components/rollouts-home/rollouts-home.tsx b/ui/src/app/components/rollouts-home/rollouts-home.tsx index 5e11c94ea5..6a5594ae84 100644 --- a/ui/src/app/components/rollouts-home/rollouts-home.tsx +++ b/ui/src/app/components/rollouts-home/rollouts-home.tsx @@ -155,7 +155,10 @@ export const RolloutsHome = () => { const EmptyMessage = (props: {namespace: string}) => { const CodeLine = (props: {children: string}) => { - return
 navigator.clipboard.writeText(props.children)}>{props.children}
; + return ( +
 navigator.clipboard.writeText(props.children)}
+            onKeyDown={() => navigator.clipboard.writeText(props.children)}
+            >{props.children}
); }; return (
From 65d2812bb0605782d4e86afd43f03ff05ec38057 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Wed, 8 Nov 2023 11:17:56 -0600 Subject: [PATCH 06/17] set default display mode to grid Signed-off-by: Philip Clark --- ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx index c3df4a2112..2b2bc34f1c 100644 --- a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx @@ -27,7 +27,7 @@ interface StatusCount { [key: string]: number; } -export const defaultDisplayMode = 'table'; +export const defaultDisplayMode = 'grid'; export const RolloutsToolbar = ({ rollouts, From c30e7a15b05d1be95450fffcf80d7e84cc55398b Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Tue, 14 Nov 2023 13:07:02 -0500 Subject: [PATCH 07/17] set strategy column to left justify Signed-off-by: Philip Clark --- ui/src/app/components/info-item/info-item.scss | 2 +- ui/src/app/components/info-item/info-item.tsx | 4 ++-- .../components/rollouts-table/rollouts-table.tsx | 13 +++++++++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/ui/src/app/components/info-item/info-item.scss b/ui/src/app/components/info-item/info-item.scss index 0cd040dde6..17cc0d4d4d 100644 --- a/ui/src/app/components/info-item/info-item.scss +++ b/ui/src/app/components/info-item/info-item.scss @@ -8,7 +8,7 @@ margin-right: 5px; color: $argo-color-gray-8; display: flex; - align-items: center; + align-items: left; min-width: 0; &--lightweight { diff --git a/ui/src/app/components/info-item/info-item.tsx b/ui/src/app/components/info-item/info-item.tsx index ab64712885..7928310bc1 100644 --- a/ui/src/app/components/info-item/info-item.tsx +++ b/ui/src/app/components/info-item/info-item.tsx @@ -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)) { @@ -55,7 +55,7 @@ export const InfoItemRow = (props: {label: string | React.ReactNode; items?: Inf
)} - {props.items &&
{itemComponents}
} + {props.items &&
{itemComponents}
} ); }; diff --git a/ui/src/app/components/rollouts-table/rollouts-table.tsx b/ui/src/app/components/rollouts-table/rollouts-table.tsx index 4525214caf..38124f1e60 100644 --- a/ui/src/app/components/rollouts-table/rollouts-table.tsx +++ b/ui/src/app/components/rollouts-table/rollouts-table.tsx @@ -12,6 +12,7 @@ import {RolloutStatus, StatusIcon} from '../status-icon/status-icon'; import {ReplicaSetStatus, ReplicaSetStatusIcon} from '../status-icon/status-icon'; import {RolloutInfo} from '../../../models/rollout/rollout'; import {InfoItemKind, InfoItemRow} from '../info-item/info-item'; +import { AlignType } from 'rc-table/lib/interface'; import './rollouts-table.scss'; export const RolloutsTable = ({ @@ -87,11 +88,19 @@ export const RolloutsTable = ({ title: 'Strategy', dataIndex: 'strategy', key: 'strategy', - align: 'left' as const, + align: 'left' as AlignType, sorter: (a: any, b: any) => a.strategy.localeCompare(b.strategy), render: (strategy: string) => { return ( - + ); }, }, From d8cb10a8a252007b81466641decab4b738c78ed5 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Tue, 14 Nov 2023 13:10:19 -0500 Subject: [PATCH 08/17] add tooltips to view buttons Signed-off-by: Philip Clark --- .../rollouts-toolbar/rollouts-toolbar.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx index 2b2bc34f1c..8858edb867 100644 --- a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx @@ -205,12 +205,16 @@ export const RolloutsToolbar = ({ })}
- - + + + + + +
Date: Tue, 14 Nov 2023 13:14:04 -0500 Subject: [PATCH 09/17] consider unknown status rollouts as needing attention Signed-off-by: Philip Clark --- ui/src/app/components/rollouts-home/rollouts-home.tsx | 2 +- ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/src/app/components/rollouts-home/rollouts-home.tsx b/ui/src/app/components/rollouts-home/rollouts-home.tsx index 6a5594ae84..5e27b9653d 100644 --- a/ui/src/app/components/rollouts-home/rollouts-home.tsx +++ b/ui/src/app/components/rollouts-home/rollouts-home.tsx @@ -64,7 +64,7 @@ export const RolloutsHome = () => { if (filters.showFavorites && favorites[r.objectMeta.name]) { favoritesMatches = true; } - if (filters.showRequiresAttention && (r.status === 'Degraded' || (r.status === 'Paused' && r.message !== 'CanaryPauseStep'))) { + if (filters.showRequiresAttention && (r.status === 'Unknown' || r.status === 'Degraded' || (r.status === 'Paused' && r.message !== 'CanaryPauseStep'))) { requiresAttentionMatches = true; } if (Object.values(filters.status).some((value) => value === true) && filters.status[r.status]) { diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx index 8858edb867..c62b5ca153 100644 --- a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx @@ -155,7 +155,9 @@ export const RolloutsToolbar = ({ const needsAttentionCount: number = React.useMemo(() => { const pausedRollouts = rollouts.filter((r) => r.status === 'Paused' && r.message !== 'CanaryPauseStep'); - return statusCounts['Degraded'] + pausedRollouts.length; + const degradedRollouts = rollouts.filter((r) => r.status === 'Degraded'); + const unknownRollouts = rollouts.filter((r) => r.status === 'Unknown'); + return pausedRollouts.length + degradedRollouts.length + unknownRollouts.length; }, [rollouts, statusCounts]); const favoriteCount: number = React.useMemo(() => { From ff17b55844af9cb6d40d6b12f1dd7b7761a5be36 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Tue, 14 Nov 2023 13:52:33 -0500 Subject: [PATCH 10/17] remove duplicate escape key listener Signed-off-by: Philip Clark --- ui/src/app/components/header/header.tsx | 11 ++++++++++- .../components/rollouts-toolbar/rollouts-toolbar.tsx | 9 --------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ui/src/app/components/header/header.tsx b/ui/src/app/components/header/header.tsx index e33b3c826f..cff0046149 100644 --- a/ui/src/app/components/header/header.tsx +++ b/ui/src/app/components/header/header.tsx @@ -20,7 +20,16 @@ export const Header = (props: {pageHasShortcuts: boolean; changeNamespace: (val: const [version, setVersion] = React.useState('v?'); const [nsInput, setNsInput] = React.useState(namespaceInfo.namespace); const {useKeybinding} = React.useContext(KeybindingContext); - useKeybinding([Key.SHIFT, Key.H], () => { + + useKeybinding([Key.SHIFT, Key.H], + () => { + props.showHelp(); + return true; + }, + true + ); + + useKeybinding(Key.ESCAPE, () => { props.showHelp(); return true; }); diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx index c62b5ca153..0eaa6dff81 100644 --- a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx @@ -173,15 +173,6 @@ export const RolloutsToolbar = ({ } return false; }); - useKeybinding(Key.ESCAPE, () => { - if (filters.name !== '') { - handleNameFilterChange(''); - searchRef.current.blur(); - return true; - } else { - return false; - } - }); return (
From 04f53919993dad97e4ddf9fdaad02bb1d7d72fe9 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Tue, 14 Nov 2023 16:59:10 -0500 Subject: [PATCH 11/17] group status filters together Signed-off-by: Philip Clark --- .../rollouts-toolbar/rollouts-toolbar.scss | 8 ++++++++ .../rollouts-toolbar/rollouts-toolbar.tsx | 20 ++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.scss b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.scss index 5acb36c0dc..bc234b43c3 100644 --- a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.scss +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.scss @@ -45,3 +45,11 @@ cursor: pointer; transition: background-color 0.3s ease; } + +.rollouts-toolbar_status-filters { + display: flex; +} + +.rollouts-toolbar_status-buttons { + margin-left: 20px; +} \ No newline at end of file diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx index 0eaa6dff81..bff6c4ff07 100644 --- a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx @@ -187,15 +187,17 @@ export const RolloutsToolbar = ({ - {Object.keys(statusCounts).map((status: string) => { - return ( - - - - ); - })} +
+ {Object.keys(statusCounts).map((status: string) => { + return ( + + + + ); + })} +
From 4bae584f35ee01655baf7402056b9c4d0a733851 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Tue, 14 Nov 2023 16:59:28 -0500 Subject: [PATCH 12/17] improve filter logic Signed-off-by: Philip Clark --- .../rollouts-home/rollouts-home.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ui/src/app/components/rollouts-home/rollouts-home.tsx b/ui/src/app/components/rollouts-home/rollouts-home.tsx index 5e27b9653d..1c893d3d34 100644 --- a/ui/src/app/components/rollouts-home/rollouts-home.tsx +++ b/ui/src/app/components/rollouts-home/rollouts-home.tsx @@ -56,18 +56,21 @@ export const RolloutsHome = () => { return true; } + const statusFiltersSet = Object.values(filters.status).some((value) => value === true); + const nameFilterSet = filters.name !== ''; + let favoritesMatches = false; let requiresAttentionMatches = false; let statusMatches = false; let nameMatches = false; - + if (filters.showFavorites && favorites[r.objectMeta.name]) { favoritesMatches = true; } if (filters.showRequiresAttention && (r.status === 'Unknown' || r.status === 'Degraded' || (r.status === 'Paused' && r.message !== 'CanaryPauseStep'))) { requiresAttentionMatches = true; } - if (Object.values(filters.status).some((value) => value === true) && filters.status[r.status]) { + if (statusFiltersSet && filters.status[r.status]) { statusMatches = true; } @@ -122,12 +125,15 @@ export const RolloutsHome = () => { } } } - - if (favoritesMatches || requiresAttentionMatches || statusMatches || nameMatches) { - return true; - } else { - return false; - } + console.log('nameFilterSet %s, showFavorites: %s, showRequiresAttention: %s, statusFiltersSet: %s', nameFilterSet, filters.showFavorites, filters.showRequiresAttention, statusFiltersSet); + console.log('nameMatches: %s, favoritesMatches: %s, requiresAttentionMatches: %s, statusMatches: %s', nameMatches, favoritesMatches, requiresAttentionMatches, statusMatches); + + return ( + (!nameFilterSet || nameMatches) && + (!filters.showFavorites || favoritesMatches) && + (!filters.showRequiresAttention || requiresAttentionMatches) && + (!statusFiltersSet || statusMatches) + ); }); }, [rollouts, filters, favorites]); From 9b5cd46613792c038351257307ea8fc134db9076 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Tue, 14 Nov 2023 16:59:46 -0500 Subject: [PATCH 13/17] remove debug logging Signed-off-by: Philip Clark --- ui/src/app/components/rollouts-home/rollouts-home.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/app/components/rollouts-home/rollouts-home.tsx b/ui/src/app/components/rollouts-home/rollouts-home.tsx index 1c893d3d34..bd70caa2d6 100644 --- a/ui/src/app/components/rollouts-home/rollouts-home.tsx +++ b/ui/src/app/components/rollouts-home/rollouts-home.tsx @@ -125,8 +125,6 @@ export const RolloutsHome = () => { } } } - console.log('nameFilterSet %s, showFavorites: %s, showRequiresAttention: %s, statusFiltersSet: %s', nameFilterSet, filters.showFavorites, filters.showRequiresAttention, statusFiltersSet); - console.log('nameMatches: %s, favoritesMatches: %s, requiresAttentionMatches: %s, statusMatches: %s', nameMatches, favoritesMatches, requiresAttentionMatches, statusMatches); return ( (!nameFilterSet || nameMatches) && From 530b942ff468ed62efbfd6aec45f11cc535835b2 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Tue, 14 Nov 2023 17:14:14 -0500 Subject: [PATCH 14/17] dont show help on escape Signed-off-by: Philip Clark --- ui/src/app/App.tsx | 5 +++++ ui/src/app/components/header/header.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ui/src/app/App.tsx b/ui/src/app/App.tsx index b1c022a70c..bcdb94b9df 100644 --- a/ui/src/app/App.tsx +++ b/ui/src/app/App.tsx @@ -36,6 +36,11 @@ const Page = (props: {path: string; component: React.ReactNode; exact?: boolean; setShowShortcuts(!showShortcuts); } }} + hideHelp={() => { + if (props.shortcuts) { + setShowShortcuts(false); + } + }} /> {props.component} diff --git a/ui/src/app/components/header/header.tsx b/ui/src/app/components/header/header.tsx index cff0046149..8bdbfc3a83 100644 --- a/ui/src/app/components/header/header.tsx +++ b/ui/src/app/components/header/header.tsx @@ -12,7 +12,7 @@ import {faBook, faKeyboard} from '@fortawesome/free-solid-svg-icons'; const Logo = () => 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}>(); @@ -30,7 +30,7 @@ export const Header = (props: {pageHasShortcuts: boolean; changeNamespace: (val: ); useKeybinding(Key.ESCAPE, () => { - props.showHelp(); + props.hideHelp(); return true; }); From 6716d50547049e8017361736aef003a9ce10df86 Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Tue, 14 Nov 2023 17:17:39 -0500 Subject: [PATCH 15/17] properly remove url search params when disabled Signed-off-by: Philip Clark --- .../rollouts-toolbar/rollouts-toolbar.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx index bff6c4ff07..51288d4dd8 100644 --- a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx @@ -116,12 +116,22 @@ export const RolloutsToolbar = ({ searchParams.set('displayMode', event.currentTarget.id); } else { searchParams.delete('displayMode'); + searchParams.delete('displaymode'); } history.push({search: searchParams.toString()}); handleFilterChange(newFilters); }; const handleStatusFilterChange = (event: React.MouseEvent) => { + const searchParams = new URLSearchParams(location.search); + + if (!filters.status[event.currentTarget.id]) { + searchParams.set(event.currentTarget.id, 'true'); + } else { + searchParams.delete(event.currentTarget.id); + } + history.push({search: searchParams.toString()}); + const newFilters = { ...filters, status: { @@ -129,14 +139,7 @@ export const RolloutsToolbar = ({ [event.currentTarget.id]: !filters.status[event.currentTarget.id], }, }; - const searchParams = new URLSearchParams(location.search); - if (event.currentTarget.id) { - searchParams.set(event.currentTarget.id, 'true'); - } else { - searchParams.delete(event.currentTarget.id); - } - history.push({search: searchParams.toString()}); - handleFilterChange(newFilters); + handleFilterChange(newFilters); }; const statusCounts: StatusCount = React.useMemo(() => { From bd848c96414dba07bac6c3765ad1f1f9ace9a3cf Mon Sep 17 00:00:00 2001 From: Philip Clark Date: Thu, 16 Nov 2023 21:22:03 -0500 Subject: [PATCH 16/17] rename to RolloutGridWidget Signed-off-by: Philip Clark --- .../rollout-grid-widget.scss} | 0 .../rollout-grid-widget.tsx} | 4 ++-- ui/src/app/components/rollouts-grid/rollouts-grid.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename ui/src/app/components/{rollout-widget/rollout-widget.scss => rollout-grid-widget/rollout-grid-widget.scss} (100%) rename ui/src/app/components/{rollout-widget/rollout-widget.tsx => rollout-grid-widget/rollout-grid-widget.tsx} (98%) diff --git a/ui/src/app/components/rollout-widget/rollout-widget.scss b/ui/src/app/components/rollout-grid-widget/rollout-grid-widget.scss similarity index 100% rename from ui/src/app/components/rollout-widget/rollout-widget.scss rename to ui/src/app/components/rollout-grid-widget/rollout-grid-widget.scss diff --git a/ui/src/app/components/rollout-widget/rollout-widget.tsx b/ui/src/app/components/rollout-grid-widget/rollout-grid-widget.tsx similarity index 98% rename from ui/src/app/components/rollout-widget/rollout-widget.tsx rename to ui/src/app/components/rollout-grid-widget/rollout-grid-widget.tsx index 8832ddfcc7..89497124ff 100644 --- a/ui/src/app/components/rollout-widget/rollout-widget.tsx +++ b/ui/src/app/components/rollout-grid-widget/rollout-grid-widget.tsx @@ -16,7 +16,7 @@ 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-widget.scss'; +import './rollout-grid-widget.scss'; export const isInProgress = (rollout: RolloutInfo): boolean => { for (const rs of rollout.replicaSets || []) { @@ -30,7 +30,7 @@ export const isInProgress = (rollout: RolloutInfo): boolean => { return false; }; -export const RolloutWidget = (props: { +export const RolloutGridWidget = (props: { rollout: RolloutInfo; deselect: () => void; selected?: boolean; diff --git a/ui/src/app/components/rollouts-grid/rollouts-grid.tsx b/ui/src/app/components/rollouts-grid/rollouts-grid.tsx index 04d2ed57d5..a5c9e22a13 100644 --- a/ui/src/app/components/rollouts-grid/rollouts-grid.tsx +++ b/ui/src/app/components/rollouts-grid/rollouts-grid.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import {useHistory} from 'react-router-dom'; import {Key, KeybindingContext, useNav} from 'react-keyhooks'; import {RolloutInfo} from '../../../models/rollout/rollout'; -import {RolloutWidget} from '../rollout-widget/rollout-widget'; +import {RolloutGridWidget} from '../rollout-grid-widget/rollout-grid-widget'; import './rollouts-grid.scss'; export const RolloutsGrid = ({ @@ -81,7 +81,7 @@ export const RolloutsGrid = ({ return (
{orderedRollouts.map((rollout, i) => ( - Date: Thu, 16 Nov 2023 21:33:53 -0500 Subject: [PATCH 17/17] prevent help menu from loading while searching Signed-off-by: Philip Clark --- ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx index 51288d4dd8..487097c5d0 100644 --- a/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx +++ b/ui/src/app/components/rollouts-toolbar/rollouts-toolbar.tsx @@ -224,6 +224,9 @@ export const RolloutsToolbar = ({ placeholder='Filter by name or label tag:value' value={filters.name} onChange={handleNameFilterChange} + onKeyDown={(event) => { + event.stopPropagation(); // Prevents shift+H from opening the help menu + }} ref={searchRef} />