-
Notifications
You must be signed in to change notification settings - Fork 900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(kubernetes): Raw resources UI MVP #8800
Conversation
The following commits need their title changed:
Please format your commit title into the form:
This allows us to easily generate changelogs & determine semantic version numbers when cutting releases. You can read more about commit conventions here. |
@ezimanyi This is the PR for Deck for the work @julienduchesne did here: |
@rbadillo : As I'm stepping away from the Spinnaker project I'll let the UI SIG and/or Kubernetes SIG take a look at this! Thanks! |
Hi @german-muzquiz This is the PR for Deck for the work @julienduchesne did here: Can you help us, please? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for putting this!
Some usability feedback:
- The number of accounts can be very large in some installations, it could help if instead of the traditional list of account checkboxes there's an autocomplete multiselect text box, probably similar to https://react-select.com/home
- The
Kind
section could be grouped by category in a similar way that Lens groups kinds. This is the replacement for spinnaker's "Clusters/Load Balancers/Firewalls" categories of VM world, so kubernetes has its own categories. - Similarly,
Namespaces
could use the autocomplete multiselect text box.
Question: How often is the raw resources endpoint being called, is it on demand, periodically?
I don't have much experience with react to actually merge a PR of this size, so I'll defer to someone more familiarized with the code.
Hi German, Thanks for taking a look. While we agree that it's not from perfect in terms of UX, we viewed this as an MVP that could be a starting point for those in the community with more expertise in UX and front-end dev could iterate on in the form of enhancements after this PR has been accepted. As far as your question, I believe the endpoint is polled every 30 seconds. |
804a1a8
to
72a834b
Compare
Makes sense, appreciate the contribution. My only concern is in the load imposed on the server using the unfiltered endpoint, but I think polling every 30 seconds is ok. |
rawResources: [], | ||
}; | ||
|
||
this.filterPubSub.subscribe(this.onFilterChange.bind(this)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you need to unusubscribe?
return ( | ||
<div style={{ width: '100%' }}> | ||
<form className="form-inline" style={{ marginBottom: '5px' }}> | ||
<div className="form-group"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use the FormikFormField
(preferred) or FormField
(not preferred, but better than raw html) and SelectInput
instead?
rawResources: IApiKubernetesResource[]; | ||
} | ||
|
||
export class K8sResources extends React.Component<IK8sResourcesProps, IK8sResourcesState> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocker by any means, but we are generally trying to use functional React components instead of class components going forward.
<div className="content"> | ||
<FilterSection heading={'Kind'} expanded={true}> | ||
{...Object.keys(this.state.kinds) | ||
.sort((a, b) => (a.toLowerCase() > b.toLowerCase() ? 1 : -1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a.localeCompare(b)
?
for (const p in this.state.displayNamespaces) { | ||
if (p == RawResourceUtils.GLOBAL_LABEL) { | ||
this.state.namespaces[''] = this.state.displayNamespaces[p]; | ||
} | ||
this.state.namespaces[p] = this.state.displayNamespaces[p]; | ||
} | ||
this.setState(this.state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend against mutation of the state
object
@@ -0,0 +1,81 @@ | |||
.raw-resource-card { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please scope all CSS using a selector with the same name as the React component it is intended for, i.e.:
.RawResource {
.card {
...
}
.column {
...
}
...
}
Overridable('rawResource.details'); | ||
export class RawResourceDetails extends React.Component<IRawResourceDetailsProps, IRawResourcesDetailState> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overridable('rawResource.details'); | |
export class RawResourceDetails extends React.Component<IRawResourceDetailsProps, IRawResourcesDetailState> { | |
@Overridable('k8s.rawResource.details') | |
export class RawResourceDetails extends React.Component<IRawResourceDetailsProps, IRawResourcesDetailState> { |
|
||
public render() { | ||
const { account, region, manifest } = this.state; | ||
const kind = get(manifest, ['manifest', 'kind'], null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If desired, type safety can be retained by writing these as:
const kind = get(manifest, ['manifest', 'kind'], null); | |
const kind = manifest?.manifest?.kind; |
</CollapsibleSection> | ||
<CollapsibleSection key="status" heading="status" defaultExpanded={true}> | ||
<ul> | ||
{get(manifest, ['manifest', 'status', 'conditions'], []).map((condition) => ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{get(manifest, ['manifest', 'status', 'conditions'], []).map((condition) => ( | |
{(manifest?.manifest?.status?.conditions ?? []).map((condition) => ( |
{...Object.entries(groups).map((entry) => { | ||
return entry[0] !== 'undefined' ? ( | ||
<RawResourceGroup title={this.groupTitle(entry[0])}> | ||
{...entry[1].map((resource) => <RawResource resource={resource}></RawResource>)} | ||
</RawResourceGroup> | ||
) : ( | ||
<></> | ||
); | ||
})} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can destructure to variables with nice names
{...Object.entries(groups).map((entry) => { | |
return entry[0] !== 'undefined' ? ( | |
<RawResourceGroup title={this.groupTitle(entry[0])}> | |
{...entry[1].map((resource) => <RawResource resource={resource}></RawResource>)} | |
</RawResourceGroup> | |
) : ( | |
<></> | |
); | |
})} | |
{...Object.entries(groups).map(([key, value]) => { | |
return key !== 'undefined' ? ( | |
<RawResourceGroup title={this.groupTitle(key)}> | |
{...value.map((resource) => <RawResource resource={resource}></RawResource>)} | |
</RawResourceGroup> | |
) : ( | |
<></> | |
); | |
})} |
{...Object.entries(groups).map((entry) => { | ||
return entry[0] !== 'undefined' ? ( | ||
<RawResourceGroup title={this.groupTitle(entry[0])}> | ||
{...entry[1].map((resource) => <RawResource resource={resource}></RawResource>)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure you need a key
on each, i.e.: <RawResource key={something}/>
|
||
type ApiK8sResource = any; | ||
|
||
const fetchK8sResources = ($q: IQService) => (application: Application): IPromise<ApiK8sResource> => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are trying to clean the codebase of IPromise
, use PromiseLike
instead
} | ||
|
||
public subscribe(subcriber: (message: IK8sResourcesFiltersState) => void) { | ||
this.subcribers.push({ notify: subcriber }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a memory leak.
this.subcribers.push({ notify: subcriber }); | |
const subscriber = { notify: subscriber }; | |
this.subcribers.push(subscriber); | |
return () => this.subscribers = this.subscribers.filter(x => x !== subscriber); |
export interface ISubcriber { | ||
notify(message: IK8sResourcesFiltersState): void; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably drop this interface and put the functions directly in the subscribers
array.
You can type it like so:
type ISubscriber = (message: IK8sResourcesFiltersState) => void;
Then when pushing to the list:
public subscribe(subcriber: ISubscriber) {
this.subcribers.push(subcriber);
return () => this.subscribers = this.subscribers.filter(x => x !== subscriber);
REST('applications') | ||
.path(application.name, 'rawResources') | ||
.get() | ||
.then((kubernetesResources: IApiKubernetesResource[]) => $q.resolve(kubernetesResources)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems redundant. What is the purpose? Also we are trying to avoid using $q
where possible, especially in react components.
.then((kubernetesResources: IApiKubernetesResource[]) => $q.resolve(kubernetesResources)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I requested some changes inline and offered some style advice
Address PR feedback from @christopherthielen
@christopherthielen Thank you for the feedback. I think it's all been addressed, please let me know if anything else is needed to get it merged. Cheers! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for addressing my initial feedback. This looks much better. I've added some more suggestions which are (except for the Formik comment) mostly style or sugar suggestions. Please address at leastthe FormField
feedback and then this LGTM
} | ||
|
||
public groupByChanged = (groupBy: string): void => { | ||
this.setState({ ...this.state, groupBy: groupBy.toLowerCase() }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...this.state,
is not necessary
this.setState({ ...this.state, groupBy: groupBy.toLowerCase() }); | |
this.setState({ groupBy: groupBy.toLowerCase() }); |
<Formik | ||
onSubmit={() => null} | ||
initialValues={{ groupBy: 'None' }} | ||
render={() => { | ||
return ( | ||
<FormikFormField | ||
onChange={this.groupByChanged} | ||
name="groupBy" | ||
label="Group By" | ||
input={(props) => ( | ||
<ReactSelectInput {...props} inputClassName="groupby" stringOptions={opts} clearable={false} /> | ||
)} | ||
/> | ||
); | ||
}} | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either use Formik or local state to manage state, but not both.
<Formik | |
onSubmit={() => null} | |
initialValues={{ groupBy: 'None' }} | |
render={() => { | |
return ( | |
<FormikFormField | |
onChange={this.groupByChanged} | |
name="groupBy" | |
label="Group By" | |
input={(props) => ( | |
<ReactSelectInput {...props} inputClassName="groupby" stringOptions={opts} clearable={false} /> | |
)} | |
/> | |
); | |
}} | |
/> | |
<FormField | |
onChange={this.groupByChanged} | |
value={this.state.groupBy} | |
label="Group By" | |
input={(props) => ( | |
<ReactSelectInput {...props} inputClassName="groupby" stringOptions={opts} clearable={false} /> | |
)} | |
/> |
const st = { ...this.state }; | ||
for (const p in st.displayNamespaces) { | ||
if (p == RawResourceUtils.GLOBAL_LABEL) { | ||
st.namespaces[''] = st.displayNamespaces[p]; | ||
} | ||
st.namespaces[p] = st.displayNamespaces[p]; | ||
} | ||
this.setState(st); | ||
this.filterPubSub.publish(this.state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more idiomatic to setState with only the key(s) being changed
const st = { ...this.state }; | |
for (const p in st.displayNamespaces) { | |
if (p == RawResourceUtils.GLOBAL_LABEL) { | |
st.namespaces[''] = st.displayNamespaces[p]; | |
} | |
st.namespaces[p] = st.displayNamespaces[p]; | |
} | |
this.setState(st); | |
this.filterPubSub.publish(this.state); | |
const namespaces = { ...this.state.namespaces } | |
for (const p in st.displayNamespaces) { | |
if (p == RawResourceUtils.GLOBAL_LABEL) { | |
namespaces[''] = st.displayNamespaces[p]; | |
} | |
namespaces[p] = st.displayNamespaces[p]; | |
} | |
this.setState({ namespaces }); | |
this.filterPubSub.publish(this.state); |
} | ||
} | ||
|
||
public publish(message: IK8sResourcesFiltersState) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd recommend using a different interface to type message
which contains only the data that is relevant to subscribers. This avoids leaking internal component state and abstractions.
If subscribers need everything in IK8sResourcesFiltersState
then disregard; this is fine.
const key = | ||
this.props.resource.account + | ||
'-' + | ||
this.props.resource.namespace + | ||
'-' + | ||
this.props.resource.kind + | ||
'-' + | ||
this.props.resource.displayName; | ||
const params = { | ||
account: this.props.resource.account, | ||
name: this.props.resource.name, | ||
region: this.props.resource.region, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A little syntax sugar, in case you didn't know it:
const key = | |
this.props.resource.account + | |
'-' + | |
this.props.resource.namespace + | |
'-' + | |
this.props.resource.kind + | |
'-' + | |
this.props.resource.displayName; | |
const params = { | |
account: this.props.resource.account, | |
name: this.props.resource.name, | |
region: this.props.resource.region, | |
}; | |
const { account, name, region, namespace, kind, displayName } = this.props.resource; | |
const key = `${account}-${namespace}-${kind}-${displayName}; | |
const params = { account, name, region }; |
return ( | ||
<UISrefActive class="active"> | ||
<UISref to=".rawResourceDetails" params={params}> | ||
<div id={key} className="RawResource card clickable clickable-row"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is only a curiosity: does this id
get used somewhere?
const name = get(manifest, ['manifest', 'metadata', 'name'], null); | ||
const kind = get(manifest, ['manifest', 'kind']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This typescript syntax sugar will retain type information. I recommend using typescript optional chaining instead of lodash .get()
in most scenarios for this reason
const name = get(manifest, ['manifest', 'metadata', 'name'], null); | |
const kind = get(manifest, ['manifest', 'kind']); | |
const name = manifest?.manifest?.metadata?.name ?? null; | |
const kind = manifest?.manifest?.kind; |
Address final PR feedback from @christopherthielen
95eacfb refactor(core/deployment): Update redblack fields without force updating (#8840) a027d62 fix(gitlab): fix help text for gitlab artifacts 8858746 feat(md): waiting status (#8836) c7eb9f4 feat(kubernetes): Raw resources UI MVP (#8800) ef87a9e fix(core/executions): Update migrated status to match API (#8831) 3a51af2 fix(core/deploymentStrategy): do not show highlander preview in deploy stage config (only show in clone dialog) 8be06a0 feat(core/deploymentStrategy): Add a preview for Highlander deploys c32c91a fix(serverGroup): Increase the timeout for api request (#8812) 29a85a0 feat(core/executions): Render newly migrated execution groups (#8807) 8f291c0 fix(core/projects): Fix duplicate Projects appearing in recent history (on search screen) (#8806)
95eacfb refactor(core/deployment): Update redblack fields without force updating (#8840) a027d62 fix(gitlab): fix help text for gitlab artifacts 8858746 feat(md): waiting status (#8836) c7eb9f4 feat(kubernetes): Raw resources UI MVP (#8800) ef87a9e fix(core/executions): Update migrated status to match API (#8831) 3a51af2 fix(core/deploymentStrategy): do not show highlander preview in deploy stage config (only show in clone dialog) 8be06a0 feat(core/deploymentStrategy): Add a preview for Highlander deploys c32c91a fix(serverGroup): Increase the timeout for api request (#8812) 29a85a0 feat(core/executions): Render newly migrated execution groups (#8807) 8f291c0 fix(core/projects): Fix duplicate Projects appearing in recent history (on search screen) (#8806)
95eacfb refactor(core/deployment): Update redblack fields without force updating (spinnaker#8840) a027d62 fix(gitlab): fix help text for gitlab artifacts 8858746 feat(md): waiting status (spinnaker#8836) c7eb9f4 feat(kubernetes): Raw resources UI MVP (spinnaker#8800) ef87a9e fix(core/executions): Update migrated status to match API (spinnaker#8831) 3a51af2 fix(core/deploymentStrategy): do not show highlander preview in deploy stage config (only show in clone dialog) 8be06a0 feat(core/deploymentStrategy): Add a preview for Highlander deploys c32c91a fix(serverGroup): Increase the timeout for api request (spinnaker#8812) 29a85a0 feat(core/executions): Render newly migrated execution groups (spinnaker#8807) 8f291c0 fix(core/projects): Fix duplicate Projects appearing in recent history (on search screen) (spinnaker#8806)
c7eb9f4 feat(kubernetes): Raw resources UI MVP (spinnaker#8800) a59e0f5 fix(kubernetes): Fix details link on pipeline deploy status for global resources (spinnaker#8802)
This PR adds a UI module to view/manage resources returned by the raw resources controller added by @julienduchesne in spinnaker/clouddriver#5057, which can be enabled by setting the K8S_RAW_RESOURCES_ENABLED environment variable to
true
when running deck.(Note: Account names and namespaces have been removed to protect the innocent)