Skip to content

Commit be2d98f

Browse files
committed
feat: group namespaces by internal/regular in CurrentNamespace
Fixes kubernetes-sigs#7058
1 parent 9699034 commit be2d98f

File tree

18 files changed

+394
-27
lines changed

18 files changed

+394
-27
lines changed

plugins/plugin-client-common/src/components/spi/Select/impl/PatternFly.tsx

+49-16
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
*/
1616

1717
import React from 'react'
18-
import { Select, SelectOption } from '@patternfly/react-core'
18+
import { Divider, Select, SelectGroup, SelectOption } from '@patternfly/react-core'
1919

20-
import { pexecInCurrentTab } from '@kui-shell/core'
20+
import { flatten, pexecInCurrentTab } from '@kui-shell/core'
2121

22-
import Props, { SelectOptions } from '../model'
22+
import Props, { SelectOptions, isGrouped, isDivider } from '../model'
2323

2424
interface State {
2525
isOpen: boolean
@@ -60,10 +60,53 @@ export default class PatternFlySelect extends React.PureComponent<Props, State>
6060
}
6161
}
6262

63-
private readonly _onClicks = this.props.options.map(option => this.onClick.bind(this, option))
63+
private readonly _onClicks = isGrouped(this.props)
64+
? flatten(
65+
this.props.groups.map(group =>
66+
isDivider(group) ? [] : group.options.map(option => this.onClick.bind(this, option))
67+
)
68+
)
69+
: this.props.options.map(option => this.onClick.bind(this, option))
70+
6471
private readonly _onSelect = this.onSelect.bind(this)
6572
private readonly _onToggle = this.onToggle.bind(this)
6673

74+
/** @return UI for the given option */
75+
private option(option: SelectOptions, index: number) {
76+
return (
77+
<SelectOption
78+
className="kui--select-option"
79+
data-value={option.label}
80+
key={index}
81+
value={option.label}
82+
isSelected={option.isSelected}
83+
description={option.description}
84+
onClick={this._onClicks[index]}
85+
isDisabled={option.isDisabled}
86+
/>
87+
)
88+
}
89+
90+
/** @return UI for all of the options */
91+
private options() {
92+
if (isGrouped(this.props)) {
93+
let runningIdx = 0
94+
const groups = this.props.groups.map((group, idx1) =>
95+
isDivider(group) ? (
96+
<Divider key={`divider-${idx1}`} />
97+
) : (
98+
<SelectGroup label={group.label} key={`group-${idx1}`}>
99+
{group.options.map(option => this.option(option, runningIdx++))}
100+
</SelectGroup>
101+
)
102+
)
103+
104+
return groups
105+
} else {
106+
return this.props.options.map((option, idx) => this.option(option, idx))
107+
}
108+
}
109+
67110
public render() {
68111
return (
69112
<Select
@@ -72,23 +115,13 @@ export default class PatternFlySelect extends React.PureComponent<Props, State>
72115
variant={this.props.variant}
73116
typeAheadAriaLabel="Select from the Options"
74117
selections={this.state.selected}
118+
isGrouped={isGrouped(this.props)}
75119
maxHeight={this.props.maxHeight}
76120
onToggle={this._onToggle}
77121
onSelect={this._onSelect}
78122
isDisabled={this.props.isDisabled}
79123
>
80-
{this.props.options.map((option, index) => (
81-
<SelectOption
82-
className="kui--select-option"
83-
data-value={option.label}
84-
key={index}
85-
value={option.label}
86-
isSelected={option.isSelected}
87-
description={option.description}
88-
onClick={this._onClicks[index]}
89-
isDisabled={option.isDisabled}
90-
/>
91-
))}
124+
{this.options()}
92125
</Select>
93126
)
94127
}

plugins/plugin-client-common/src/components/spi/Select/model.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ export type SelectOptions = {
2121
isDisabled?: boolean
2222
}
2323

