Skip to content

Commit

Permalink
feat(kubernetes): Raw resources UI MVP (spinnaker#8800)
Browse files Browse the repository at this point in the history
* feat(kubernetes): Raw resources UI MVP

* feat(kubernetes): Raw resources UI MVP

Address PR feedback from @christopherthielen

* feat(kubernetes): Raw resources UI MVP

    Address final PR feedback from @christopherthielen

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
jeffherald and mergify[bot] committed Jan 11, 2021
1 parent a59e0f5 commit c7eb9f4
Show file tree
Hide file tree
Showing 23 changed files with 888 additions and 309 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/core/src/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface IFeatures {
snapshots?: boolean;
savePipelinesStageEnabled?: boolean;
functions?: boolean;
kubernetesRawResources?: boolean;
}

export interface IDockerInsightSettings {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { ReactComponent as spMenuCanaryConfig } from './vectors/spMenuCanaryConf
import { ReactComponent as spMenuCanaryReport } from './vectors/spMenuCanaryReport.svg';
import { ReactComponent as spMenuClusters } from './vectors/spMenuClusters.svg';
import { ReactComponent as spMenuConfig } from './vectors/spMenuConfig.svg';
import { ReactComponent as spMenuK8s } from './vectors/spMenuK8s.svg';
import { ReactComponent as spMenuLoadBalancers } from './vectors/spMenuLoadBalancers.svg';
import { ReactComponent as spMenuMeme } from './vectors/spMenuMeme.svg';
import { ReactComponent as spMenuPager } from './vectors/spMenuPager.svg';
Expand Down Expand Up @@ -215,6 +216,7 @@ export const iconsByName = {
spMenuCanaryReport,
spMenuClusters,
spMenuConfig,
spMenuK8s,
spMenuLoadBalancers,
spMenuMeme,
spMenuPager,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/scripts/modules/kubernetes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './serverGroup';
export * from './securityGroup';
export * from './manifest';
export * from './loadBalancer';
export * from './rawResource';
19 changes: 16 additions & 3 deletions app/scripts/modules/kubernetes/src/kubernetes.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { module } from 'angular';

import { CloudProviderRegistry, STAGE_ARTIFACT_SELECTOR_COMPONENT_REACT, YAML_EDITOR_COMPONENT } from '@spinnaker/core';
import {
CloudProviderRegistry,
STAGE_ARTIFACT_SELECTOR_COMPONENT_REACT,
SETTINGS,
YAML_EDITOR_COMPONENT,
} from '@spinnaker/core';

import { KUBERNETES_MANIFEST_DELETE_CTRL } from './manifest/delete/delete.controller';
import { KUBERNETES_MANIFEST_SCALE_CTRL } from './manifest/scale/scale.controller';
Expand Down Expand Up @@ -38,6 +43,8 @@ import { KUBERNETES_DISABLE_MANIFEST_STAGE } from './pipelines/stages/traffic/di
import { KubernetesSecurityGroupReader } from './securityGroup/securityGroup.reader';
import { KUBERNETES_ROLLING_RESTART } from './manifest/rollout/RollingRestart';

import { KUBERNETS_RAW_RESOURCE_MODULE } from './rawResource';

import kubernetesLogo from './logo/kubernetes.logo.svg';

import './validation/applicationName.validator';
Expand All @@ -54,7 +61,7 @@ templates.keys().forEach(function (key) {

export const KUBERNETES_MODULE = 'spinnaker.kubernetes';

module(KUBERNETES_MODULE, [
const requires = [
KUBERNETES_INSTANCE_DETAILS_CTRL,
KUBERNETES_LOAD_BALANCER_DETAILS_CTRL,
KUBERNETES_SECURITY_GROUP_DETAILS_CTRL,
Expand Down Expand Up @@ -91,7 +98,13 @@ module(KUBERNETES_MODULE, [
KUBERNETES_DISABLE_MANIFEST_STAGE,
STAGE_ARTIFACT_SELECTOR_COMPONENT_REACT,
KUBERNETES_ROLLING_RESTART,
]).config(() => {
];

if (SETTINGS.feature.kubernetesRawResources) {
requires.push(KUBERNETS_RAW_RESOURCE_MODULE);
}

module(KUBERNETES_MODULE, requires).config(() => {
CloudProviderRegistry.registerProvider('kubernetes', {
name: 'Kubernetes',
logo: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.K8sResources {
width: 100%;
.StandardFieldLayout {
width: 25%;
.StandardFieldLayout_Label {
min-width: 72px;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Application, ApplicationDataSource, FormField, ReactSelectInput } from '@spinnaker/core';
import React from 'react';
import { FiltersPubSub } from '../controller/FiltersPubSub';
import { KUBERNETS_RAW_RESOURCE_DATA_SOURCE_KEY } from '../rawResource.dataSource';
import { RawResource } from './group/RawResource';
import { RawResourceGroups } from './group/RawResourceGroups';
import { IK8sResourcesFiltersState } from './K8sResourcesFilters';
import './K8sResources.less';
import { RawResourceUtils } from './RawResourceUtils';

export interface IK8sResourcesProps {
app: Application;
}

interface IK8sResourcesState {
groupBy: string;
filters: IK8sResourcesFiltersState;
rawResources: IApiKubernetesResource[];
}

export class K8sResources extends React.Component<IK8sResourcesProps, IK8sResourcesState> {
private dataSource: ApplicationDataSource<IApiKubernetesResource[]>;
private filterPubSub: FiltersPubSub = FiltersPubSub.getInstance(this.props.app.name);
private sub = this.onFilterChange.bind(this);

constructor(props: IK8sResourcesProps) {
super(props);
this.dataSource = this.props.app.getDataSource(KUBERNETS_RAW_RESOURCE_DATA_SOURCE_KEY);
this.state = {
groupBy: 'none',
filters: null,
rawResources: [],
};

this.filterPubSub.subscribe(this.sub);
}

public componentWillUnmount() {
this.filterPubSub.unsubscribe(this.sub);
}

public onFilterChange(message: IK8sResourcesFiltersState) {
this.setState({ ...this.state, filters: message });
}

public async componentDidMount() {
await this.dataSource.ready();

this.setState({
...this.state,
groupBy: this.state.groupBy,
rawResources: await this.dataSource.data.sort((a, b) => a.name.localeCompare(b.name)),
});
}

private groupByChanged = (e: React.ChangeEvent<any>): void => {
this.setState({ groupBy: e.target.value.toLowerCase() });
};

public render() {
const opts = ['None', 'Account', 'Kind', 'Namespace'];
return (
<div className="K8sResources">
<div className="header row">
<FormField
onChange={this.groupByChanged}
value={this.state.groupBy.charAt(0).toUpperCase() + this.state.groupBy.substr(1)}
name="groupBy"
label="Group By"
input={(props) => (
<ReactSelectInput {...props} inputClassName="groupby" stringOptions={opts} clearable={false} />
)}
/>
</div>
<div className="content">
{this.state.groupBy === 'none' ? (
<>
{...this.state.rawResources
.filter((resource) => this.matchFilters(resource))
.map((resource) => (
<RawResource key={RawResourceUtils.resourceKey(resource)} resource={resource}></RawResource>
))}
</>
) : (
<RawResourceGroups
resources={this.state.rawResources.filter((resource) => this.matchFilters(resource))}
groupBy={this.state.groupBy}
></RawResourceGroups>
)}
</div>
</div>
);
}

private matchFilters(resource: IApiKubernetesResource) {
if (this.state.filters == null) {
return true;
}
const accountMatch =
Object.values(this.state.filters.accounts).every((x) => !x) || this.state.filters.accounts[resource.account];
const kindMatch =
Object.values(this.state.filters.kinds).every((x) => !x) || this.state.filters.kinds[resource.kind];
const namespaceMatch =
Object.values(this.state.filters.namespaces).every((x) => !x) ||
this.state.filters.namespaces[resource.namespace];
return accountMatch && kindMatch && namespaceMatch;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Application, ApplicationDataSource, FilterSection, FilterCheckbox } from '@spinnaker/core';
import React from 'react';
import { FiltersPubSub } from '../controller/FiltersPubSub';
import { RawResourceUtils } from './RawResourceUtils';
import { KUBERNETS_RAW_RESOURCE_DATA_SOURCE_KEY } from '../rawResource.dataSource';

export interface IK8sResourcesFiltersProps {
app: Application;
}

export interface IK8sResourcesFiltersState {
accounts: Record<string, boolean>;
kinds: Record<string, boolean>;
namespaces: Record<string, boolean>;
displayNamespaces: Record<string, boolean>;
}

export class K8sResourcesFilters extends React.Component<IK8sResourcesFiltersProps, IK8sResourcesFiltersState> {
private dataSource: ApplicationDataSource<IApiKubernetesResource[]>;
private filterPubSub: FiltersPubSub = FiltersPubSub.getInstance(this.props.app.name);

constructor(props: IK8sResourcesFiltersProps) {
super(props);
this.dataSource = this.props.app.getDataSource(KUBERNETS_RAW_RESOURCE_DATA_SOURCE_KEY);

this.state = {
accounts: {},
kinds: {},
namespaces: {},
displayNamespaces: {},
};
}

public async componentDidMount() {
await this.dataSource.ready();
const ns = Object.assign({}, ...this.dataSource.data.map((resource) => ({ [resource.namespace]: false })));
const displayNs = { ...ns };
if ('' in displayNs) {
delete displayNs[''];
displayNs[RawResourceUtils.GLOBAL_LABEL] = false;
}
this.setState({
accounts: Object.assign({}, ...this.dataSource.data.map((resource) => ({ [resource.account]: false }))),
kinds: Object.assign({}, ...this.dataSource.data.map((resource) => ({ [resource.kind]: false }))),
namespaces: ns,
displayNamespaces: displayNs,
});
}

public render() {
return (
<div className="content">
<FilterSection heading={'Kind'} expanded={true}>
{...Object.keys(this.state.kinds)
.sort((a, b) => a.localeCompare(b))
.map((key) => (
<FilterCheckbox
heading={key}
key={key}
sortFilterType={this.state.kinds}
onChange={this.onCheckbox.bind(this)}
></FilterCheckbox>
))}
</FilterSection>
<FilterSection heading={'Account'} expanded={true}>
{...Object.keys(this.state.accounts)
.sort((a, b) => a.localeCompare(b))
.map((key) => (
<FilterCheckbox
heading={key}
key={key}
sortFilterType={this.state.accounts}
onChange={this.onCheckbox.bind(this)}
></FilterCheckbox>
))}
</FilterSection>
<FilterSection heading={'Namespace'} expanded={true}>
{...Object.keys(this.state.displayNamespaces)
.sort((a, b) => a.localeCompare(b))
.map((key) => (
<FilterCheckbox
heading={key}
key={key}
sortFilterType={this.state.displayNamespaces}
onChange={this.onNsCheckbox.bind(this)}
></FilterCheckbox>
))}
</FilterSection>
</div>
);
}

private onNsCheckbox() {
const { namespaces, displayNamespaces } = { ...this.state };
for (const p in displayNamespaces) {
if (p == RawResourceUtils.GLOBAL_LABEL) {
namespaces[''] = displayNamespaces[p];
}
namespaces[p] = displayNamespaces[p];
}
this.setState({ namespaces });
this.filterPubSub.publish(this.state);
}

private onCheckbox() {
this.setState(this.state);
this.filterPubSub.publish(this.state);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export class RawResourceUtils {
static GLOBAL_LABEL = '(global)';

static namespaceDisplayName(ns: string): string {
if (ns === null || ns === '') {
return RawResourceUtils.GLOBAL_LABEL;
}
return ns;
}

static resourceKey(resource: IApiKubernetesResource): string {
if (resource === null) {
return '';
}
return resource.namespace === ''
? resource.account + '-' + resource.kind + '-' + resource.name
: resource.account + '-' + resource.namespace + '-' + resource.kind + '-' + resource.name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';
import './RawResource.less';

interface IRawResourceGroupProps {
title: string;
resources?: IApiKubernetesResource[];
}

interface IRawResourceGroupState {
open: boolean;
}

export class RawResourceGroup extends React.Component<IRawResourceGroupProps, IRawResourceGroupState> {
constructor(props: IRawResourceGroupProps) {
super(props);

this.state = {
open: true,
};
}

public render() {
return (
<div className="RawResourceGroup">
<div className="clickable sticky-header header" onClick={this.onHeaderClick.bind(this)}>
<span className={`glyphicon pipeline-toggle glyphicon-chevron-${this.state.open ? 'down' : 'right'}`} />
<div className="shadowed">
<h4 className="group-title">{this.props.title}</h4>
</div>
</div>
<div className={`items${this.state.open ? '' : ' hidden'}`}>{this.props.children}</div>
</div>
);
}

private onHeaderClick() {
this.setState({
open: !this.state.open,
});
}
}
Loading

0 comments on commit c7eb9f4

Please sign in to comment.