Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kubernetes): Raw resources UI MVP #8800

Merged
merged 6 commits into from
Jan 11, 2021
Merged

Conversation

jeffherald
Copy link
Contributor

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.

rrv
(Note: Account names and namespaces have been removed to protect the innocent)

@spinnakerbot
Copy link
Contributor

The following commits need their title changed:

  • 0141cb0: Raw resources view POC

  • 08e0d7b: WIP commit before move

  • ea1c91b: Mostly working Raw Resource Details

  • be5085c: Mostly working raw resource details

  • 9f486ac: Add enabling/disabling raw resources module by K8S_RAW_RESOURCES_ENABLED env var

  • 68d78b2: Add css for clickable-row

  • dc47351: Add kubernetes logo and minor clean-up

  • ac81c7f: Remove inline styles

  • 6e7f48c: Display (global) when a resource has no namespace

  • 569ef1f: Fix manifest path for global resources

  • 08672e5: Fix for global namespace filtering

Please format your commit title into the form:

<type>(<scope>): <subject>, e.g. fix(kubernetes): address NPE in status check

This allows us to easily generate changelogs & determine semantic version numbers when cutting releases. You can read more about commit conventions here.

@rbadillo
Copy link

rbadillo commented Dec 15, 2020

@ezimanyi This is the PR for Deck for the work @julienduchesne did here:
spinnaker/clouddriver#5057
spinnaker/gate#1384

@ezimanyi
Copy link
Contributor

@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!

@rbadillo
Copy link

Hi @german-muzquiz This is the PR for Deck for the work @julienduchesne did here:
spinnaker/clouddriver#5057
spinnaker/gate#1384

Can you help us, please?

Copy link
Contributor

@german-muzquiz german-muzquiz left a 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.

@jeffherald
Copy link
Contributor Author

jeffherald commented Dec 20, 2020

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.

@german-muzquiz
Copy link
Contributor

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));
Copy link
Contributor

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">
Copy link
Contributor

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> {
Copy link
Contributor

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))
Copy link
Contributor

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)?

Comment on lines 94 to 100
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);
Copy link
Contributor

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 {
Copy link
Contributor

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 {
   ...
 }

 ...
}

Comment on lines 46 to 47
Overridable('rawResource.details');
export class RawResourceDetails extends React.Component<IRawResourceDetailsProps, IRawResourcesDetailState> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);
Copy link
Contributor

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:

Suggested change
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) => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{get(manifest, ['manifest', 'status', 'conditions'], []).map((condition) => (
{(manifest?.manifest?.status?.conditions ?? []).map((condition) => (

Comment on lines 39 to 47
{...Object.entries(groups).map((entry) => {
return entry[0] !== 'undefined' ? (
<RawResourceGroup title={this.groupTitle(entry[0])}>
{...entry[1].map((resource) => <RawResource resource={resource}></RawResource>)}
</RawResourceGroup>
) : (
<></>
);
})}
Copy link
Contributor

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

Suggested change
{...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>)}
Copy link
Contributor

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> =>
Copy link
Contributor

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 });
Copy link
Contributor

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.

Suggested change
this.subcribers.push({ notify: subcriber });
const subscriber = { notify: subscriber };
this.subcribers.push(subscriber);
return () => this.subscribers = this.subscribers.filter(x => x !== subscriber);

Comment on lines 3 to 5
export interface ISubcriber {
notify(message: IK8sResourcesFiltersState): void;
}
Copy link
Contributor

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));
Copy link
Contributor

@christopherthielen christopherthielen Jan 4, 2021

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.

Suggested change
.then((kubernetesResources: IApiKubernetesResource[]) => $q.resolve(kubernetesResources));

Copy link
Contributor

@christopherthielen christopherthielen left a 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

@jeffherald
Copy link
Contributor Author

@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!

Copy link
Contributor

@christopherthielen christopherthielen left a 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() });
Copy link
Contributor

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

Suggested change
this.setState({ ...this.state, groupBy: groupBy.toLowerCase() });
this.setState({ groupBy: groupBy.toLowerCase() });

Comment on lines 65 to 80
<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} />
)}
/>
);
}}
/>
Copy link
Contributor

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.

Suggested change
<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} />
)}
/>

Comment on lines 94 to 102
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);
Copy link
Contributor

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

Suggested change
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) {
Copy link
Contributor

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.

Comment on lines 19 to 31
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,
};
Copy link
Contributor

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:

Suggested change
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">
Copy link
Contributor

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?

Comment on lines 111 to 112
const name = get(manifest, ['manifest', 'metadata', 'name'], null);
const kind = get(manifest, ['manifest', 'kind']);
Copy link
Contributor

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

Suggested change
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;

@christopherthielen christopherthielen added the ready to merge Reviewed and ready for merge label Jan 11, 2021
@mergify mergify bot merged commit c7eb9f4 into spinnaker:master Jan 11, 2021
vigneshm added a commit that referenced this pull request Jan 13, 2021
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)
vigneshm added a commit that referenced this pull request Jan 13, 2021
c7eb9f4 feat(kubernetes): Raw resources UI MVP (#8800)
a59e0f5 fix(kubernetes): Fix details link on pipeline deploy status for global resources (#8802)
mergify bot pushed a commit that referenced this pull request Jan 13, 2021
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)
mergify bot pushed a commit that referenced this pull request Jan 13, 2021
c7eb9f4 feat(kubernetes): Raw resources UI MVP (#8800)
a59e0f5 fix(kubernetes): Fix details link on pipeline deploy status for global resources (#8802)
paragbhingre pushed a commit to paragbhingre/deck that referenced this pull request Jan 21, 2021
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)
paragbhingre pushed a commit to paragbhingre/deck that referenced this pull request Jan 21, 2021
c7eb9f4 feat(kubernetes): Raw resources UI MVP (spinnaker#8800)
a59e0f5 fix(kubernetes): Fix details link on pipeline deploy status for global resources (spinnaker#8802)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready to merge Reviewed and ready for merge target-release/1.25
Projects
None yet
6 participants