24-
export type Props = {
25-
/** select options */
26-
options: SelectOptions[]
27-
24+
type BaseProps = {
2825
/** Variant of rendered Select */
2926
variant: 'single' | 'checkbox' | 'typeahead' | 'typeaheadmulti'
3027

@@ -50,4 +47,27 @@ export type Props = {
5047
isDisabled?: boolean
5148
}
5249

50+
type Divider = { divider: true }
51+
type Group = { label: string; options: SelectOptions[] }
52+
53+
type GroupedProps = BaseProps & {
54+
/** select groups */
55+
groups: (Divider | Group)[]
56+
}
57+
58+
export function isDivider(group: Divider | Group): group is Divider {
59+
return (group as Divider).divider === true
60+
}
61+
62+
type NonGroupedProps = BaseProps & {
63+
/** select options */
64+
options: SelectOptions[]
65+
}
66+
67+
type Props = GroupedProps | NonGroupedProps
68+
69+
export function isGrouped(props: Props): props is GroupedProps {
70+
return Array.isArray((props as GroupedProps).groups)
71+
}
72+
5373
export default Props

plugins/plugin-client-common/web/scss/components/Select/PatternFly.scss

+26-2
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,37 @@
2121
.kui--popover .kui--select {
2222
font-size: 0.75rem;
2323

24+
/** Groups with no label: don't show the empty label element */
25+
.pf-c-select__menu-group-title:empty {
26+
display: none;
27+
}
28+
29+
/** For some reason, PatternFly doesn't seem to get the ellipsis right here; @patternfly/[email protected] */
30+
.pf-c-select__menu-item-main {
31+
overflow: hidden;
32+
text-overflow: ellipsis;
33+
}
34+
2435
.pf-c-select__toggle-typeahead {
2536
font-size: 0.75rem;
2637
font-family: var(--font-sans-serif);
2738
}
2839

40+
.kui--select-option[aria-selected='true'] {
41+
font-weight: 500;
42+
color: var(--active-tab-color);
43+
}
44+
.pf-c-select__menu-item-description {
45+
font-weight: normal;
46+
}
47+
48+
.pf-c-select__menu-group-title,
49+
.pf-c-select__menu-item-icon {
50+
font-size: inherit;
51+
}
52+
2953
button {
30-
padding-top: 0.25em;
31-
padding-bottom: 0.25em;
54+
padding-top: 0.375em;
55+
padding-bottom: 0.375em;
3256
}
3357
}

plugins/plugin-kubectl/components/src/CurrentContext.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export default class CurrentContext extends React.PureComponent<{}, State> {
163163
const options = this.state.allContexts.map(context => ({
164164
label: this.renderName(context.metadata.name),
165165
isSelected: context.spec.isCurrent,
166+
description: context.spec.isCurrent ? strings('This is your current context') : undefined,
166167
command: `kubectl config use-context ${encodeComponent(context.metadata.name)}`
167168
}))
168169

plugins/plugin-kubectl/components/src/CurrentNamespace.tsx

+29-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import React from 'react'
1818

1919
import { Icons, ViewLevel, Select, TextWithIconWidget } from '@kui-shell/plugin-client-common'
20+
2021
import {
2122
i18n,
2223
eventChannelUnsafe,
@@ -29,13 +30,16 @@ import {
2930
unwireToStandardEvents,
3031
inBrowser
3132
} from '@kui-shell/core'
33+
3234
import {
3335
KubeContext,
3436
getCurrentDefaultNamespace,
3537
onKubectlConfigChangeEvents,
3638
offKubectlConfigChangeEvents
3739
} from '@kui-shell/plugin-kubectl'
3840

41+
import { isInternalNamespace } from '@kui-shell/plugin-kubectl/heuristics'
42+
3943
interface State {
4044
currentNamespace: string
4145
allNamespaces: string[]
@@ -165,12 +169,31 @@ export default class CurrentNamespace extends React.PureComponent<{}, State> {
165169
)
166170
}
167171

172+
/** @return the options model for the given namespace named `ns` */
173+
private optionFor(ns: string) {
174+
const isSelected = ns === this.state.currentNamespace
175+
176+
return {
177+
label: ns,
178+
isSelected,
179+
description: isSelected ? strings('This is your current namespace') : undefined,
180+
command: `kubectl config set-context --current --namespace=${ns}`
181+
}
182+
}
183+
168184
private switchNamespace() {
169-
const options = this.state.allNamespaces.map(namespace => ({
170-
label: namespace,
171-
isSelected: namespace === this.state.currentNamespace,
172-
command: `kubectl config set-context --current --namespace=${namespace}`
173-
}))
185+
const internalNs = this.state.allNamespaces.filter(_ => isInternalNamespace[_]).map(_ => this.optionFor(_))
186+
const regularNs = this.state.allNamespaces.filter(_ => !isInternalNamespace[_]).map(_ => this.optionFor(_))
187+
188+
const options = internalNs.length > 0 ? undefined : regularNs
189+
const groups =
190+
internalNs.length === 0
191+
? undefined
192+
: [
193+
{ label: '', options: regularNs },
194+
{ divider: true as const },
195+
{ label: strings('System Namespaces'), options: internalNs }
196+
]
174197

175198
return (
176199
<React.Suspense fallback={<div />}>
@@ -180,6 +203,7 @@ export default class CurrentNamespace extends React.PureComponent<{}, State> {
180203
className="small-top-pad"
181204
selected={this.state.currentNamespace}
182205
options={options}
206+
groups={groups}
183207
isOpen
184208
isClosable={false}
185209
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@kui-shell/plugin-kubectl-heuristics",
3+
"version": "1.0.1",
4+
"description": "Heuristics for plugin-kubectl",
5+
"author": "Nick Mitchell",
6+
"license": "Apache-2.0",
7+
"keywords": [
8+
"kui",
9+
"plugin",
10+
"sample",
11+
"example"
12+
],
13+
"main": "dist/index.js",
14+
"module": "mdist/index.js",
15+
"types": "mdist/index.d.ts",
16+
"publishConfig": {
17+
"access": "public"
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2021 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export default ['calico-system']
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2021 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export default ['ibm-cert-store', 'ibm-operators', 'ibm-system']
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2021 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* This heuristic exports a map from Namespace-as-string to `true`
19+
* that indicates whether the given-named namespace is a
20+
* system/internal namespace.
21+
*
22+
* To extend, either add a new namespace name to one of the existing
23+
* files, or create a new file and add it to the `isInternal` array below
24+
*
25+
*/
26+
27+
import ibm from './ibm'
28+
import kube from './kube'
29+
import istio from './istio'
30+
import calico from './calico'
31+
import knative from './knative'
32+
import openshift from './openshift'
33+
import operators from './operators'
34+
35+
/** Make sure to add any new files to this array */
36+
const isInternal: string[] = [...ibm, ...kube, ...istio, ...calico, ...knative, ...openshift, ...operators]
37+
38+
const isInternalMap = isInternal.reduce((M, ns) => {
39+
M[ns] = true
40+
return M
41+
}, {} as Record<string, true>)
42+
43+
export default isInternalMap
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2021 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export default ['istio-system']
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2021 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export default ['knative-serving']

0 commit comments

Comments
 (0)