Skip to content

Commit 6900988

Browse files
perenesenkoalexmt
andauthored
feat: allow selection of namespace in rollout dashboard (argoproj#1291)
Signed-off-by: Andrii Perenesenko <[email protected]> Signed-off-by: Alexander Matyushentsev <[email protected]> Co-authored-by: Alexander Matyushentsev <[email protected]>
1 parent 96ba030 commit 6900988

File tree

14 files changed

+323
-226
lines changed

14 files changed

+323
-226
lines changed

pkg/apiclient/rollout/rollout.pb.go

Lines changed: 156 additions & 99 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apiclient/rollout/rollout.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ message RolloutWatchEvent {
5959

6060
message NamespaceInfo {
6161
string namespace = 1;
62+
repeated string availableNamespaces = 2;
6263
}
6364

6465
message RolloutInfoList {

pkg/apiclient/rollout/rollout.swagger.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4130,6 +4130,12 @@
41304130
"properties": {
41314131
"namespace": {
41324132
"type": "string"
4133+
},
4134+
"availableNamespaces": {
4135+
"type": "array",
4136+
"items": {
4137+
"type": "string"
4138+
}
41334139
}
41344140
}
41354141
},

server/server.go

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import (
2424
"k8s.io/client-go/dynamic"
2525
kubeinformers "k8s.io/client-go/informers"
2626
"k8s.io/client-go/kubernetes"
27-
appslisters "k8s.io/client-go/listers/apps/v1"
28-
corelisters "k8s.io/client-go/listers/core/v1"
2927
"k8s.io/client-go/tools/cache"
3028

3129
"github.com/argoproj/argo-rollouts/pkg/apiclient/rollout"
@@ -69,42 +67,13 @@ const (
6967

7068
// ArgoRolloutsServer holds information about rollouts server
7169
type ArgoRolloutsServer struct {
72-
Options ServerOptions
73-
NamespaceVC NamespaceViewController
74-
stopCh chan struct{}
75-
}
76-
77-
type NamespaceViewController struct {
78-
namespace string
79-
80-
kubeInformerFactory kubeinformers.SharedInformerFactory
81-
replicaSetLister appslisters.ReplicaSetNamespaceLister
82-
podLister corelisters.PodNamespaceLister
83-
cacheSyncs []cache.InformerSynced
84-
}
85-
86-
func (vc *NamespaceViewController) Start(ctx context.Context) {
87-
vc.kubeInformerFactory.Start(ctx.Done())
88-
cache.WaitForCacheSync(ctx.Done(), vc.cacheSyncs...)
70+
Options ServerOptions
71+
stopCh chan struct{}
8972
}
9073

9174
// NewServer creates an ArgoRolloutsServer
9275
func NewServer(o ServerOptions) *ArgoRolloutsServer {
93-
kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(o.KubeClientset, 0, kubeinformers.WithNamespace(o.Namespace))
94-
95-
vc := NamespaceViewController{
96-
namespace: o.Namespace,
97-
kubeInformerFactory: kubeInformerFactory,
98-
podLister: kubeInformerFactory.Core().V1().Pods().Lister().Pods(o.Namespace),
99-
replicaSetLister: kubeInformerFactory.Apps().V1().ReplicaSets().Lister().ReplicaSets(o.Namespace),
100-
}
101-
102-
vc.cacheSyncs = append(vc.cacheSyncs,
103-
kubeInformerFactory.Apps().V1().ReplicaSets().Informer().HasSynced,
104-
kubeInformerFactory.Core().V1().Pods().Informer().HasSynced,
105-
)
106-
107-
return &ArgoRolloutsServer{Options: o, NamespaceVC: vc}
76+
return &ArgoRolloutsServer{Options: o}
10877
}
10978

11079
type spaFileSystem struct {
@@ -258,20 +227,27 @@ func (s *ArgoRolloutsServer) WatchRolloutInfo(q *rollout.RolloutInfoQuery, ws ro
258227
return nil
259228
}
260229

261-
func (s *ArgoRolloutsServer) ListReplicaSetsAndPods(ctx context.Context) ([]*appsv1.ReplicaSet, []*corev1.Pod, error) {
262-
s.NamespaceVC.Start(ctx)
230+
func (s *ArgoRolloutsServer) ListReplicaSetsAndPods(ctx context.Context, namespace string) ([]*appsv1.ReplicaSet, []*corev1.Pod, error) {
263231

264-
allReplicaSets, err := s.NamespaceVC.replicaSetLister.List(labels.Everything())
232+
allReplicaSets, err := s.Options.KubeClientset.AppsV1().ReplicaSets(namespace).List(ctx, v1.ListOptions{})
265233
if err != nil {
266234
return nil, nil, err
267235
}
268236

269-
allPods, err := s.NamespaceVC.podLister.List(labels.Everything())
237+
allPods, err := s.Options.KubeClientset.CoreV1().Pods(namespace).List(ctx, v1.ListOptions{})
270238
if err != nil {
271-
return allReplicaSets, nil, err
239+
return nil, nil, err
272240
}
273241

274-
return allReplicaSets, allPods, nil
242+
var allReplicaSetsP = make([]*appsv1.ReplicaSet, len(allReplicaSets.Items))
243+
for i := range allReplicaSets.Items {
244+
allReplicaSetsP[i] = &allReplicaSets.Items[i]
245+
}
246+
var allPodsP = make([]*corev1.Pod, len(allPods.Items))
247+
for i := range allPods.Items {
248+
allPodsP[i] = &allPods.Items[i]
249+
}
250+
return allReplicaSetsP, allPodsP, nil
275251
}
276252

277253
// ListRollouts returns a list of all rollouts
@@ -283,7 +259,7 @@ func (s *ArgoRolloutsServer) ListRolloutInfos(ctx context.Context, q *rollout.Ro
283259
return nil, err
284260
}
285261

286-
allReplicaSets, allPods, err := s.ListReplicaSetsAndPods(ctx)
262+
allReplicaSets, allPods, err := s.ListReplicaSetsAndPods(ctx, q.GetNamespace())
287263
if err != nil {
288264
return nil, err
289265
}
@@ -316,13 +292,19 @@ func (s *ArgoRolloutsServer) WatchRolloutInfos(q *rollout.RolloutInfoListQuery,
316292
return
317293
}
318294
}
319-
ctx := context.Background()
295+
ctx := ws.Context()
320296
rolloutIf := s.Options.RolloutsClientset.ArgoprojV1alpha1().Rollouts(q.GetNamespace())
321297

322-
allReplicaSets, allPods, err := s.ListReplicaSetsAndPods(ctx)
323-
if err != nil {
324-
return err
325-
}
298+
kubeInformerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(s.Options.KubeClientset, 0, kubeinformers.WithNamespace(q.Namespace))
299+
podsLister := kubeInformerFactory.Core().V1().Pods().Lister().Pods(q.GetNamespace())
300+
rsLister := kubeInformerFactory.Apps().V1().ReplicaSets().Lister().ReplicaSets(q.GetNamespace())
301+
kubeInformerFactory.Start(ws.Context().Done())
302+
303+
cache.WaitForCacheSync(
304+
ws.Context().Done(),
305+
kubeInformerFactory.Core().V1().Pods().Informer().HasSynced,
306+
kubeInformerFactory.Apps().V1().ReplicaSets().Informer().HasSynced,
307+
)
326308

327309
watchIf, err := rolloutIf.Watch(ctx, v1.ListOptions{})
328310
if err != nil {
@@ -355,6 +337,15 @@ L:
355337
}
356338
continue
357339
}
340+
allPods, err := podsLister.List(labels.Everything())
341+
if err != nil {
342+
return err
343+
}
344+
allReplicaSets, err := rsLister.List(labels.Everything())
345+
if err != nil {
346+
return err
347+
}
348+
358349
// get shallow rollout info
359350
ri := info.NewRolloutInfo(ro, allReplicaSets, allPods, nil, nil)
360351
send(ri)
@@ -365,15 +356,29 @@ L:
365356

366357
func (s *ArgoRolloutsServer) RolloutToRolloutInfo(ro *v1alpha1.Rollout) (*rollout.RolloutInfo, error) {
367358
ctx := context.Background()
368-
allReplicaSets, allPods, err := s.ListReplicaSetsAndPods(ctx)
359+
allReplicaSets, allPods, err := s.ListReplicaSetsAndPods(ctx, ro.Namespace)
369360
if err != nil {
370361
return nil, err
371362
}
372363
return info.NewRolloutInfo(ro, allReplicaSets, allPods, nil, nil), nil
373364
}
374365

375366
func (s *ArgoRolloutsServer) GetNamespace(ctx context.Context, e *empty.Empty) (*rollout.NamespaceInfo, error) {
376-
return &rollout.NamespaceInfo{Namespace: s.Options.Namespace}, nil
367+
var m = make(map[string]bool)
368+
var namespaces []string
369+
370+
rolloutList, err := s.Options.RolloutsClientset.ArgoprojV1alpha1().Rollouts("").List(ctx, v1.ListOptions{})
371+
if err == nil {
372+
for _, r := range rolloutList.Items {
373+
ns := r.Namespace
374+
if !m[ns] {
375+
m[ns] = true
376+
namespaces = append(namespaces, ns)
377+
}
378+
}
379+
}
380+
381+
return &rollout.NamespaceInfo{Namespace: s.Options.Namespace, AvailableNamespaces: namespaces}, nil
377382
}
378383

379384
func (s *ArgoRolloutsServer) PromoteRollout(ctx context.Context, q *rollout.PromoteRolloutRequest) (*v1alpha1.Rollout, error) {

ui/src/app/App.tsx

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,18 @@ import * as React from 'react';
66
import {Key, KeybindingContext, KeybindingProvider} from 'react-keyhooks';
77
import {Redirect, Route, Router, Switch} from 'react-router-dom';
88
import './App.scss';
9+
import {NamespaceContext, RolloutAPI} from './shared/context/api';
910
import {Modal} from './components/modal/modal';
1011
import {Rollout} from './components/rollout/rollout';
1112
import {RolloutsList} from './components/rollouts-list/rollouts-list';
1213
import {Shortcut, Shortcuts} from './components/shortcuts/shortcuts';
13-
import {NamespaceProvider} from './shared/context/api';
1414
import {ThemeProvider} from 'argo-ux';
1515

1616
const bases = document.getElementsByTagName('base');
1717
const base = bases.length > 0 ? bases[0].getAttribute('href') || '/' : '/';
1818
export const history = createBrowserHistory({basename: base});
1919

20-
const Page = (props: {path: string; component: React.ReactNode; exact?: boolean; shortcuts?: Shortcut[]}) => {
20+
const Page = (props: {path: string; component: React.ReactNode; exact?: boolean; shortcuts?: Shortcut[], changeNamespace: (val: string) => void}) => {
2121
const {useKeybinding} = React.useContext(KeybindingContext);
2222
const [showShortcuts, setShowShortcuts] = React.useState(false);
2323
useKeybinding(
@@ -40,6 +40,7 @@ const Page = (props: {path: string; component: React.ReactNode; exact?: boolean;
4040
<Route path={props.path} exact={props.exact}>
4141
<React.Fragment>
4242
<Header
43+
changeNamespace={props.changeNamespace}
4344
pageHasShortcuts={!!props.shortcuts}
4445
showHelp={() => {
4546
if (props.shortcuts) {
@@ -54,33 +55,54 @@ const Page = (props: {path: string; component: React.ReactNode; exact?: boolean;
5455
);
5556
};
5657

58+
export const NAMESPACE_KEY = 'namespace';
59+
const init = window.localStorage.getItem(NAMESPACE_KEY);
60+
5761
const App = () => {
62+
const [namespace, setNamespace] = React.useState(init);
63+
const [availableNamespaces, setAvailableNamespaces] = React.useState([]);
64+
React.useEffect(() => {
65+
RolloutAPI.rolloutServiceGetNamespace().then((info) => {
66+
if (!namespace) {
67+
setNamespace(info.namespace);
68+
}
69+
setAvailableNamespaces(info.availableNamespaces);
70+
});
71+
}, []);
72+
const changeNamespace = (val: string) => {
73+
setNamespace(val);
74+
window.localStorage.setItem(NAMESPACE_KEY, namespace);
75+
};
76+
5877
return (
5978
<ThemeProvider>
60-
<NamespaceProvider>
61-
<KeybindingProvider>
62-
<Router history={history}>
63-
<Switch>
64-
<Redirect exact={true} path='/' to='/rollouts' />
79+
{namespace && (
80+
<NamespaceContext.Provider value={{namespace, availableNamespaces}}>
81+
<KeybindingProvider>
82+
<Router history={history}>
83+
<Switch>
84+
<Redirect exact={true} path='/' to='/rollouts' />
6585

66-
<Page
67-
exact
68-
path='/rollouts'
69-
component={<RolloutsList />}
70-
shortcuts={[
71-
{key: '/', description: 'Search'},
72-
{key: 'TAB', description: 'Search, navigate search items'},
73-
{key: [faArrowLeft, faArrowRight, faArrowUp, faArrowDown], description: 'Navigate rollouts list'},
74-
{key: ['SHIFT', 'H'], description: 'Show help menu', combo: true},
75-
]}
76-
/>
77-
<Page path='/rollout/:name' component={<Rollout />} />
86+
<Page
87+
exact
88+
path='/rollouts'
89+
component={<RolloutsList />}
90+
shortcuts={[
91+
{key: '/', description: 'Search'},
92+
{key: 'TAB', description: 'Search, navigate search items'},
93+
{key: [faArrowLeft, faArrowRight, faArrowUp, faArrowDown], description: 'Navigate rollouts list'},
94+
{key: ['SHIFT', 'H'], description: 'Show help menu', combo: true},
95+
]}
96+
changeNamespace={changeNamespace}
97+
/>
98+
<Page path='/rollout/:name' component={<Rollout />} changeNamespace={changeNamespace} />
7899

79-
<Redirect path='*' to='/' />
80-
</Switch>
81-
</Router>
82-
</KeybindingProvider>
83-
</NamespaceProvider>
100+
<Redirect path='*' to='/' />
101+
</Switch>
102+
</Router>
103+
</KeybindingProvider>
104+
</NamespaceContext.Provider>
105+
)}
84106
</ThemeProvider>
85107
);
86108
};

ui/src/app/components/header/header.scss

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,15 @@
5959
display: flex;
6060
align-items: center;
6161
}
62-
&__version {
62+
&__label {
6363
color: $shine;
6464
margin: 0 15px;
65+
margin: auto;
66+
padding: 5px;
67+
}
68+
&__namespace {
69+
color: black;
70+
display: flex;
71+
position: relative;
6572
}
6673
}

ui/src/app/components/header/header.tsx

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,38 @@
11
import * as React from 'react';
22

3-
import {ActionButton, Brand, InfoItemRow, ThemeToggle, Tooltip, useData, Header as GenericHeader} from 'argo-ux';
3+
import {
4+
ActionButton,
5+
Brand,
6+
InfoItemRow,
7+
ThemeToggle,
8+
Tooltip,
9+
Header as GenericHeader,
10+
Autocomplete,
11+
ThemeDiv,
12+
} from 'argo-ux';
413
import {useParams} from 'react-router';
5-
import {RolloutNamespaceInfo, RolloutServiceApi} from '../../../models/rollout/generated';
6-
import {RolloutAPIContext} from '../../shared/context/api';
14+
import {NamespaceContext, RolloutAPIContext} from '../../shared/context/api';
715
import {faBook, faKeyboard} from '@fortawesome/free-solid-svg-icons';
816

917
import './header.scss';
10-
import {Link} from 'react-router-dom';
18+
import {Link, useHistory} from 'react-router-dom';
1119

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

14-
export const Header = (props: {pageHasShortcuts: boolean; showHelp: () => void}) => {
15-
const getNs = React.useCallback(() => new RolloutServiceApi().rolloutServiceGetNamespace(), []);
16-
const [nsData, loading] = useData<RolloutNamespaceInfo>(getNs);
17-
const [namespace, setNamespace] = React.useState('Unknown');
18-
React.useEffect(() => {
19-
if (!loading && nsData && nsData.namespace) {
20-
setNamespace(nsData.namespace);
21-
}
22-
}, [nsData, loading]);
22+
export const Header = (props: {pageHasShortcuts: boolean; changeNamespace: (val: string) => void; showHelp: () => void}) => {
23+
const history = useHistory();
24+
const namespaceInfo = React.useContext(NamespaceContext);
2325
const {name} = useParams<{name: string}>();
2426
const api = React.useContext(RolloutAPIContext);
2527
const [version, setVersion] = React.useState('v?');
28+
const [nsInput, setNsInput] = React.useState(namespaceInfo.namespace);
2629
React.useEffect(() => {
2730
const getVersion = async () => {
2831
const v = await api.rolloutServiceVersion();
2932
setVersion(v.rolloutsVersion);
3033
};
3134
getVersion();
32-
});
35+
}, []);
3336
return (
3437
<GenericHeader>
3538
<Link to='/'>
@@ -51,8 +54,20 @@ export const Header = (props: {pageHasShortcuts: boolean; showHelp: () => void})
5154
<ThemeToggle />
5255
</Tooltip>
5356
</span>
54-
<InfoItemRow label={'NS:'} items={{content: namespace}} />
55-
<div className='rollouts-header__version'>{version}</div>
57+
{(namespaceInfo.availableNamespaces || []).length == 0 ? (
58+
<InfoItemRow label={'NS:'} items={{content: namespaceInfo.namespace}} />
59+
) : (
60+
<ThemeDiv className='rollouts-header__namespace'>
61+
<div className='rollouts-header__label'>NS:</div>
62+
<Autocomplete items={namespaceInfo.availableNamespaces || []}
63+
placeholder='Namespace'
64+
onChange={(el) => setNsInput(el.target.value)}
65+
onItemClick={(val) => { props.changeNamespace(val ? val : nsInput); history.push(`/rollouts`) } }
66+
value={nsInput}
67+
/>
68+
</ThemeDiv>
69+
)}
70+
<div className='rollouts-header__label'>{version}</div>
5671
</div>
5772
</GenericHeader>
5873
);

0 commit comments

Comments
 (0)