cleanup Lens repo with tighter linting

Signed-off-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
Sebastian Malton committed 2020-07-09 17:00:23 -04:00
1 parent e468105143
commit b1ff34879a
470 files changed
+8447 -7443

No files matched your search

+1 -1
View File
@@ -1 +1 @@
export * from "./not-found"
export * from "./not-found";
@@ -3,13 +3,13 @@ import { Trans } from "@lingui/macro";
import { MainLayout } from "../layout/main-layout";
export class NotFound extends React.Component {
render() {
render(): JSX.Element {
return (
<MainLayout className="NotFound" contentClass="flex" footer={null}>
<p className="box center">
<Trans>Page not found</Trans>
</p>
</MainLayout>
)
);
}
}
@@ -16,6 +16,9 @@ import { createInstallChartTab } from "../dock/install-chart.store";
import { Badge } from "../badge";
import { _i18n } from "../../i18n";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const placeholder = require("./helm-placeholder.svg");
interface Props {
chart: HelmChart;
hideDetails(): void;
@@ -31,7 +34,9 @@ export class HelmChartDetails extends Component<Props> {
@disposeOnUnmount
chartSelector = autorun(async () => {
if (!this.props.chart) return;
if (!this.props.chart) {
return;
}
this.chartVersions = null;
this.selectedChart = null;
this.description = null;
@@ -43,42 +48,43 @@ export class HelmChartDetails extends Component<Props> {
});
});
loadChartData(version?: string) {
loadChartData(version?: string): void {
const { chart: { name, repo } } = this.props;
if (this.chartPromise) this.chartPromise.cancel();
if (this.chartPromise) {
this.chartPromise.cancel();
}
this.chartPromise = helmChartsApi.get(repo, name, version);
}
@autobind()
onVersionChange(opt: SelectOption) {
onVersionChange(opt: SelectOption): void {
const version = opt.value;
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
this.description = null;
this.loadChartData(version);
this.chartPromise.then(data => {
this.description = data.readme
this.description = data.readme;
});
}
@autobind()
install() {
install(): void {
createInstallChartTab(this.selectedChart);
this.props.hideDetails()
this.props.hideDetails();
}
renderIntroduction() {
renderIntroduction(): JSX.Element {
const { selectedChart, chartVersions, onVersionChange } = this;
const placeholder = require("./helm-placeholder.svg");
return (
<div className="introduction flex align-flex-start">
<img
className="intro-logo"
src={selectedChart.getIcon() || placeholder}
onError={(event) => event.currentTarget.src = placeholder}
src={selectedChart.icon || placeholder}
onError={(event: React.SyntheticEvent<HTMLImageElement, Event>): void => event.currentTarget.src = placeholder}
/>
<div className="intro-contents box grow">
<div className="description flex align-center justify-space-between">
{selectedChart.getDescription()}
{selectedChart.description}
<Button primary label={_i18n._(t`Install`)} onClick={this.install}/>
</div>
<DrawerItem name={_i18n._(t`Version`)} className="version" onClick={stopPropagation}>
@@ -86,16 +92,16 @@ export class HelmChartDetails extends Component<Props> {
themeName="outlined"
menuPortalTarget={null}
options={chartVersions.map(chart => chart.version)}
value={selectedChart.getVersion()}
value={selectedChart.version}
onChange={onVersionChange}
/>
</DrawerItem>
<DrawerItem name={_i18n._(t`Home`)}>
<a href={selectedChart.getHome()} target="_blank">{selectedChart.getHome()}</a>
<a href={selectedChart.home} target="_blank" rel="noreferrer">{selectedChart.home}</a>
</DrawerItem>
<DrawerItem name={_i18n._(t`Maintainers`)} className="maintainers">
{selectedChart.getMaintainers().map(({ name, email, url }) =>
<a key={name} href={url ? url : `mailto:${email}`} target="_blank">{name}</a>
<a key={name} href={url ? url : `mailto:${email}`} target="_blank" rel="noreferrer">{name}</a>
)}
</DrawerItem>
{selectedChart.getKeywords().length > 0 && (
@@ -108,8 +114,10 @@ export class HelmChartDetails extends Component<Props> {
);
}
renderContent() {
if (this.selectedChart === null || this.description === null) return <Spinner center/>;
renderContent(): JSX.Element {
if (this.selectedChart === null || this.description === null) {
return <Spinner center/>;
}
return (
<div className="box grow">
{this.renderIntroduction()}
@@ -120,7 +128,7 @@ export class HelmChartDetails extends Component<Props> {
);
}
render() {
render(): JSX.Element {
const { chart, hideDetails } = this.props;
const title = chart ? <Trans>Chart: {chart.getFullName()}</Trans> : "";
return (
@@ -2,51 +2,48 @@ import { observable } from "mobx";
import { autobind } from "../../utils";
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
import { ItemStore } from "../../item.store";
import flatten from "lodash/flatten"
import flatten from "lodash/flatten";
import compareVersions from 'compare-versions';
export interface IChartVersion {
export interface ChartVersion {
repo: string;
version: string;
}
@autobind()
export class HelmChartStore extends ItemStore<HelmChart> {
@observable versions = observable.map<string, IChartVersion[]>();
@observable versions = observable.map<string, ChartVersion[]>();
loadAll() {
return this.loadItems(() => helmChartsApi.list());
async loadAll(): Promise<void> {
await this.loadItems(() => helmChartsApi.list());
}
getByName(name: string, repo: string) {
return this.items.find(chart => chart.getName() === name && chart.getRepository() === repo);
getByName(desiredName: string, desiredRepo: string): HelmChart {
return this.items.find(({ name, repo }) => desiredName === name && desiredRepo === repo);
}
protected sortVersions = (versions: IChartVersion[]) => {
return versions.sort((first, second) => {
return compareVersions(second.version, first.version)
});
protected sortVersions = (versions: ChartVersion[]): ChartVersion[] => {
return versions.sort((first, second) => compareVersions(second.version, first.version));
};
async getVersions(chartName: string, force?: boolean): Promise<IChartVersion[]> {
async getVersions(chartName: string, force?: boolean): Promise<ChartVersion[]> {
let versions = this.versions.get(chartName);
if (versions && !force) {
return versions;
}
const loadVersions = (repo: string) => {
return helmChartsApi.get(repo, chartName).then(({ versions }) => {
return versions.map(chart => ({
repo: repo,
version: chart.getVersion()
}))
})
const loadVersions = async (repo: string): Promise<ChartVersion[]> => {
const { versions } = await helmChartsApi.get(repo, chartName);
return versions.map(({version}) => ({ repo, version, }));
};
if (!this.isLoaded) {
await this.loadAll();
}
const repos = this.items
.filter(chart => chart.getName() === chartName)
.map(chart => chart.getRepository());
.map(({repo}) => repo);
versions = await Promise.all(repos.map(loadVersions))
.then(flatten)
.then(this.sortVersions);
@@ -55,7 +52,7 @@ export class HelmChartStore extends ItemStore<HelmChart> {
return versions;
}
reset() {
reset(): void {
super.reset();
this.versions.clear();
}
@@ -1,14 +1,14 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { appsRoute } from "../+apps/apps.route";
import { buildURL } from "../../navigation";
export const helmChartsRoute: RouteProps = {
path: appsRoute.path + "/charts/:repo?/:chartName?"
}
};
export interface IHelmChartsRouteParams {
export interface HelmChartsRouteParams {
chartName?: string;
repo?: string;
}
export const helmChartsURL = buildURL<IHelmChartsRouteParams>(helmChartsRoute.path)
export const helmChartsURL = buildURL<HelmChartsRouteParams>(helmChartsRoute.path);
@@ -3,7 +3,7 @@ import "./helm-charts.scss";
import React, { Component } from "react";
import { RouteComponentProps } from "react-router";
import { observer } from "mobx-react";
import { helmChartsURL, IHelmChartsRouteParams } from "./helm-charts.route";
import { helmChartsURL, HelmChartsRouteParams } from "./helm-charts.route";
import { helmChartStore } from "./helm-chart.store";
import { HelmChart } from "../../api/endpoints/helm-charts.api";
import { HelmChartDetails } from "./helm-chart-details";
@@ -18,39 +18,38 @@ enum sortBy {
repo = "repo",
}
interface Props extends RouteComponentProps<IHelmChartsRouteParams> {
interface Props extends RouteComponentProps<HelmChartsRouteParams> {
}
@observer
export class HelmCharts extends Component<Props> {
componentDidMount() {
componentDidMount(): void {
helmChartStore.loadAll();
}
get selectedChart() {
const { match: { params: { chartName, repo } } } = this.props
get selectedChart(): HelmChart {
const { match: { params: { chartName, repo } } } = this.props;
return helmChartStore.getByName(chartName, repo);
}
showDetails = (chart: HelmChart) => {
showDetails = (chart: HelmChart): void => {
if (!chart) {
navigation.merge(helmChartsURL())
}
else {
navigation.merge(helmChartsURL());
} else {
navigation.merge(helmChartsURL({
params: {
chartName: chart.getName(),
repo: chart.getRepository(),
repo: chart.repo,
}
}))
}));
}
}
hideDetails = () => {
hideDetails = (): void => {
this.showDetails(null);
}
render() {
render(): JSX.Element {
return (
<>
<ItemListLayout
@@ -59,19 +58,19 @@ export class HelmCharts extends Component<Props> {
isClusterScoped={true}
isSelectable={false}
sortingCallbacks={{
[sortBy.name]: (chart: HelmChart) => chart.getName(),
[sortBy.repo]: (chart: HelmChart) => chart.getRepository(),
[sortBy.name]: (chart: HelmChart): string => chart.getName(),
[sortBy.repo]: ({repo}: HelmChart): string => repo,
}}
searchFilters={[
(chart: HelmChart) => chart.getName(),
(chart: HelmChart) => chart.getVersion(),
(chart: HelmChart) => chart.getAppVersion(),
(chart: HelmChart) => chart.getKeywords(),
(chart: HelmChart): string => chart.getName(),
({ version }: HelmChart): string => version,
(chart: HelmChart): string => chart.getAppVersion(),
({ keywords }: HelmChart): string[] => keywords,
]}
filterItems={[
(items: HelmChart[]) => items.filter(item => !item.deprecated)
(items: HelmChart[]): HelmChart[] => items.filter(item => !item.deprecated)
]}
customizeHeader={() => (
customizeHeader={(): JSX.Element => (
<SearchInput placeholder={_i18n._(t`Search Helm Charts`)}/>
)}
renderTableHeader={[
@@ -83,18 +82,18 @@ export class HelmCharts extends Component<Props> {
{ title: <Trans>Repository</Trans>, className: "repository", sortBy: sortBy.repo },
]}
renderTableContents={(chart: HelmChart) => [
<figure>
renderTableContents={(chart: HelmChart): (HTMLElement | string | React.ReactNode)[] => [
<figure key="placeholder-img">
<img
src={chart.getIcon() || require("./helm-placeholder.svg")}
onLoad={evt => evt.currentTarget.classList.add("visible")}
src={chart.icon || require("./helm-placeholder.svg")}
onLoad={(evt): void => evt.currentTarget.classList.add("visible")}
/>
</figure>,
chart.getName(),
chart.getDescription(),
chart.getVersion(),
chart.description,
chart.version,
chart.getAppVersion(),
{ title: chart.getRepository(), className: chart.getRepository().toLowerCase() }
{ title: chart.repo, className: chart.repo.toLowerCase() }
]}
detailsItem={this.selectedChart}
onDetails={this.showDetails}
@@ -7,7 +7,7 @@ import { observable, reaction } from "mobx";
import { Link } from "react-router-dom";
import { t, Trans } from "@lingui/macro";
import kebabCase from "lodash/kebabCase";
import { HelmRelease, helmReleasesApi, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
import { HelmRelease, helmReleasesApi, ReleaseInfo } from "../../api/endpoints/helm-releases.api";
import { HelmReleaseMenu } from "./release-menu";
import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
@@ -35,14 +35,16 @@ interface Props {
@observer
export class ReleaseDetails extends Component<Props> {
@observable details: IReleaseDetails;
@observable details: ReleaseInfo;
@observable values = "";
@observable saving = false;
@observable releaseSecret: Secret;
@disposeOnUnmount
releaseSelector = reaction(() => this.props.release, release => {
if (!release) return;
if (!release) {
return;
}
this.loadDetails();
this.loadValues();
this.releaseSecret = null;
@@ -51,33 +53,36 @@ export class ReleaseDetails extends Component<Props> {
@disposeOnUnmount
secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
if (!this.props.release) return;
if (!this.props.release) {
return;
}
const { getReleaseSecret } = releaseStore;
const { release } = this.props;
const secret = getReleaseSecret(release);
if (this.releaseSecret) {
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) {
return;
}
this.loadDetails();
}
this.releaseSecret = secret;
});
async loadDetails() {
async loadDetails(): Promise<void> {
const { release } = this.props;
this.details = null;
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
this.details = await helmReleasesApi.get(release.getName(), release.namespace);
}
async loadValues() {
async loadValues(): Promise<void> {
const { release } = this.props;
this.values = "";
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
this.values = await helmReleasesApi.getValues(release.getName(), release.namespace);
}
updateValues = async () => {
updateValues = async (): Promise<void> => {
const { release } = this.props;
const name = release.getName();
const namespace = release.getNs()
const { namespace, name} = release;
const data = {
chart: release.getChart(),
repo: await release.getRepo(),
@@ -96,13 +101,13 @@ export class ReleaseDetails extends Component<Props> {
this.saving = false;
}
upgradeVersion = () => {
upgradeVersion = (): void => {
const { release, hideDetails } = this.props;
createUpgradeChartTab(release);
hideDetails();
}
renderValues() {
renderValues(): JSX.Element {
const { values, saving } = this;
return (
<div className="values">
@@ -111,7 +116,7 @@ export class ReleaseDetails extends Component<Props> {
<AceEditor
mode="yaml"
value={values}
onChange={values => this.values = values}
onChange={(values): string => this.values = values}
/>
<Button
primary
@@ -121,11 +126,13 @@ export class ReleaseDetails extends Component<Props> {
/>
</div>
</div>
)
);
}
renderNotes() {
if (!this.details.info?.notes) return null;
renderNotes(): JSX.Element {
if (!this.details.info?.notes) {
return null;
}
const { notes } = this.details.info;
return (
<div className="notes">
@@ -134,9 +141,11 @@ export class ReleaseDetails extends Component<Props> {
);
}
renderResources() {
renderResources(): JSX.Element {
const { resources } = this.details;
if (!resources) return null;
if (!resources) {
return null;
}
const groups = groupBy(resources, item => item.kind);
const tables = Object.entries(groups).map(([kind, items]) => {
return (
@@ -177,10 +186,12 @@ export class ReleaseDetails extends Component<Props> {
);
}
renderContent() {
renderContent(): JSX.Element {
const { release } = this.props;
const { details } = this;
if (!release) return null;
if (!release) {
return null;
}
if (!details) {
return <Spinner center/>;
}
@@ -201,7 +212,7 @@ export class ReleaseDetails extends Component<Props> {
{release.getUpdated()} <Trans>ago</Trans> ({release.updated})
</DrawerItem>
<DrawerItem name={<Trans>Namespace</Trans>}>
{release.getNs()}
{release.namespace}
</DrawerItem>
<DrawerItem name={<Trans>Version</Trans>} onClick={stopPropagation}>
<div className="version flex gaps align-center">
@@ -222,13 +233,13 @@ export class ReleaseDetails extends Component<Props> {
<DrawerTitle title={_i18n._(t`Resources`)}/>
{this.renderResources()}
</div>
)
);
}
render() {
const { release, hideDetails } = this.props
const title = release ? <Trans>Release: {release.getName()}</Trans> : ""
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>
render(): JSX.Element {
const { release, hideDetails } = this.props;
const title = release ? <Trans>Release: {release.getName()}</Trans> : "";
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>;
return (
<Drawer
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
@@ -240,6 +251,6 @@ export class ReleaseDetails extends Component<Props> {
>
{this.renderContent()}
</Drawer>
)
);
}
}
@@ -17,26 +17,28 @@ interface Props extends MenuActionsProps {
export class HelmReleaseMenu extends React.Component<Props> {
@autobind()
remove() {
remove(): Promise<void> {
return releaseStore.remove(this.props.release);
}
@autobind()
upgrade() {
upgrade(): void {
const { release, hideDetails } = this.props;
createUpgradeChartTab(release);
hideDetails && hideDetails();
}
@autobind()
rollback() {
rollback(): void {
ReleaseRollbackDialog.open(this.props.release);
}
renderContent() {
renderContent(): JSX.Element {
const { release, toolbar } = this.props;
if (!release) return;
const hasRollback = release && release.getRevision() > 1;
if (!release) {
return;
}
const hasRollback = release && release.revision > 1;
return (
<>
{hasRollback && (
@@ -46,18 +48,19 @@ export class HelmReleaseMenu extends React.Component<Props> {
</MenuItem>
)}
</>
)
);
}
render() {
const { className, release, ...menuProps } = this.props;
render(): JSX.Element {
const { className, release: _release, ...menuProps } = this.props;
return (
<MenuActions
{...menuProps}
className={cssNames("HelmReleaseMenu", className)}
removeAction={this.remove}
children={this.renderContent()}
/>
>
{this.renderContent()}
</MenuActions>
);
}
}
@@ -6,11 +6,11 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { HelmRelease, helmReleasesApi, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { HelmRelease, helmReleasesApi, ReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { releaseStore } from "./release.store";
import { Select, SelectOption } from "../select";
import { Notifications } from "../notifications";
import orderBy from "lodash/orderBy"
import orderBy from "lodash/orderBy";
interface Props extends DialogProps {
}
@@ -21,15 +21,15 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
@observable.ref static release: HelmRelease = null;
@observable isLoading = false;
@observable revision: IReleaseRevision;
@observable revisions = observable.array<IReleaseRevision>();
@observable revision: ReleaseRevision;
@observable revisions = observable.array<ReleaseRevision>();
static open(release: HelmRelease) {
static open(release: HelmRelease): void {
ReleaseRollbackDialog.isOpen = true;
ReleaseRollbackDialog.release = release;
}
static close() {
static close(): void {
ReleaseRollbackDialog.isOpen = false;
}
@@ -37,10 +37,10 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
return ReleaseRollbackDialog.release;
}
onOpen = async () => {
onOpen = async (): Promise<void> => {
this.isLoading = true;
const currentRevision = this.release.getRevision();
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
const currentRevision = this.release.revision;
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.namespace);
releases = releases.filter(item => item.revision !== currentRevision); // remove current
releases = orderBy(releases, "revision", "desc"); // sort
this.revisions.replace(releases);
@@ -48,24 +48,24 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
this.isLoading = false;
}
rollback = async () => {
rollback = async (): Promise<void> => {
const revisionNumber = this.revision.revision;
try {
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
await releaseStore.rollback(this.release.getName(), this.release.namespace, revisionNumber);
this.close();
} catch (err) {
Notifications.error(err);
}
};
close = () => {
close = (): void => {
ReleaseRollbackDialog.close();
}
renderContent() {
renderContent(): JSX.Element {
const { revision, revisions } = this;
if (!revision) {
return <p><Trans>No revisions to rollback.</Trans></p>
return <p><Trans>No revisions to rollback.</Trans></p>;
}
return (
<div className="flex gaps align-center">
@@ -74,17 +74,19 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
themeName="light"
value={revision}
options={revisions}
formatOptionLabel={({ value }: SelectOption<IReleaseRevision>) => `${value.revision} - ${value.chart}`}
onChange={({ value }: SelectOption<IReleaseRevision>) => this.revision = value}
formatOptionLabel={({ value }: SelectOption<ReleaseRevision>): string => `${value.revision} - ${value.chart}`}
onChange={({ value }: SelectOption<ReleaseRevision>): void => {
this.revision = value;
}}
/>
</div>
)
);
}
render() {
render(): JSX.Element {
const { ...dialogProps } = this.props;
const releaseName = this.release ? this.release.getName() : "";
const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5>
const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5>;
return (
<Dialog
{...dialogProps}
@@ -104,6 +106,6 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
</WizardStep>
</Wizard>
</Dialog>
)
);
}
}
@@ -1,14 +1,14 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { appsRoute } from "../+apps/apps.route";
import { buildURL } from "../../navigation";
export const releaseRoute: RouteProps = {
path: appsRoute.path + "/releases/:namespace?/:name?"
}
};
export interface IReleaseRouteParams {
export interface ReleaseRouteParams {
name?: string;
namespace?: string;
}
export const releaseURL = buildURL<IReleaseRouteParams>(releaseRoute.path);
export const releaseURL = buildURL<ReleaseRouteParams>(releaseRoute.path);
@@ -1,11 +1,12 @@
import isEqual from "lodash/isEqual";
import { action, observable, when, IReactionDisposer, reaction } from "mobx";
import { autobind } from "../../utils";
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
import { HelmRelease, helmReleasesApi, ReleaseCreatePayload, ReleaseUpdatePayload, ReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
import { ItemStore } from "../../item.store";
import { configStore } from "../../config.store";
import { secretsStore } from "../+config-secrets/secrets.store";
import { Secret } from "../../api/endpoints";
import { KubeJsonApiData } from "client/api/kube-json-api";
@autobind()
export class ReleaseStore extends ItemStore<HelmRelease> {
@@ -19,47 +20,51 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
});
}
watch() {
watch(): void {
this.secretWatcher = reaction(() => secretsStore.items.toJS(), () => {
if (this.isLoading) return;
if (this.isLoading) {
return;
}
const secrets = this.getReleaseSecrets();
const amountChanged = secrets.length !== this.releaseSecrets.length;
const labelsChanged = this.releaseSecrets.some(item => {
const secret = secrets.find(secret => secret.getId() == item.getId());
if (!secret) return;
if (!secret) {
return;
}
return !isEqual(item.getLabels(), secret.getLabels());
});
if (amountChanged || labelsChanged) {
this.loadAll();
}
this.releaseSecrets = [...secrets];
})
});
}
unwatch() {
unwatch(): void {
this.secretWatcher();
}
getReleaseSecrets() {
getReleaseSecrets(): Secret[] {
return secretsStore.getByLabel({ owner: "helm" });
}
getReleaseSecret(release: HelmRelease) {
getReleaseSecret(release: HelmRelease): Secret {
const labels = {
owner: "helm",
name: release.getName()
}
};
return secretsStore.getByLabel(labels)
.filter(secret => secret.getNs() == release.getNs())[0];
.filter(secret => secret.getNs() == release.namespace)[0];
}
@action
async loadAll() {
async loadAll(): Promise<void> {
this.isLoading = true;
let items;
let items: HelmRelease[];
try {
const { isClusterAdmin, allowedNamespaces } = configStore;
items = await this.loadItems(!isClusterAdmin ? allowedNamespaces : null);
items = await this.loadItems(...(!isClusterAdmin ? allowedNamespaces : []));
} finally {
if (items) {
items = this.sortItems(items);
@@ -70,42 +75,49 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
}
async loadItems(namespaces?: string[]) {
async loadItems(...namespaces: any[]): Promise<HelmRelease[]> {
if (!namespaces) {
return helmReleasesApi.list();
}
else {
} else {
return Promise
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
.then(items => items.flat());
}
}
async create(payload: IReleaseCreatePayload) {
async create(payload: ReleaseCreatePayload): Promise<ReleaseUpdateDetails> {
const response = await helmReleasesApi.create(payload);
if (this.isLoaded) this.loadAll();
if (this.isLoaded) {
this.loadAll();
}
return response;
}
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
async update(name: string, namespace: string, payload: ReleaseUpdatePayload): Promise<ReleaseUpdateDetails> {
const response = await helmReleasesApi.update(name, namespace, payload);
if (this.isLoaded) this.loadAll();
if (this.isLoaded) {
this.loadAll();
}
return response;
}
async rollback(name: string, namespace: string, revision: number) {
async rollback(name: string, namespace: string, revision: number): Promise<KubeJsonApiData> {
const response = await helmReleasesApi.rollback(name, namespace, revision);
if (this.isLoaded) this.loadAll();
if (this.isLoaded) {
this.loadAll();
}
return response;
}
async remove(release: HelmRelease) {
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.getNs()));
async remove(release: HelmRelease): Promise<void> {
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.namespace));
}
async removeSelectedItems() {
if (!this.selectedItems.length) return;
return Promise.all(this.selectedItems.map(this.remove));
async removeSelectedItems(): Promise<void> {
if (!this.selectedItems.length) {
return;
}
await Promise.all(this.selectedItems.map(this.remove));
}
}
@@ -6,7 +6,7 @@ import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { RouteComponentProps } from "react-router";
import { releaseStore } from "./release.store";
import { IReleaseRouteParams, releaseURL } from "./release.route";
import { ReleaseRouteParams, releaseURL } from "./release.route";
import { HelmRelease } from "../../api/endpoints/helm-releases.api";
import { ReleaseDetails } from "./release-details";
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
@@ -24,59 +24,58 @@ enum sortBy {
updated = "update"
}
interface Props extends RouteComponentProps<IReleaseRouteParams> {
interface Props extends RouteComponentProps<ReleaseRouteParams> {
}
@observer
export class HelmReleases extends Component<Props> {
componentDidMount() {
componentDidMount(): void {
// Watch for secrets associated with releases and react to their changes
releaseStore.watch();
}
componentWillUnmount() {
componentWillUnmount(): void {
releaseStore.unwatch();
}
get selectedRelease() {
get selectedRelease(): HelmRelease {
const { match: { params: { name, namespace } } } = this.props;
return releaseStore.items.find(release => {
return release.getName() == name && release.getNs() == namespace;
return release.getName() == name && release.namespace == namespace;
});
}
showDetails = (item: HelmRelease) => {
showDetails = (item: HelmRelease): void => {
if (!item) {
navigation.merge(releaseURL())
}
else {
navigation.merge(releaseURL());
} else {
navigation.merge(releaseURL({
params: {
name: item.getName(),
namespace: item.getNs()
namespace: item.namespace
}
}))
}));
}
}
hideDetails = () => {
hideDetails = (): void => {
this.showDetails(null);
}
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
renderRemoveDialogMessage(selectedItems: HelmRelease[]): JSX.Element {
const releaseNames = selectedItems.map(item => item.getName()).join(", ");
return (
<div>
<Trans>Remove <b>{releaseNames}</b>?</Trans>
<p className="warning">
<Trans>Note: StatefulSet Volumes won't be deleted automatically</Trans>
<Trans>Note: StatefulSet Volumes won&apos;t be deleted automatically</Trans>
</p>
</div>
)
);
}
render() {
render(): JSX.Element {
return (
<>
<ItemListLayout
@@ -84,19 +83,19 @@ export class HelmReleases extends Component<Props> {
store={releaseStore}
dependentStores={[secretsStore]}
sortingCallbacks={{
[sortBy.name]: (release: HelmRelease) => release.getName(),
[sortBy.namespace]: (release: HelmRelease) => release.getNs(),
[sortBy.revision]: (release: HelmRelease) => release.getRevision(),
[sortBy.chart]: (release: HelmRelease) => release.getChart(),
[sortBy.status]: (release: HelmRelease) => release.getStatus(),
[sortBy.updated]: (release: HelmRelease) => release.getUpdated(false, false),
[sortBy.name]: (release: HelmRelease): string => release.getName(),
[sortBy.namespace]: (release: HelmRelease): string => release.namespace,
[sortBy.revision]: (release: HelmRelease): number => release.revision,
[sortBy.chart]: (release: HelmRelease): string => release.getChart(),
[sortBy.status]: (release: HelmRelease): string => release.status,
[sortBy.updated]: (release: HelmRelease): string | number => release.getUpdated(false, false),
}}
searchFilters={[
(release: HelmRelease) => release.getName(),
(release: HelmRelease) => release.getNs(),
(release: HelmRelease) => release.getChart(),
(release: HelmRelease) => release.getStatus(),
(release: HelmRelease) => release.getVersion(),
(release: HelmRelease): string => release.getName(),
(release: HelmRelease): string => release.namespace,
(release: HelmRelease): string => release.getChart(),
(release: HelmRelease): string => release.getStatus(),
(release: HelmRelease): string | number => release.getVersion(),
]}
renderHeaderTitle={<Trans>Releases</Trans>}
renderTableHeader={[
@@ -109,30 +108,30 @@ export class HelmReleases extends Component<Props> {
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
{ title: <Trans>Updated</Trans>, className: "updated", sortBy: sortBy.updated },
]}
renderTableContents={(release: HelmRelease) => {
renderTableContents={(release: HelmRelease): (string | number | React.ReactNode)[] => {
const version = release.getVersion();
return [
release.getName(),
release.getNs(),
release.namespace,
release.getChart(),
release.getRevision(),
release.revision,
<>
{version}
</>,
release.appVersion,
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
release.getUpdated(),
]
];
}}
renderItemMenu={(release: HelmRelease) => {
renderItemMenu={(release: HelmRelease): JSX.Element => {
return (
<HelmReleaseMenu
release={release}
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
/>
)
);
}}
customizeRemoveDialog={(selectedItems: HelmRelease[]) => ({
customizeRemoveDialog={(selectedItems: HelmRelease[]): {message: JSX.Element} => ({
message: this.renderRemoveDialogMessage(selectedItems)
})}
detailsItem={this.selectedRelease}
+3 -3
View File
@@ -24,10 +24,10 @@ export class Apps extends React.Component {
url: releaseURL({ query }),
path: releaseRoute.path,
},
]
];
}
render() {
render(): JSX.Element {
const tabRoutes = Apps.tabRoutes;
return (
<MainLayout className="Apps" tabs={tabRoutes}>
@@ -36,6 +36,6 @@ export class Apps extends React.Component {
<Redirect to={tabRoutes[0].url}/>
</Switch>
</MainLayout>
)
);
}
}
@@ -1,4 +1,4 @@
import "./cluster-issues.scss"
import "./cluster-issues.scss";
import * as React from "react";
import { observer } from "mobx-react";
@@ -20,7 +20,7 @@ interface Props {
className?: string;
}
interface IWarning extends ItemObject {
interface Warning extends ItemObject {
kind: string;
message: string;
selfLink: string;
@@ -34,16 +34,16 @@ enum sortBy {
@observer
export class ClusterIssues extends React.Component<Props> {
private sortCallbacks = {
[sortBy.type]: (warning: IWarning) => warning.kind,
[sortBy.object]: (warning: IWarning) => warning.getName(),
[sortBy.type]: (warning: Warning): string => warning.kind,
[sortBy.object]: (warning: Warning): string => warning.getName(),
};
@computed get warnings() {
const warnings: IWarning[] = [];
@computed get warnings(): Warning[] {
const warnings: Warning[] = [];
// Node bad conditions
nodesStore.items.forEach(node => {
const { kind, selfLink, getId, getName } = node
const { kind, selfLink, getId, getName } = node;
node.getWarningConditions().forEach(({ message }) => {
warnings.push({
kind,
@@ -51,8 +51,8 @@ export class ClusterIssues extends React.Component<Props> {
getName,
selfLink,
message,
})
})
});
});
});
// Warning events for Workloads
@@ -67,13 +67,13 @@ export class ClusterIssues extends React.Component<Props> {
kind,
selfLink: lookupApiLink(involvedObject, error),
});
})
});
return warnings;
}
@autobind()
getTableRow(uid: string) {
getTableRow(uid: string): JSX.Element {
const { warnings } = this;
const warning = warnings.find(warn => warn.getId() == uid);
const { getId, getName, message, kind, selfLink } = warning;
@@ -97,7 +97,7 @@ export class ClusterIssues extends React.Component<Props> {
);
}
renderContent() {
renderContent(): JSX.Element {
const { warnings } = this;
if (!eventStore.isLoaded) {
return (
@@ -139,7 +139,7 @@ export class ClusterIssues extends React.Component<Props> {
);
}
render() {
render(): JSX.Element {
return (
<div className={cssNames("ClusterIssues flex column", this.props.className)}>
{this.renderContent()}
@@ -21,7 +21,9 @@ export const ClusterMetricSwitchers = observer(() => {
asButtons
className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })}
value={metricNodeRole}
onChange={(metric: MetricNodeRole) => clusterStore.metricNodeRole = metric}
onChange={(metric: MetricNodeRole): void => {
clusterStore.metricNodeRole = metric;
}}
>
<Radio label={<Trans>Master</Trans>} value={MetricNodeRole.MASTER}/>
<Radio label={<Trans>Worker</Trans>} value={MetricNodeRole.WORKER}/>
@@ -32,7 +34,9 @@ export const ClusterMetricSwitchers = observer(() => {
asButtons
className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })}
value={metricType}
onChange={(value: MetricType) => clusterStore.metricType = value}
onChange={(value: MetricType): void => {
clusterStore.metricType = value;
}}
>
<Radio label={<Trans>CPU</Trans>} value={MetricType.CPU}/>
<Radio label={<Trans>Memory</Trans>} value={MetricType.MEMORY}/>
@@ -34,13 +34,13 @@ export const ClusterMetrics = observer(() => {
yAxes: [{
ticks: {
suggestedMax: cpuCapacity,
callback: (value) => value
callback: (value): any => value
}
}]
},
tooltips: {
callbacks: {
label: ({ index }, data) => {
label: ({ index }, data): string => {
const value = data.datasets[0].data[index] as ChartPoint;
return value.y.toString();
}
@@ -52,13 +52,13 @@ export const ClusterMetrics = observer(() => {
yAxes: [{
ticks: {
suggestedMax: memoryCapacity,
callback: (value: string) => !value ? 0 : bytesToUnits(parseInt(value))
callback: (value: string): string | number => !value ? 0 : bytesToUnits(parseInt(value))
}
}]
},
tooltips: {
callbacks: {
label: ({ index }, data) => {
label: ({ index }, data): string => {
const value = data.datasets[0].data[index] as ChartPoint;
return bytesToUnits(parseInt(value.y as string), 3);
}
@@ -67,12 +67,12 @@ export const ClusterMetrics = observer(() => {
};
const options = metricType === MetricType.CPU ? cpuOptions : memoryOptions;
const renderMetrics = () => {
const renderMetrics = (): JSX.Element => {
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
return <Spinner center/>;
}
if (!memoryCapacity || !cpuCapacity) {
return <ClusterNoMetrics className="empty"/>
return <ClusterNoMetrics className="empty"/>;
}
return (
<BarChart
@@ -7,7 +7,7 @@ interface Props {
className: string;
}
export function ClusterNoMetrics({ className }: Props) {
export function ClusterNoMetrics({ className }: Props): JSX.Element {
return (
<div className={cssNames("ClusterNoMetrics flex column box grow justify-center align-center", className)}>
<Icon material="info"/>
@@ -17,16 +17,16 @@ import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
export const ClusterPieCharts = observer(() => {
const { i18n } = useLingui();
const renderLimitWarning = () => {
const renderLimitWarning = (): JSX.Element => {
return (
<div className="node-warning flex gaps align-center">
<Icon material="info"/>
<p><Trans>Specified limits are higher than node capacity!</Trans></p>
</div>
);
}
};
const renderCharts = () => {
const renderCharts = (): JSX.Element => {
const data = getMetricLastPoints(clusterStore.metrics);
const { memoryUsage, memoryRequests, memoryCapacity, memoryLimits } = data;
const { cpuUsage, cpuRequests, cpuCapacity, cpuLimits } = data;
@@ -35,7 +35,9 @@ export const ClusterPieCharts = observer(() => {
const memoryLimitsOverload = memoryLimits > memoryCapacity;
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
if (!memoryCapacity || !cpuCapacity || !podCapacity) return null;
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
return null;
}
const cpuData: ChartData = {
datasets: [
{
@@ -168,9 +170,9 @@ export const ClusterPieCharts = observer(() => {
</div>
</div>
);
}
};
const renderContent = () => {
const renderContent = (): JSX.Element => {
const { masterNodes, workerNodes } = nodesStore;
const { metricNodeRole, metricsLoaded } = clusterStore;
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
@@ -194,11 +196,11 @@ export const ClusterPieCharts = observer(() => {
return <ClusterNoMetrics className="empty"/>;
}
return renderCharts();
}
};
return (
<div className="ClusterPieCharts flex">
{renderContent()}
</div>
);
})
});
@@ -3,6 +3,6 @@ import { buildURL } from "../../navigation";
export const clusterRoute: RouteProps = {
path: "/cluster"
}
};
export const clusterURL = buildURL(clusterRoute.path)
export const clusterURL = buildURL(clusterRoute.path);
@@ -1,8 +1,8 @@
import { observable, reaction, when } from "mobx";
import { KubeObjectStore } from "../../kube-object.store";
import { Cluster, clusterApi, IClusterMetrics } from "../../api/endpoints";
import { autobind, createStorage } from "../../utils";
import { IMetricsReqParams, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { Cluster, clusterApi, ClusterMetrics } from "../../api/endpoints";
import { autobind, StorageHelper } from "../../utils";
import { MetricsReqParams, normalizeMetrics, Metrics } from "../../api/endpoints/metrics.api";
import { nodesStore } from "../+nodes/nodes.store";
import { apiManager } from "../../api/api-manager";
@@ -20,8 +20,8 @@ export enum MetricNodeRole {
export class ClusterStore extends KubeObjectStore<Cluster> {
api = clusterApi
@observable metrics: Partial<IClusterMetrics> = {};
@observable liveMetrics: Partial<IClusterMetrics> = {};
@observable metrics: Partial<ClusterMetrics> = {};
@observable liveMetrics: Partial<ClusterMetrics> = {};
@observable metricsLoaded = false;
@observable metricType: MetricType;
@observable metricNodeRole: MetricNodeRole;
@@ -31,18 +31,20 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
this.resetMetrics();
// sync user setting with local storage
const storage = createStorage("cluster_metric_switchers", {});
const storage = new StorageHelper("cluster_metric_switchers", {});
Object.assign(this, storage.get());
reaction(() => {
const { metricType, metricNodeRole } = this;
return { metricType, metricNodeRole }
return { metricType, metricNodeRole };
},
settings => storage.set(settings)
);
// auto-update metrics
reaction(() => this.metricNodeRole, () => {
if (!this.metricsLoaded) return;
if (!this.metricsLoaded) {
return;
}
this.metrics = {};
this.liveMetrics = {};
this.metricsLoaded = false;
@@ -52,29 +54,33 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
// check which node type to select
reaction(() => nodesStore.items.length, () => {
const { masterNodes, workerNodes } = nodesStore;
if (!masterNodes.length) this.metricNodeRole = MetricNodeRole.WORKER;
if (!workerNodes.length) this.metricNodeRole = MetricNodeRole.MASTER;
if (!masterNodes.length) {
this.metricNodeRole = MetricNodeRole.WORKER;
}
if (!workerNodes.length) {
this.metricNodeRole = MetricNodeRole.MASTER;
}
});
}
async loadMetrics(params?: IMetricsReqParams) {
async loadMetrics(params?: MetricsReqParams): Promise<ClusterMetrics<Metrics>> {
await when(() => nodesStore.isLoaded);
const { masterNodes, workerNodes } = nodesStore;
const nodes = this.metricNodeRole === MetricNodeRole.MASTER && masterNodes.length ? masterNodes : workerNodes;
return clusterApi.getMetrics(nodes.map(node => node.getName()), params);
}
async getAllMetrics() {
async getAllMetrics(): Promise<void> {
await this.getMetrics();
await this.getLiveMetrics();
this.metricsLoaded = true;
}
async getMetrics() {
async getMetrics(): Promise<void> {
this.metrics = await this.loadMetrics();
}
async getLiveMetrics() {
async getLiveMetrics(): Promise<void> {
const step = 3;
const range = 15;
const end = Date.now() / 1000;
@@ -82,25 +88,25 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
}
getMetricsValues(source: Partial<IClusterMetrics>): [number, string][] {
getMetricsValues(source: Partial<ClusterMetrics>): [number, string][] {
switch (this.metricType) {
case MetricType.CPU:
return normalizeMetrics(source.cpuUsage).data.result[0].values
return normalizeMetrics(source.cpuUsage).data.result[0].values;
case MetricType.MEMORY:
return normalizeMetrics(source.memoryUsage).data.result[0].values
return normalizeMetrics(source.memoryUsage).data.result[0].values;
default:
return [];
}
}
resetMetrics() {
resetMetrics(): void {
this.metrics = {};
this.metricsLoaded = false;
this.metricType = MetricType.CPU;
this.metricNodeRole = MetricNodeRole.WORKER;
}
reset() {
reset(): void {
super.reset();
this.resetMetrics();
}
@@ -1,4 +1,4 @@
import "./cluster.scss"
import "./cluster.scss";
import React from "react";
import { computed, reaction } from "mobx";
@@ -6,7 +6,7 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { MainLayout } from "../layout/main-layout";
import { ClusterIssues } from "./cluster-issues";
import { Spinner } from "../spinner";
import { cssNames, interval, isElectron } from "../../utils";
import { cssNames, IntervalManager, isElectron } from "../../utils";
import { ClusterPieCharts } from "./cluster-pie-charts";
import { ClusterMetrics } from "./cluster-metrics";
import { nodesStore } from "../+nodes/nodes.store";
@@ -18,16 +18,16 @@ import { isAllowedResource } from "../../api/rbac";
@observer
export class Cluster extends React.Component {
private watchers = [
interval(60, () => clusterStore.getMetrics()),
interval(20, () => eventStore.loadAll())
new IntervalManager(60, () => clusterStore.getMetrics()),
new IntervalManager(20, () => eventStore.loadAll())
];
private dependentStores = [nodesStore, podsStore];
async componentDidMount() {
async componentDidMount(): Promise<void> {
const { dependentStores } = this;
if (!isAllowedResource("nodes")) {
dependentStores.splice(dependentStores.indexOf(nodesStore), 1)
dependentStores.splice(dependentStores.indexOf(nodesStore), 1);
}
this.watchers.forEach(watcher => watcher.start(true));
@@ -38,22 +38,22 @@ export class Cluster extends React.Component {
disposeOnUnmount(this, [
...dependentStores.map(store => store.subscribe()),
() => this.watchers.forEach(watcher => watcher.stop()),
(): void => this.watchers.forEach(watcher => watcher.stop()),
reaction(
() => clusterStore.metricNodeRole,
() => this.watchers.forEach(watcher => watcher.restart())
)
])
]);
}
@computed get isLoaded() {
@computed get isLoaded(): boolean {
return (
nodesStore.isLoaded &&
podsStore.isLoaded
)
);
}
render() {
render(): JSX.Element {
const { isLoaded } = this;
return (
<MainLayout>
@@ -68,6 +68,6 @@ export class Cluster extends React.Component {
)}
</div>
</MainLayout>
)
);
}
}
@@ -1,2 +1,2 @@
export * from "./cluster.routes"
export * from "./cluster.routes";
@@ -7,7 +7,7 @@ import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
import { KubeObjectDetailsProps } from "../kube-object";
import { cssNames } from "../../utils";
import { HorizontalPodAutoscaler, hpaApi, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
import { HorizontalPodAutoscaler, hpaApi, HpaMetricType, HpaMetric } from "../../api/endpoints/hpa.api";
import { KubeEventDetails } from "../+events/kube-event-details";
import { Trans } from "@lingui/macro";
import { Table, TableCell, TableHead, TableRow } from "../table";
@@ -21,10 +21,10 @@ interface Props extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
@observer
export class HpaDetails extends React.Component<Props> {
renderMetrics() {
renderMetrics(): JSX.Element {
const { object: hpa } = this.props;
const renderName = (metric: IHpaMetric) => {
const renderName = (metric: HpaMetric): JSX.Element => {
switch (metric.type) {
case HpaMetricType.Resource:
const addition = metric.resource.targetAverageUtilization ? <Trans>(as a percentage of request)</Trans> : "";
@@ -51,7 +51,7 @@ export class HpaDetails extends React.Component<Props> {
</Trans>
);
}
}
};
return (
<Table>
@@ -60,7 +60,7 @@ export class HpaDetails extends React.Component<Props> {
<TableCell className="metrics"><Trans>Current / Target</Trans></TableCell>
</TableHead>
{
hpa.getMetrics().map((metric, index) => {
hpa.spec.metrics.map((metric, index) => {
const name = renderName(metric);
const values = hpa.getMetricValues(metric);
return (
@@ -68,16 +68,18 @@ export class HpaDetails extends React.Component<Props> {
<TableCell className="name">{name}</TableCell>
<TableCell className="metrics">{values}</TableCell>
</TableRow>
)
);
})
}
</Table>
);
}
render() {
render(): JSX.Element {
const { object: hpa } = this.props;
if (!hpa) return;
if (!hpa) {
return;
}
const { scaleTargetRef } = hpa.spec;
return (
<div className="HpaDetails">
@@ -92,20 +94,22 @@ export class HpaDetails extends React.Component<Props> {
</DrawerItem>
<DrawerItem name={<Trans>Min Pods</Trans>}>
{hpa.getMinPods()}
{hpa.spec.minReplicas}
</DrawerItem>
<DrawerItem name={<Trans>Max Pods</Trans>}>
{hpa.getMaxPods()}
{hpa.spec.maxReplicas}
</DrawerItem>
<DrawerItem name={<Trans>Replicas</Trans>}>
{hpa.getReplicas()}
{hpa.status.currentReplicas}
</DrawerItem>
<DrawerItem name={<Trans>Status</Trans>} labelsOnly>
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
if (!isReady) return null;
if (!isReady) {
return null;
}
return (
<Badge
key={type}
@@ -113,7 +117,7 @@ export class HpaDetails extends React.Component<Props> {
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
);
})}
</DrawerItem>
@@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
export const hpaRoute: RouteProps = {
path: "/hpa"
};
export interface HpaRouteParams {
}
export interface IHpaRouteParams {
}
export const hpaURL = buildURL<IHpaRouteParams>(hpaRoute.path)
export const hpaURL = buildURL<HpaRouteParams>(hpaRoute.path);
@@ -1,4 +1,4 @@
import "./hpa.scss"
import "./hpa.scss";
import * as React from "react";
import { observer } from "mobx-react";
@@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router";
import { Trans } from "@lingui/macro";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object";
import { IHpaRouteParams } from "./hpa.route";
import { HpaRouteParams } from "./hpa.route";
import { HorizontalPodAutoscaler, hpaApi } from "../../api/endpoints/hpa.api";
import { hpaStore } from "./hpa.store";
import { Badge } from "../badge";
@@ -22,32 +22,32 @@ enum sortBy {
age = "age",
}
interface Props extends RouteComponentProps<IHpaRouteParams> {
interface Props extends RouteComponentProps<HpaRouteParams> {
}
@observer
export class HorizontalPodAutoscalers extends React.Component<Props> {
getTargets(hpa: HorizontalPodAutoscaler) {
const metrics = hpa.getMetrics();
getTargets(hpa: HorizontalPodAutoscaler): JSX.Element {
const { metrics } = hpa.spec;
const metricsRemainCount = metrics.length - 1;
const metricsRemain = metrics.length > 1 ? <Trans>{metricsRemainCount} more...</Trans> : null;
const metricValues = hpa.getMetricValues(metrics[0]);
return <p>{metricValues} {metricsRemain && "+"}{metricsRemain}</p>;
}
render() {
render(): JSX.Element {
return (
<KubeObjectListLayout
className="HorizontalPodAutoscalers" store={hpaStore}
sortingCallbacks={{
[sortBy.name]: (item: HorizontalPodAutoscaler) => item.getName(),
[sortBy.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),
[sortBy.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(),
[sortBy.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(),
[sortBy.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas()
[sortBy.name]: (item: HorizontalPodAutoscaler): string => item.getName(),
[sortBy.namespace]: (item: HorizontalPodAutoscaler): string => item.getNs(),
[sortBy.minPods]: (item: HorizontalPodAutoscaler): number => item.spec.minReplicas,
[sortBy.maxPods]: (item: HorizontalPodAutoscaler): number => item.spec.maxReplicas,
[sortBy.replicas]: (item: HorizontalPodAutoscaler): number => item.status.currentReplicas
}}
searchFilters={[
(item: HorizontalPodAutoscaler) => item.getSearchFields()
(item: HorizontalPodAutoscaler): string[] => item.getSearchFields()
]}
renderHeaderTitle={<Trans>Horizontal Pod Autoscalers</Trans>}
renderTableHeader={[
@@ -60,16 +60,18 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status" },
]}
renderTableContents={(hpa: HorizontalPodAutoscaler) => [
renderTableContents={(hpa: HorizontalPodAutoscaler): (string | number | React.ReactNode)[] => [
hpa.getName(),
hpa.getNs(),
this.getTargets(hpa),
hpa.getMinPods(),
hpa.getMaxPods(),
hpa.getReplicas(),
hpa.spec.minReplicas,
hpa.spec.maxReplicas,
hpa.status.currentReplicas,
hpa.getAge(),
hpa.getConditions().map(({ type, tooltip, isReady }) => {
if (!isReady) return null;
if (!isReady) {
return null;
}
return (
<Badge
key={type}
@@ -77,23 +79,23 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
tooltip={tooltip}
className={cssNames(type.toLowerCase())}
/>
)
);
})
]}
renderItemMenu={(item: HorizontalPodAutoscaler) => {
return <HpaMenu object={item}/>
renderItemMenu={(item: HorizontalPodAutoscaler): JSX.Element => {
return <HpaMenu object={item}/>;
}}
/>
);
}
}
export function HpaMenu(props: KubeObjectMenuProps<HorizontalPodAutoscaler>) {
export function HpaMenu(props: KubeObjectMenuProps<HorizontalPodAutoscaler>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(hpaApi, {
Menu: HpaMenu,
})
});
@@ -1,3 +1,3 @@
export * from "./hpa"
export * from "./hpa-details"
export * from "./hpa.route"
export * from "./hpa";
export * from "./hpa-details";
export * from "./hpa.route";
@@ -23,7 +23,7 @@ export class ConfigMapDetails extends React.Component<Props> {
@observable isSaving = false;
@observable data = observable.map();
async componentDidMount() {
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(() => {
const { object: configMap } = this.props;
@@ -31,10 +31,10 @@ export class ConfigMapDetails extends React.Component<Props> {
this.data.replace(configMap.data); // refresh
}
})
])
]);
}
save = async () => {
save = async (): Promise<void> => {
const { object: configMap } = this.props;
try {
this.isSaving = true;
@@ -49,9 +49,11 @@ export class ConfigMapDetails extends React.Component<Props> {
}
}
render() {
render(): JSX.Element {
const { object: configMap } = this.props;
if (!configMap) return null;
if (!configMap) {
return null;
}
const data = Object.entries(this.data.toJSON());
return (
<div className="ConfigMapDetails">
@@ -71,11 +73,13 @@ export class ConfigMapDetails extends React.Component<Props> {
theme="round-black"
className="box grow"
value={value}
onChange={v => this.data.set(name, v)}
onChange={(v): void => {
this.data.set(name, v);
}}
/>
</div>
</div>
)
);
})
}
<Button
@@ -96,4 +100,4 @@ export class ConfigMapDetails extends React.Component<Props> {
apiManager.registerViews(configMapApi, {
Details: ConfigMapDetails
})
});
@@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
export const configMapsRoute: RouteProps = {
path: "/configmaps"
};
export interface ConfigMapsRouteParams {
}
export interface IConfigMapsRouteParams {
}
export const configMapsURL = buildURL<IConfigMapsRouteParams>(configMapsRoute.path);
export const configMapsURL = buildURL<ConfigMapsRouteParams>(configMapsRoute.path);
@@ -1,4 +1,4 @@
import "./config-maps.scss"
import "./config-maps.scss";
import * as React from "react";
import { observer } from "mobx-react";
@@ -8,7 +8,7 @@ import { configMapsStore } from "./config-maps.store";
import { ConfigMap, configMapApi } from "../../api/endpoints/configmap.api";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object";
import { IConfigMapsRouteParams } from "./config-maps.route";
import { ConfigMapsRouteParams } from "./config-maps.route";
import { apiManager } from "../../api/api-manager";
enum sortBy {
@@ -18,24 +18,24 @@ enum sortBy {
age = "age",
}
interface Props extends RouteComponentProps<IConfigMapsRouteParams> {
interface Props extends RouteComponentProps<ConfigMapsRouteParams> {
}
@observer
export class ConfigMaps extends React.Component<Props> {
render() {
render(): JSX.Element {
return (
<KubeObjectListLayout
className="ConfigMaps" store={configMapsStore}
sortingCallbacks={{
[sortBy.name]: (item: ConfigMap) => item.getName(),
[sortBy.namespace]: (item: ConfigMap) => item.getNs(),
[sortBy.keys]: (item: ConfigMap) => item.getKeys(),
[sortBy.age]: (item: ConfigMap) => item.metadata.creationTimestamp,
[sortBy.name]: (item: ConfigMap): string => item.getName(),
[sortBy.namespace]: (item: ConfigMap): string => item.getNs(),
[sortBy.keys]: (item: ConfigMap): string[] => item.getKeys(),
[sortBy.age]: (item: ConfigMap): string => item.metadata.creationTimestamp,
}}
searchFilters={[
(item: ConfigMap) => item.getSearchFields(),
(item: ConfigMap) => item.getKeys()
(item: ConfigMap): string[] => item.getSearchFields(),
(item: ConfigMap): string[] => item.getKeys()
]}
renderHeaderTitle={<Trans>Config Maps</Trans>}
renderTableHeader={[
@@ -44,26 +44,26 @@ export class ConfigMaps extends React.Component<Props> {
{ title: <Trans>Keys</Trans>, className: "keys", sortBy: sortBy.keys },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(configMap: ConfigMap) => [
renderTableContents={(configMap: ConfigMap): (string | number)[] => [
configMap.getName(),
configMap.getNs(),
configMap.getKeys().join(", "),
configMap.getAge(),
]}
renderItemMenu={(item: ConfigMap) => {
return <ConfigMapMenu object={item}/>
renderItemMenu={(item: ConfigMap): JSX.Element => {
return <ConfigMapMenu object={item}/>;
}}
/>
);
}
}
export function ConfigMapMenu(props: KubeObjectMenuProps<ConfigMap>) {
export function ConfigMapMenu(props: KubeObjectMenuProps<ConfigMap>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(configMapApi, {
Menu: ConfigMapMenu,
})
});
@@ -1,3 +1,3 @@
export * from "./config-maps.route"
export * from "./config-maps"
export * from "./config-map-details"
export * from "./config-maps.route";
export * from "./config-maps";
export * from "./config-map-details";
@@ -9,7 +9,7 @@ import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Input } from "../input";
import { systemName } from "../input/input.validators";
import { IResourceQuotaValues, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { ResourceQuotaValues, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { Select } from "../select";
import { Icon } from "../icon";
import { Button } from "../button";
@@ -20,11 +20,16 @@ import { SubTitle } from "../layout/sub-title";
interface Props extends DialogProps {
}
export interface QuotaOption {
label: string | JSX.Element;
value: string;
}
@observer
export class AddQuotaDialog extends React.Component<Props> {
@observable static isOpen = false;
static defaultQuotas: IResourceQuotaValues = {
static defaultQuotas: ResourceQuotaValues = {
"limits.cpu": "",
"limits.memory": "",
"requests.cpu": "",
@@ -53,43 +58,45 @@ export class AddQuotaDialog extends React.Component<Props> {
@observable namespace = this.defaultNamespace;
@observable quotas = AddQuotaDialog.defaultQuotas;
static open() {
static open(): void {
AddQuotaDialog.isOpen = true;
}
static close() {
static close(): void {
AddQuotaDialog.isOpen = false;
}
@computed get quotaEntries() {
@computed get quotaEntries(): [string, string][] {
return Object.entries(this.quotas)
.filter(([type, value]) => !!value.trim());
.filter(([_type, value]) => !!value.trim());
}
@computed get quotaOptions() {
return Object.keys(this.quotas).map(quota => {
const isCompute = quota.endsWith(".cpu") || quota.endsWith(".memory");
const isStorage = quota.endsWith(".storage") || quota === "persistentvolumeclaims";
const isCount = quota.startsWith("count/");
@computed get quotaOptions(): QuotaOption[] {
return Object.keys(this.quotas).map(value => {
const isCompute = value.endsWith(".cpu") || value.endsWith(".memory");
const isStorage = value.endsWith(".storage") || value === "persistentvolumeclaims";
const isCount = value.startsWith("count/");
const icon = isCompute ? "memory" : isStorage ? "storage" : isCount ? "looks_one" : "";
return {
label: icon ? <span className="nobr"><Icon material={icon}/> {quota}</span> : quota,
value: quota,
label: icon ? <span className="nobr"><Icon material={icon}/> {value}</span> : value,
value,
};
});
}
setQuota = () => {
if (!this.quotaSelectValue) return;
setQuota = (): void => {
if (!this.quotaSelectValue) {
return;
}
this.quotas[this.quotaSelectValue] = this.quotaInputValue;
this.quotaInputValue = "";
}
close = () => {
close = (): void => {
AddQuotaDialog.close();
}
reset = () => {
reset = (): void => {
this.quotaName = "";
this.quotaSelectValue = "";
this.quotaInputValue = "";
@@ -97,10 +104,10 @@ export class AddQuotaDialog extends React.Component<Props> {
this.quotas = AddQuotaDialog.defaultQuotas;
}
addQuota = async () => {
addQuota = async (): Promise<void> => {
try {
const { quotaName, namespace } = this;
const quotas = this.quotaEntries.reduce<IResourceQuotaValues>((quotas, [name, value]) => {
const quotas = this.quotaEntries.reduce<ResourceQuotaValues>((quotas, [name, value]) => {
quotas[name] = value;
return quotas;
}, {});
@@ -115,7 +122,7 @@ export class AddQuotaDialog extends React.Component<Props> {
}
}
onInputQuota = (evt: React.KeyboardEvent) => {
onInputQuota = (evt: React.KeyboardEvent): void => {
switch (evt.key) {
case "Enter":
this.setQuota();
@@ -124,7 +131,7 @@ export class AddQuotaDialog extends React.Component<Props> {
}
}
render() {
render(): JSX.Element {
const { ...dialogProps } = this.props;
const header = <h5><Trans>Create ResourceQuota</Trans></h5>;
return (
@@ -146,7 +153,9 @@ export class AddQuotaDialog extends React.Component<Props> {
required autoFocus
placeholder={_i18n._(t`ResourceQuota name`)}
validators={systemName}
value={this.quotaName} onChange={v => this.quotaName = v.toLowerCase()}
value={this.quotaName} onChange={(v): void => {
this.quotaName = v.toLowerCase();
}}
className="box grow"
/>
</div>
@@ -157,7 +166,7 @@ export class AddQuotaDialog extends React.Component<Props> {
placeholder={_i18n._(t`Namespace`)}
themeName="light"
className="box grow"
onChange={({ value }) => this.namespace = value}
onChange={({ value }): void => this.namespace = value}
/>
<SubTitle title={<Trans>Values</Trans>}/>
@@ -168,13 +177,15 @@ export class AddQuotaDialog extends React.Component<Props> {
placeholder={_i18n._(t`Select a quota..`)}
options={this.quotaOptions}
value={this.quotaSelectValue}
onChange={({ value }) => this.quotaSelectValue = value}
onChange={({ value }): void => this.quotaSelectValue = value}
/>
<Input
maxLength={10}
placeholder={_i18n._(t`Value`)}
value={this.quotaInputValue}
onChange={v => this.quotaInputValue = v}
onChange={(v): void => {
this.quotaInputValue = v;
}}
onKeyDown={this.onInputQuota}
className="box grow"
/>
@@ -191,14 +202,16 @@ export class AddQuotaDialog extends React.Component<Props> {
<div key={quota} className="quota flex gaps inline align-center">
<div className="name">{quota}</div>
<div className="value">{value}</div>
<Icon material="clear" onClick={() => this.quotas[quota] = ""}/>
<Icon material="clear" onClick={(): void => {
this.quotas[quota] = "";
}}/>
</div>
)
);
})}
</div>
</WizardStep>
</Wizard>
</Dialog>
)
);
}
}
@@ -1,3 +1,3 @@
export * from "./resource-quotas.route"
export * from "./resource-quotas"
export * from "./resource-quota-details"
export * from "./resource-quotas.route";
export * from "./resource-quotas";
export * from "./resource-quota-details";
@@ -17,22 +17,26 @@ interface Props extends KubeObjectDetailsProps<ResourceQuota> {
@observer
export class ResourceQuotaDetails extends React.Component<Props> {
renderQuotas = (quota: ResourceQuota) => {
const { hard, used } = quota.status
if (!hard || !used) return null
const transformUnit = (name: string, value: string) => {
renderQuotas = (quota: ResourceQuota): JSX.Element[] => {
const { hard, used } = quota.status;
if (!hard || !used) {
return null;
}
const transformUnit = (name: string, value: string): number => {
if (name.includes("memory") || name.includes("storage")) {
return unitsToBytes(value)
return unitsToBytes(value);
}
if (name.includes("cpu")) {
return cpuUnitsToNumber(value)
return cpuUnitsToNumber(value);
}
return parseInt(value)
}
return parseInt(value);
};
return Object.entries(hard).map(([name, value]) => {
if (!used[name]) return null
const current = transformUnit(name, used[name])
const max = transformUnit(name, value)
if (!used[name]) {
return null;
}
const current = transformUnit(name, used[name]);
const max = transformUnit(name, value);
return (
<div key={name} className={cssNames("param", kebabCase(name))}>
<span className="title">{name}</span>
@@ -45,13 +49,15 @@ export class ResourceQuotaDetails extends React.Component<Props> {
}
/>
</div>
)
})
);
});
}
render() {
render(): JSX.Element {
const { object: quota } = this.props;
if (!quota) return null;
if (!quota) {
return null;
}
return (
<div className="ResourceQuotaDetails">
<KubeObjectMeta object={quota}/>
@@ -91,4 +97,4 @@ export class ResourceQuotaDetails extends React.Component<Props> {
apiManager.registerViews(resourceQuotaApi, {
Details: ResourceQuotaDetails
})
});
@@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
export const resourceQuotaRoute: RouteProps = {
path: "/resourcequotas"
};
export interface ResourceQuotaRouteParams {
}
export interface IResourceQuotaRouteParams {
}
export const resourceQuotaURL = buildURL<IResourceQuotaRouteParams>(resourceQuotaRoute.path);
export const resourceQuotaURL = buildURL<ResourceQuotaRouteParams>(resourceQuotaRoute.path);
@@ -9,7 +9,7 @@ import { KubeObjectListLayout } from "../kube-object";
import { ResourceQuota, resourceQuotaApi } from "../../api/endpoints/resource-quota.api";
import { AddQuotaDialog } from "./add-quota-dialog";
import { resourceQuotaStore } from "./resource-quotas.store";
import { IResourceQuotaRouteParams } from "./resource-quotas.route";
import { ResourceQuotaRouteParams } from "./resource-quotas.route";
import { apiManager } from "../../api/api-manager";
enum sortBy {
@@ -18,24 +18,24 @@ enum sortBy {
age = "age"
}
interface Props extends RouteComponentProps<IResourceQuotaRouteParams> {
interface Props extends RouteComponentProps<ResourceQuotaRouteParams> {
}
@observer
export class ResourceQuotas extends React.Component<Props> {
render() {
render(): JSX.Element {
return (
<>
<KubeObjectListLayout
className="ResourceQuotas" store={resourceQuotaStore}
sortingCallbacks={{
[sortBy.name]: (item: ResourceQuota) => item.getName(),
[sortBy.namespace]: (item: ResourceQuota) => item.getNs(),
[sortBy.age]: (item: ResourceQuota) => item.metadata.creationTimestamp,
[sortBy.name]: (item: ResourceQuota): string => item.getName(),
[sortBy.namespace]: (item: ResourceQuota): string => item.getNs(),
[sortBy.age]: (item: ResourceQuota): string => item.metadata.creationTimestamp,
}}
searchFilters={[
(item: ResourceQuota) => item.getSearchFields(),
(item: ResourceQuota) => item.getName(),
(item: ResourceQuota): string[] => item.getSearchFields(),
(item: ResourceQuota): string => item.getName(),
]}
renderHeaderTitle={<Trans>Resource Quotas</Trans>}
renderTableHeader={[
@@ -43,16 +43,14 @@ export class ResourceQuotas extends React.Component<Props> {
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(resourceQuota: ResourceQuota) => [
renderTableContents={(resourceQuota: ResourceQuota): (string | number)[] => [
resourceQuota.getName(),
resourceQuota.getNs(),
resourceQuota.getAge(),
]}
renderItemMenu={(item: ResourceQuota) => {
return <ResourceQuotaMenu object={item}/>
}}
renderItemMenu={(item: ResourceQuota): JSX.Element => <ResourceQuotaMenu object={item} />}
addRemoveButtons={{
onAdd: () => AddQuotaDialog.open(),
onAdd: (): void => AddQuotaDialog.open(),
addTooltip: <Trans>Create new ResourceQuota</Trans>
}}
/>
@@ -62,7 +60,7 @@ export class ResourceQuotas extends React.Component<Props> {
}
}
export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>) {
export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
);
@@ -70,4 +68,4 @@ export function ResourceQuotaMenu(props: KubeObjectMenuProps<ResourceQuota>) {
apiManager.registerViews(resourceQuotaApi, {
Menu: ResourceQuotaMenu,
})
});
@@ -1,4 +1,4 @@
import "./add-secret-dialog.scss"
import "./add-secret-dialog.scss";
import React from "react";
import { observable } from "mobx";
@@ -14,7 +14,7 @@ import { SubTitle } from "../layout/sub-title";
import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Select, SelectOption } from "../select";
import { Icon } from "../icon";
import { IKubeObjectMetadata } from "../../api/kube-object";
import { KubeObjectMetadata } from "../../api/kube-object";
import { base64 } from "../../utils";
import { Notifications } from "../notifications";
import { showDetails } from "../../navigation";
@@ -23,34 +23,41 @@ import upperFirst from "lodash/upperFirst";
interface Props extends Partial<DialogProps> {
}
interface ISecretTemplateField {
interface SecretTemplateField {
key: string;
value?: string;
required?: boolean;
}
interface ISecretTemplate {
[field: string]: ISecretTemplateField[];
annotations?: ISecretTemplateField[];
labels?: ISecretTemplateField[];
data?: ISecretTemplateField[];
interface SecretTemplate {
[field: string]: SecretTemplateField[];
annotations?: SecretTemplateField[];
labels?: SecretTemplateField[];
data?: SecretTemplateField[];
}
type ISecretField = keyof ISecretTemplate;
type SecretField = keyof SecretTemplate;
type RecursivePartial<T> = {
[P in keyof T]?:
T[P] extends (infer U)[] ? RecursivePartial<U>[] :
T[P] extends object ? RecursivePartial<T[P]> :
T[P];
};
@observer
export class AddSecretDialog extends React.Component<Props> {
@observable static isOpen = false;
static open() {
static open(): void {
AddSecretDialog.isOpen = true;
}
static close() {
static close(): void {
AddSecretDialog.isOpen = false;
}
private secretTemplate: { [p: string]: ISecretTemplate } = {
private secretTemplate: Partial<Record<SecretType, SecretTemplate>> = {
[SecretType.Opaque]: {},
[SecretType.ServiceAccountToken]: {
annotations: [
@@ -60,7 +67,7 @@ export class AddSecretDialog extends React.Component<Props> {
},
}
get types() {
get types(): SecretType[] {
return Object.keys(this.secretTemplate) as SecretType[];
}
@@ -69,38 +76,39 @@ export class AddSecretDialog extends React.Component<Props> {
@observable namespace = "default";
@observable type = SecretType.Opaque;
reset = () => {
reset = (): void => {
this.name = "";
this.secret = this.secretTemplate;
}
close = () => {
close = (): void => {
AddSecretDialog.close();
}
private getDataFromFields = (fields: ISecretTemplateField[] = [], processValue?: (val: string) => string) => {
private getDataFromFields = (fields: SecretTemplateField[] = [], processValue?: (val: string) => string): any => {
return fields.reduce<any>((data, field) => {
const { key, value } = field;
if (key) {
data[key] = processValue ? processValue(value) : value;
}
return data;
}, {})
}, {});
}
createSecret = async () => {
createSecret = async (): Promise<void> => {
const { name, namespace, type } = this;
const { data = [], labels = [], annotations = [] } = this.secret[type];
const metadata: Partial<KubeObjectMetadata> = {
name,
namespace,
annotations: this.getDataFromFields(annotations),
labels: this.getDataFromFields(labels),
};
const secret: Partial<Secret> = {
type: type,
data: this.getDataFromFields(data, val => val ? base64.encode(val) : ""),
metadata: {
name: name,
namespace: namespace,
annotations: this.getDataFromFields(annotations),
labels: this.getDataFromFields(labels),
} as IKubeObjectMetadata
}
metadata: metadata as KubeObjectMetadata,
};
try {
const newSecret = await secretsApi.create({ namespace, name }, secret);
showDetails(newSecret.selfLink);
@@ -111,18 +119,18 @@ export class AddSecretDialog extends React.Component<Props> {
}
}
addField = (field: ISecretField) => {
addField = (field: SecretField): void => {
const fields = this.secret[this.type][field] || [];
fields.push({ key: "", value: "" });
this.secret[this.type][field] = fields;
}
removeField = (field: ISecretField, index: number) => {
removeField = (field: SecretField, index: number): void => {
const fields = this.secret[this.type][field] || [];
fields.splice(index, 1);
}
renderFields(field: ISecretField) {
renderFields(field: SecretField): JSX.Element {
const fields = this.secret[this.type][field] || [];
return (
<>
@@ -131,7 +139,7 @@ export class AddSecretDialog extends React.Component<Props> {
small
tooltip={_i18n._(t`Add field`)}
material="add_circle_outline"
onClick={() => this.addField(field)}
onClick={(): void => this.addField(field)}
/>
</SubTitle>
<div className="secret-fields">
@@ -145,14 +153,18 @@ export class AddSecretDialog extends React.Component<Props> {
title={key}
tabIndex={required ? -1 : 0}
readOnly={required}
value={key} onChange={v => item.key = v}
value={key} onChange={(v): void => {
item.key = v;
}}
/>
<Input
multiLine maxRows={5}
required={required}
className="value"
placeholder={_i18n._(t`Value`)}
value={value} onChange={v => item.value = v}
value={value} onChange={(v): void => {
item.value = v;
}}
/>
<Icon
small
@@ -160,17 +172,17 @@ export class AddSecretDialog extends React.Component<Props> {
tooltip={required ? <Trans>Required field</Trans> : <Trans>Remove field</Trans>}
className="remove-icon"
material="remove_circle_outline"
onClick={() => this.removeField(field, index)}
onClick={(): void => this.removeField(field, index)}
/>
</div>
)
);
})}
</div>
</>
)
);
}
render() {
render(): JSX.Element {
const { ...dialogProps } = this.props;
const { namespace, name, type } = this;
const header = <h5><Trans>Create Secret</Trans></h5>;
@@ -189,7 +201,9 @@ export class AddSecretDialog extends React.Component<Props> {
autoFocus required
placeholder={_i18n._(t`Name`)}
validators={systemName}
value={name} onChange={v => this.name = v}
value={name} onChange={(v): void => {
this.name = v;
}}
/>
</div>
<div className="flex auto gaps">
@@ -198,7 +212,7 @@ export class AddSecretDialog extends React.Component<Props> {
<NamespaceSelect
themeName="light"
value={namespace}
onChange={({ value }) => this.namespace = value}
onChange={({ value }): void => this.namespace = value}
/>
</div>
<div className="secret-type">
@@ -206,7 +220,7 @@ export class AddSecretDialog extends React.Component<Props> {
<Select
themeName="light"
options={this.types}
value={type} onChange={({ value }: SelectOption) => this.type = value}
value={type} onChange={({ value }: SelectOption): void => this.type = value}
/>
</div>
</div>
@@ -216,6 +230,6 @@ export class AddSecretDialog extends React.Component<Props> {
</WizardStep>
</Wizard>
</Dialog>
)
);
}
}
@@ -1,4 +1,4 @@
export * from "./secrets.route"
export * from "./secrets"
export * from "./secret-details"
export * from "./secrets.route";
export * from "./secrets";
export * from "./secret-details";
@@ -27,7 +27,7 @@ export class SecretDetails extends React.Component<Props> {
@observable data: { [name: string]: string } = {};
@observable revealSecret: { [name: string]: boolean } = {};
async componentDidMount() {
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(() => {
const { object: secret } = this.props;
@@ -36,10 +36,10 @@ export class SecretDetails extends React.Component<Props> {
this.revealSecret = {};
}
})
])
]);
}
saveSecret = async () => {
saveSecret = async (): Promise<void> => {
const { object: secret } = this.props;
this.isSaving = true;
try {
@@ -51,13 +51,15 @@ export class SecretDetails extends React.Component<Props> {
this.isSaving = false;
}
editData = (name: string, value: string, encoded: boolean) => {
editData = (name: string, value: string, encoded: boolean): void => {
this.data[name] = encoded ? value : base64.encode(value);
}
render() {
render(): JSX.Element {
const { object: secret } = this.props;
if (!secret) return null;
if (!secret) {
return null;
}
return (
<div className="SecretDetails">
<KubeObjectMeta object={secret}/>
@@ -86,18 +88,20 @@ export class SecretDetails extends React.Component<Props> {
theme="round-black"
className="box grow"
value={value || ""}
onChange={value => this.editData(name, value, !revealSecret)}
onChange={(value): void => this.editData(name, value, !revealSecret)}
/>
{decodedVal && (
<Icon
material={`visibility${revealSecret ? "" : "_off"}`}
tooltip={revealSecret ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
onClick={() => this.revealSecret[name] = !revealSecret}
onClick={(): void => {
this.revealSecret[name] = !revealSecret;
}}
/>)
}
</div>
</div>
)
);
})
}
<Button
@@ -115,4 +119,4 @@ export class SecretDetails extends React.Component<Props> {
apiManager.registerViews(secretsApi, {
Details: SecretDetails,
})
});
@@ -3,9 +3,9 @@ import { buildURL } from "../../navigation";
export const secretsRoute: RouteProps = {
path: "/secrets"
}
};
export interface ISecretsRouteParams {
export interface SecretsRouteParams {
}
export const secretsURL = buildURL(secretsRoute.path);
@@ -1,4 +1,4 @@
import "./secrets.scss"
import "./secrets.scss";
import * as React from "react";
import { observer } from "mobx-react";
@@ -7,7 +7,7 @@ import { RouteComponentProps } from "react-router";
import { Secret, secretsApi } from "../../api/endpoints";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { AddSecretDialog } from "./add-secret-dialog";
import { ISecretsRouteParams } from "./secrets.route";
import { SecretsRouteParams } from "./secrets.route";
import { KubeObjectListLayout } from "../kube-object";
import { Badge } from "../badge";
import { secretsStore } from "./secrets.store";
@@ -22,27 +22,27 @@ enum sortBy {
age = "age",
}
interface Props extends RouteComponentProps<ISecretsRouteParams> {
interface Props extends RouteComponentProps<SecretsRouteParams> {
}
@observer
export class Secrets extends React.Component<Props> {
render() {
render(): JSX.Element {
return (
<>
<KubeObjectListLayout
className="Secrets" store={secretsStore}
sortingCallbacks={{
[sortBy.name]: (item: Secret) => item.getName(),
[sortBy.namespace]: (item: Secret) => item.getNs(),
[sortBy.labels]: (item: Secret) => item.getLabels(),
[sortBy.keys]: (item: Secret) => item.getKeys(),
[sortBy.type]: (item: Secret) => item.type,
[sortBy.age]: (item: Secret) => item.metadata.creationTimestamp,
[sortBy.name]: (item: Secret): string => item.getName(),
[sortBy.namespace]: (item: Secret): string => item.getNs(),
[sortBy.labels]: (item: Secret): string[] => item.getLabels(),
[sortBy.keys]: (item: Secret): string[] => item.getKeys(),
[sortBy.type]: (item: Secret): string => item.type,
[sortBy.age]: (item: Secret): string => item.metadata.creationTimestamp,
}}
searchFilters={[
(item: Secret) => item.getSearchFields(),
(item: Secret) => item.getKeys(),
(item: Secret): string[] => item.getSearchFields(),
(item: Secret): string[] => item.getKeys(),
]}
renderHeaderTitle={<Trans>Secrets</Trans>}
renderTableHeader={[
@@ -53,7 +53,7 @@ export class Secrets extends React.Component<Props> {
{ title: <Trans>Type</Trans>, className: "type", sortBy: sortBy.type },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(secret: Secret) => [
renderTableContents={(secret: Secret): (string | JSX.Element[] | number)[] => [
secret.getName(),
secret.getNs(),
secret.getLabels().map(label => <Badge key={label} label={label}/>),
@@ -61,11 +61,11 @@ export class Secrets extends React.Component<Props> {
secret.type,
secret.getAge(),
]}
renderItemMenu={(item: Secret) => {
return <SecretMenu object={item}/>
renderItemMenu={(item: Secret): JSX.Element => {
return <SecretMenu object={item}/>;
}}
addRemoveButtons={{
onAdd: () => AddSecretDialog.open(),
onAdd: (): void => AddSecretDialog.open(),
addTooltip: <Trans>Create new Secret</Trans>
}}
/>
@@ -75,12 +75,12 @@ export class Secrets extends React.Component<Props> {
}
}
export function SecretMenu(props: KubeObjectMenuProps<Secret>) {
export function SecretMenu(props: KubeObjectMenuProps<Secret>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(secretsApi, {
Menu: SecretMenu,
})
});
@@ -1,12 +1,12 @@
import { RouteProps } from "react-router";
import { configMapsURL } from "../+config-maps";
import { Config } from "./config";
import { IURLParams } from "../../navigation";
import { URLParams } from "../../navigation";
export const configRoute: RouteProps = {
get path() {
return Config.tabRoutes.map(({ path }) => path).flat()
return Config.tabRoutes.map(({ path }) => path).flat();
}
}
};
export const configURL = (params?: IURLParams) => configMapsURL(params);
export const configURL = (params?: URLParams): string => configMapsURL(params);
@@ -10,7 +10,7 @@ import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config
import { configURL } from "./config.route";
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
import { buildURL } from "../../navigation";
import { isAllowedResource } from "../../api/rbac"
import { isAllowedResource } from "../../api/rbac";
export const certificatesURL = buildURL("/certificates");
export const issuersURL = buildURL("/issuers");
@@ -19,15 +19,15 @@ export const clusterIssuersURL = buildURL("/clusterissuers");
@observer
export class Config extends React.Component {
static get tabRoutes(): TabRoute[] {
const query = namespaceStore.getContextParams()
const routes: TabRoute[] = []
const query = namespaceStore.getContextParams();
const routes: TabRoute[] = [];
if (isAllowedResource("configmaps")) {
routes.push({
title: <Trans>ConfigMaps</Trans>,
component: ConfigMaps,
url: configMapsURL({ query }),
path: configMapsRoute.path,
})
});
}
if (isAllowedResource("secrets")) {
routes.push({
@@ -35,7 +35,7 @@ export class Config extends React.Component {
component: Secrets,
url: secretsURL({ query }),
path: secretsRoute.path,
})
});
}
if (isAllowedResource("resourcequotas")) {
routes.push({
@@ -43,7 +43,7 @@ export class Config extends React.Component {
component: ResourceQuotas,
url: resourceQuotaURL({ query }),
path: resourceQuotaRoute.path,
})
});
}
if (isAllowedResource("horizontalpodautoscalers")) {
routes.push({
@@ -51,12 +51,12 @@ export class Config extends React.Component {
component: HorizontalPodAutoscalers,
url: hpaURL({ query }),
path: hpaRoute.path,
})
});
}
return routes;
}
render() {
render(): JSX.Element {
const tabRoutes = Config.tabRoutes;
return (
<MainLayout className="Config" tabs={tabRoutes}>
@@ -65,6 +65,6 @@ export class Config extends React.Component {
<Redirect to={configURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
)
);
}
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./config.route"
export * from "./config"
export * from "./config.route";
export * from "./config";
@@ -1,7 +1,7 @@
import "./certificate-details.scss"
import "./certificate-details.scss";
import React from "react";
import moment from "moment"
import moment from "moment";
import { observer } from "mobx-react";
import { Link } from "react-router-dom";
import { Trans } from "@lingui/macro";
@@ -19,9 +19,11 @@ interface Props extends KubeObjectDetailsProps<Certificate> {
@observer
export class CertificateDetails extends React.Component<Props> {
render() {
render(): JSX.Element {
const { object: cert, className } = this.props;
if (!cert) return;
if (!cert) {
return;
}
const { spec, status } = cert;
const { acme, isCA, commonName, secretName, dnsNames, duration, ipAddresses, keyAlgorithm, keySize, organization, renewBefore } = spec;
const { lastFailureTime, notAfter } = status;
@@ -104,7 +106,7 @@ export class CertificateDetails extends React.Component<Props> {
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
);
})}
</DrawerItem>
@@ -126,7 +128,7 @@ export class CertificateDetails extends React.Component<Props> {
</DrawerItem>
)}
</div>
)
);
})}
</>
)}
@@ -139,4 +141,4 @@ export class CertificateDetails extends React.Component<Props> {
apiManager.registerViews(certificatesApi, {
Details: CertificateDetails
})
});
@@ -1,4 +1,4 @@
import "./certificates.scss"
import "./certificates.scss";
import * as React from "react";
import { observer } from "mobx-react";
@@ -24,10 +24,10 @@ enum sortBy {
@observer
export class Certificates extends React.Component<KubeObjectListLayoutProps> {
render() {
render(): JSX.Element {
const { store = apiManager.getStore(certificatesApi), ...layoutProps } = this.props;
if (!store) {
return <Spinner center/>
return <Spinner center/>;
}
return (
<KubeObjectListLayout
@@ -35,19 +35,19 @@ export class Certificates extends React.Component<KubeObjectListLayoutProps> {
store={store}
className="Certificates"
sortingCallbacks={{
[sortBy.name]: (item: Certificate) => item.getName(),
[sortBy.namespace]: (item: Certificate) => item.getNs(),
[sortBy.secretName]: (item: Certificate) => item.getSecretName(),
[sortBy.commonName]: (item: Certificate) => item.getCommonName(),
[sortBy.issuer]: (item: Certificate) => item.getIssuerName(),
[sortBy.type]: (item: Certificate) => item.getType(),
[sortBy.name]: (item: Certificate): string => item.getName(),
[sortBy.namespace]: (item: Certificate): string => item.getNs(),
[sortBy.secretName]: (item: Certificate): string => item.getSecretName(),
[sortBy.commonName]: (item: Certificate): string => item.getCommonName(),
[sortBy.issuer]: (item: Certificate): string => item.getIssuerName(),
[sortBy.type]: (item: Certificate): string => item.getType(),
}}
searchFilters={[
(item: Certificate) => item.getSearchFields(),
(item: Certificate) => item.getSecretName(),
(item: Certificate) => item.getCommonName(),
(item: Certificate) => item.getIssuerName(),
(item: Certificate) => item.getType(),
(item: Certificate): string[] => item.getSearchFields(),
(item: Certificate): string => item.getSecretName(),
(item: Certificate): string => item.getCommonName(),
(item: Certificate): string => item.getIssuerName(),
(item: Certificate): string => item.getType(),
]}
renderHeaderTitle={<Trans>Certificates</Trans>}
renderTableHeader={[
@@ -60,16 +60,16 @@ export class Certificates extends React.Component<KubeObjectListLayoutProps> {
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status" },
]}
renderTableContents={(cert: Certificate) => {
renderTableContents={(cert: Certificate): (string | JSX.Element | number | JSX.Element[])[] => {
return [
cert.getName(),
cert.getNs(),
cert.getCommonName(),
cert.getType(),
<Link to={cert.getIssuerDetailsUrl()} onClick={stopPropagation}>
<Link key="issuer" to={cert.getIssuerDetailsUrl()} onClick={stopPropagation}>
{cert.getIssuerName()}
</Link>,
<Link to={cert.getSecretDetailsUrl()} onClick={stopPropagation}>
<Link key="secret" to={cert.getSecretDetailsUrl()} onClick={stopPropagation}>
{cert.getSecretName()}
</Link>,
cert.getAge(),
@@ -81,25 +81,25 @@ export class Certificates extends React.Component<KubeObjectListLayoutProps> {
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
);
})
]
];
}}
renderItemMenu={(item: Certificate) => {
return <CertificateMenu object={item}/>
renderItemMenu={(item: Certificate): JSX.Element => {
return <CertificateMenu object={item}/>;
}}
/>
);
}
}
export function CertificateMenu(props: KubeObjectMenuProps<Certificate>) {
export function CertificateMenu(props: KubeObjectMenuProps<Certificate>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(certificatesApi, {
List: Certificates,
Menu: CertificateMenu
})
});
@@ -1,4 +1,4 @@
export * from "./certificates"
export * from "./certificate-details"
export * from "./issuers"
export * from "./issuer-details"
export * from "./certificates";
export * from "./certificate-details";
export * from "./issuers";
export * from "./issuer-details";
@@ -1,4 +1,4 @@
import "./issuer-details.scss"
import "./issuer-details.scss";
import React from "react";
import { observer } from "mobx-react";
@@ -21,7 +21,7 @@ interface Props extends KubeObjectDetailsProps<Issuer> {
@observer
export class IssuerDetails extends React.Component<Props> {
@autobind()
renderSecretLink(secretName: string) {
renderSecretLink(secretName: string): JSX.Element | string {
const namespace = this.props.object.getNs();
if (!namespace) {
return secretName;
@@ -34,12 +34,14 @@ export class IssuerDetails extends React.Component<Props> {
<Link to={secretDetailsUrl}>
{secretName}
</Link>
)
);
}
render() {
render(): JSX.Element {
const { object: issuer, className } = this.props;
if (!issuer) return;
if (!issuer) {
return;
}
const { renderSecretLink } = this;
const { spec: { acme, ca, vault, venafi }, status } = issuer;
return (
@@ -59,12 +61,12 @@ export class IssuerDetails extends React.Component<Props> {
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
);
})}
</DrawerItem>
{acme && (() => {
const { email, server, skipTLSVerify, privateKeySecretRef, solvers } = acme;
{acme && ((): JSX.Element => {
const { email, server, skipTLSVerify, privateKeySecretRef } = acme;
return (
<>
<DrawerTitle title="ACME"/>
@@ -86,10 +88,10 @@ export class IssuerDetails extends React.Component<Props> {
{skipTLSVerify ? <Trans>Yes</Trans> : <Trans>No</Trans>}
</DrawerItem>
</>
)
);
})()}
{ca && (() => {
{ca && ((): JSX.Element => {
const { secretName } = ca;
return (
<>
@@ -98,10 +100,10 @@ export class IssuerDetails extends React.Component<Props> {
{renderSecretLink(secretName)}
</DrawerItem>
</>
)
);
})()}
{vault && (() => {
{vault && ((): JSX.Element => {
const { auth, caBundle, path, server } = vault;
const { path: authPath, roleId, secretRef } = auth.appRole;
return (
@@ -130,10 +132,10 @@ export class IssuerDetails extends React.Component<Props> {
</DrawerItem>
)}
</>
)
);
})()}
{venafi && (() => {
{venafi && ((): JSX.Element => {
const { zone, cloud, tpp } = venafi;
return (
<>
@@ -161,7 +163,7 @@ export class IssuerDetails extends React.Component<Props> {
</>
)}
</>
)
);
})()}
<KubeEventDetails object={issuer}/>
@@ -172,4 +174,4 @@ export class IssuerDetails extends React.Component<Props> {
apiManager.registerViews([issuersApi, clusterIssuersApi], {
Details: IssuerDetails
})
});
@@ -1,4 +1,4 @@
import "./issuers.scss"
import "./issuers.scss";
import * as React from "react";
import { observer } from "mobx-react";
@@ -21,7 +21,7 @@ enum sortBy {
@observer
export class ClusterIssuers extends React.Component<KubeObjectListLayoutProps> {
render() {
render(): JSX.Element {
const store = apiManager.getStore(clusterIssuersApi);
return (
<Issuers
@@ -30,16 +30,16 @@ export class ClusterIssuers extends React.Component<KubeObjectListLayoutProps> {
store={store}
renderHeaderTitle={<Trans>Cluster Issuers</Trans>}
/>
)
);
}
}
@observer
export class Issuers extends React.Component<KubeObjectListLayoutProps> {
render() {
render(): JSX.Element {
const { store = apiManager.getStore(issuersApi), ...layoutProps } = this.props;
if (!store) {
return <Spinner center/>
return <Spinner center/>;
}
return (
<KubeObjectListLayout
@@ -48,15 +48,15 @@ export class Issuers extends React.Component<KubeObjectListLayoutProps> {
{...layoutProps}
className="Issuers"
sortingCallbacks={{
[sortBy.name]: (item: Issuer) => item.getName(),
[sortBy.namespace]: (item: Issuer) => item.getNs(),
[sortBy.type]: (item: Issuer) => item.getType(),
[sortBy.labels]: (item: Issuer) => item.getLabels(),
[sortBy.age]: (item: Issuer) => item.metadata.creationTimestamp,
[sortBy.name]: (item: Issuer): string => item.getName(),
[sortBy.namespace]: (item: Issuer): string => item.getNs(),
[sortBy.type]: (item: Issuer): string => item.getType(),
[sortBy.labels]: (item: Issuer): string[] => item.getLabels(),
[sortBy.age]: (item: Issuer): string=> item.metadata.creationTimestamp,
}}
searchFilters={[
(item: Issuer) => item.getSearchFields(),
(item: Issuer) => item.getType(),
(item: Issuer): string[] => item.getSearchFields(),
(item: Issuer): string => item.getType(),
]}
renderTableHeader={[
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
@@ -66,7 +66,7 @@ export class Issuers extends React.Component<KubeObjectListLayoutProps> {
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status" },
]}
renderTableContents={(issuer: Issuer) => [
renderTableContents={(issuer: Issuer): (string | JSX.Element[] | number)[] => [
issuer.getName(),
issuer.getNs(),
issuer.getLabels().map(label => <Badge key={label} label={label} title={label}/>),
@@ -80,24 +80,24 @@ export class Issuers extends React.Component<KubeObjectListLayoutProps> {
tooltip={tooltip}
className={cssNames({ [type.toLowerCase()]: isReady })}
/>
)
);
})
]}
renderItemMenu={(item: Issuer) => {
return <IssuerMenu object={item}/>
renderItemMenu={(item: Issuer): JSX.Element => {
return <IssuerMenu object={item}/>;
}}
/>
);
}
}
export function IssuerMenu(props: KubeObjectMenuProps<Issuer>) {
export function IssuerMenu(props: KubeObjectMenuProps<Issuer>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews([issuersApi, clusterIssuersApi], {
List: Issuers,
Menu: IssuerMenu,
})
});
@@ -20,9 +20,11 @@ interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
@observer
export class CRDDetails extends React.Component<Props> {
render() {
render(): JSX.Element {
const { object: crd } = this.props;
if (!crd) return null;
if (!crd) {
return null;
}
const { plural, singular, kind, listKind } = crd.getNames();
const printerColumns = crd.getPrinterColumns();
const validation = crd.getValidation();
@@ -59,7 +61,7 @@ export class CRDDetails extends React.Component<Props> {
<DrawerItem name={<Trans>Conditions</Trans>} className="conditions" labelsOnly>
{
crd.getConditions().map(condition => {
const { type, message, lastTransitionTime, status } = condition
const { type, message, lastTransitionTime, status } = condition;
return (
<Badge
key={type}
@@ -111,7 +113,7 @@ export class CRDDetails extends React.Component<Props> {
<Badge label={JSONPath}/>
</TableCell>
</TableRow>
)
);
})
}
</Table>
@@ -129,10 +131,10 @@ export class CRDDetails extends React.Component<Props> {
</>
}
</div>
)
);
}
}
apiManager.registerViews(crdApi, {
Details: CRDDetails
})
});
@@ -1,4 +1,4 @@
import "./crd-list.scss"
import "./crd-list.scss";
import React from "react";
import { Trans } from "@lingui/macro";
@@ -25,25 +25,28 @@ enum sortBy {
@observer
export class CrdList extends React.Component {
@computed get groups() {
return navigation.searchParams.getAsArray("groups")
@computed get groups(): string[] {
return navigation.searchParams.getAsArray("groups");
}
onGroupChange(group: string) {
onGroupChange(group: string): void {
const groups = [...this.groups];
const index = groups.findIndex(item => item == group);
if (index !== -1) groups.splice(index, 1);
else groups.push(group);
setQueryParams({ groups })
if (index !== -1) {
groups.splice(index, 1);
} else {
groups.push(group);
}
setQueryParams({ groups });
}
render() {
render(): JSX.Element {
const selectedGroups = this.groups;
const sortingCallbacks = {
[sortBy.kind]: (crd: CustomResourceDefinition) => crd.getResourceKind(),
[sortBy.group]: (crd: CustomResourceDefinition) => crd.getGroup(),
[sortBy.version]: (crd: CustomResourceDefinition) => crd.getVersion(),
[sortBy.scope]: (crd: CustomResourceDefinition) => crd.getScope(),
[sortBy.kind]: (crd: CustomResourceDefinition): string => crd.getResourceKind(),
[sortBy.group]: (crd: CustomResourceDefinition): string => crd.getGroup(),
[sortBy.version]: (crd: CustomResourceDefinition): string => crd.getVersion(),
[sortBy.scope]: (crd: CustomResourceDefinition): string => crd.getScope(),
};
return (
<KubeObjectListLayout
@@ -53,15 +56,19 @@ export class CrdList extends React.Component {
sortingCallbacks={sortingCallbacks}
searchFilters={Object.values(sortingCallbacks)}
filterItems={[
(items: CustomResourceDefinition[]) => {
return selectedGroups.length ? items.filter(item => selectedGroups.includes(item.getGroup())) : items
(items: CustomResourceDefinition[]): CustomResourceDefinition[] => {
return selectedGroups.length ? items.filter(item => selectedGroups.includes(item.getGroup())) : items;
}
]}
renderHeaderTitle={<Trans>Custom Resources</Trans>}
customizeHeader={() => {
customizeHeader={(): (JSX.Element | {filters: JSX.Element}) => {
let placeholder = <Trans>All groups</Trans>;
if (selectedGroups.length == 1) placeholder = <><Trans>Group</Trans>: {selectedGroups[0]}</>
if (selectedGroups.length >= 2) placeholder = <><Trans>Groups</Trans>: {selectedGroups.join(", ")}</>
if (selectedGroups.length == 1) {
placeholder = <><Trans>Group</Trans>: {selectedGroups[0]}</>;
}
if (selectedGroups.length >= 2) {
placeholder = <><Trans>Groups</Trans>: {selectedGroups.join(", ")}</>;
}
return {
// fixme: move to global filters
filters: (
@@ -69,9 +76,9 @@ export class CrdList extends React.Component {
className="group-select"
placeholder={placeholder}
options={Object.keys(crdStore.groups)}
onChange={({ value: group }: SelectOption) => this.onGroupChange(group)}
onChange={({ value: group }: SelectOption): void => this.onGroupChange(group)}
controlShouldRenderValue={false}
formatOptionLabel={({ value: group }: SelectOption) => {
formatOptionLabel={({ value: group }: SelectOption): JSX.Element => {
const isSelected = selectedGroups.includes(group);
return (
<div className="flex gaps align-center">
@@ -79,11 +86,11 @@ export class CrdList extends React.Component {
<span>{group}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
)
);
}}
/>
)
}
};
}}
renderTableHeader={[
{ title: <Trans>Resource</Trans>, className: "kind", sortBy: sortBy.kind },
@@ -92,29 +99,29 @@ export class CrdList extends React.Component {
{ title: <Trans>Scope</Trans>, className: "scope", sortBy: sortBy.scope },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(crd: CustomResourceDefinition) => {
renderTableContents={(crd: CustomResourceDefinition): (JSX.Element | string | number)[] => {
return [
<Link to={crd.getResourceUrl()} onClick={stopPropagation}>
<Link key="title" to={crd.getResourceUrl()} onClick={stopPropagation}>
{crd.getResourceTitle()}
</Link>,
crd.getGroup(),
crd.getVersion(),
crd.getScope(),
crd.getAge(),
]
];
}}
renderItemMenu={(item: CustomResourceDefinition) => {
return <CRDMenu object={item}/>
renderItemMenu={(item: CustomResourceDefinition): JSX.Element => {
return <CRDMenu object={item}/>;
}}
/>
)
);
}
}
export function CRDMenu(props: KubeObjectMenuProps<CustomResourceDefinition>) {
export function CRDMenu(props: KubeObjectMenuProps<CustomResourceDefinition>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(crdApi, {
@@ -13,6 +13,7 @@ import { apiManager } from "../../api/api-manager";
import { crdStore } from "./crd.store";
import { KubeObject } from "../../api/kube-object";
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { CustomResourceDefinition } from "client/api/endpoints/crd.api";
// fixme: provide type-safe check for status
interface Props extends KubeObjectDetailsProps<KubeObject & { status: any }> {
@@ -20,23 +21,25 @@ interface Props extends KubeObjectDetailsProps<KubeObject & { status: any }> {
@observer
export class CrdResourceDetails extends React.Component<Props> {
@computed get crd() {
@computed get crd(): CustomResourceDefinition {
return crdStore.getByObject(this.props.object);
}
@computed get CustomDetailsViews() {
return apiManager.getViews(this.props.object.selfLink).Details;
@computed get CustomDetailsViews(): React.ComponentType<KubeObjectDetailsProps<KubeObject>> | null {
return apiManager.getViews(this.props.object.selfLink).Details || null;
}
render() {
render(): JSX.Element {
const { object } = this.props;
const { crd, CustomDetailsViews } = this;
if (!object || !crd) return null;
if (!object || !crd) {
return null;
}
const className = cssNames("CrdResourceDetails", crd.getResourceKind());
const extraColumns = crd.getPrinterColumns();
const showStatus = !extraColumns.find(column => column.name == "Status") && object.status?.conditions;
if (CustomDetailsViews) {
return <CustomDetailsViews className={className} object={object}/>
return <CustomDetailsViews className={className} object={object}/>;
}
return (
<div className={className}>
@@ -47,7 +50,7 @@ export class CrdResourceDetails extends React.Component<Props> {
<DrawerItem key={name} name={name}>
{jsonPath.query(object, column.JSONPath.slice(1))}
</DrawerItem>
)
);
})}
{showStatus && (
<DrawerItem name={<Trans>Status</Trans>} className="status" labelsOnly>
@@ -64,6 +67,6 @@ export class CrdResourceDetails extends React.Component<Props> {
</DrawerItem>
)}
</div>
)
);
}
}
@@ -8,13 +8,15 @@ import { RouteComponentProps } from "react-router";
import { KubeObjectListLayout } from "../kube-object";
import { KubeObject } from "../../api/kube-object";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { ICRDRouteParams } from "./crd.route";
import { CRDRouteParams } from "./crd.route";
import { autorun, computed } from "mobx";
import { crdStore } from "./crd.store";
import { SortingCallback } from "../table";
import { apiManager } from "../../api/api-manager";
import { CustomResourceDefinition } from "client/api/endpoints/crd.api";
import { KubeObjectStore } from "client/kube-object.store";
interface Props extends RouteComponentProps<ICRDRouteParams> {
interface Props extends RouteComponentProps<CRDRouteParams> {
}
enum sortBy {
@@ -25,7 +27,7 @@ enum sortBy {
@observer
export class CrdResources extends React.Component<Props> {
componentDidMount() {
componentDidMount(): void {
disposeOnUnmount(this, [
autorun(() => {
const { store } = this;
@@ -33,32 +35,36 @@ export class CrdResources extends React.Component<Props> {
store.loadAll();
}
})
])
]);
}
@computed get crd() {
@computed get crd(): CustomResourceDefinition {
const { group, name } = this.props.match.params;
return crdStore.getByGroup(group, name);
}
@computed get store() {
if (!this.crd) return null
@computed get store(): KubeObjectStore<any> | null {
if (!this.crd) {
return null;
}
return apiManager.getStore(this.crd.getResourceApiBase());
}
render() {
render(): JSX.Element {
const { crd, store } = this;
if (!crd) return null;
if (!crd) {
return null;
}
const isNamespaced = crd.isNamespaced();
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
const sortingCallbacks: { [sortBy: string]: SortingCallback } = {
[sortBy.name]: (item: KubeObject) => item.getName(),
[sortBy.namespace]: (item: KubeObject) => item.getNs(),
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
}
};
extraColumns.forEach(column => {
sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.query(item, column.JSONPath.slice(1))
})
sortingCallbacks[column.name] = (item: KubeObject): any[] => jsonPath.query(item, column.JSONPath.slice(1));
});
// todo: merge extra columns and other params to predefined view
const { List } = apiManager.getViews(crd.getResourceApiBase());
const ListView = List || KubeObjectListLayout;
@@ -69,7 +75,7 @@ export class CrdResources extends React.Component<Props> {
store={store}
sortingCallbacks={sortingCallbacks}
searchFilters={[
(item: KubeObject) => item.getSearchFields(),
(item: KubeObject): string[] => item.getSearchFields(),
]}
renderHeaderTitle={crd.getResourceTitle()}
renderTableHeader={[
@@ -81,11 +87,11 @@ export class CrdResources extends React.Component<Props> {
title: name,
className: name.toLowerCase(),
sortBy: name
}
};
}),
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(crdInstance: KubeObject) => [
renderTableContents={(crdInstance: KubeObject): any[] => [
crdInstance.getName(),
isNamespaced && crdInstance.getNs(),
...extraColumns.map(column =>
@@ -93,20 +99,20 @@ export class CrdResources extends React.Component<Props> {
),
crdInstance.getAge(),
]}
renderItemMenu={(item: KubeObject) => {
return <CrdResourceMenu object={item}/>
renderItemMenu={(item: KubeObject): JSX.Element => {
return <CrdResourceMenu object={item}/>;
}}
/>
)
);
}
}
export function CrdResourceMenu(props: KubeObjectMenuProps<KubeObject>) {
export function CrdResourceMenu(props: KubeObjectMenuProps<KubeObject>): JSX.Element {
const { Menu } = apiManager.getViews(props.object.selfLink);
if (Menu) {
return <Menu {...props}/>
return <Menu {...props}/>;
}
return (
<KubeObjectMenu {...props}/>
)
);
}
@@ -3,24 +3,24 @@ import { buildURL } from "../../navigation";
export const crdRoute: RouteProps = {
path: "/crd"
}
};
export const crdDefinitionsRoute: RouteProps = {
path: crdRoute.path + "/definitions"
}
};
export const crdResourcesRoute: RouteProps = {
path: crdRoute.path + "/:group/:name"
}
};
export interface ICRDListQuery {
export interface CRDListQuery {
groups?: string;
}
export interface ICRDRouteParams {
export interface CRDRouteParams {
group: string;
name: string;
}
export const crdURL = buildURL<{}, ICRDListQuery>(crdDefinitionsRoute.path);
export const crdResourcesURL = buildURL<ICRDRouteParams>(crdResourcesRoute.path);
export const crdURL = buildURL<{}, CRDListQuery>(crdDefinitionsRoute.path);
export const crdResourcesURL = buildURL<CRDRouteParams>(crdResourcesRoute.path);
@@ -17,17 +17,17 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
// auto-init stores for crd-s
reaction(() => this.items.toJS(), items => {
items.forEach(this.initStore);
})
});
}
protected sortItems(items: CustomResourceDefinition[]) {
protected sortItems(items: CustomResourceDefinition[]): CustomResourceDefinition[] {
return super.sortItems(items, [
crd => crd.getGroup(),
crd => crd.getName(),
])
(crd): string => crd.getGroup(),
(crd): string => crd.getName(),
]);
}
protected initStore(crd: CustomResourceDefinition) {
protected initStore(crd: CustomResourceDefinition): void {
const apiBase = crd.getResourceApiBase();
let api = apiManager.getApi(apiBase);
if (!api) {
@@ -44,28 +44,35 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
}
}
@computed get groups() {
@computed get groups(): Record<string, CustomResourceDefinition[]> {
const groups: Record<string, CustomResourceDefinition[]> = {};
return this.items.reduce((groups, crd) => {
const group = crd.getGroup();
if (!groups[group]) groups[group] = [];
groups[group].push(crd);
return groups;
}, groups)
return this.items
.reduce((groups, crd) => {
const group = crd.getGroup();
if (!groups[group]) {
groups[group] = [];
}
groups[group].push(crd);
return groups;
}, groups);
}
getByGroup(group: string, pluralName: string) {
getByGroup(group: string, pluralName: string): CustomResourceDefinition {
const crdInGroup = this.groups[group];
if (!crdInGroup) return null;
if (!crdInGroup) {
return null;
}
return crdInGroup.find(crd => crd.getPluralName() === pluralName);
}
getByObject(obj: KubeObject) {
if (!obj) return null
getByObject(obj: KubeObject): CustomResourceDefinition {
if (!obj) {
return null;
}
const { kind, apiVersion } = obj;
return this.items.find(crd => {
return kind === crd.getResourceKind() && apiVersion === `${crd.getGroup()}/${crd.getVersion()}`
})
return kind === crd.getResourceKind() && apiVersion === `${crd.getGroup()}/${crd.getVersion()}`;
});
}
}
@@ -20,10 +20,10 @@ export class CustomResources extends React.Component {
url: crdURL(),
path: crdRoute.path,
}
]
];
}
render() {
render(): JSX.Element {
return (
<MainLayout>
<Switch>
@@ -5,4 +5,4 @@ export * from "./crd-resources";
export * from "./crd-resource-details";
// customized crd-s
export * from "./certmanager.k8s.io"
export * from "./certmanager.k8s.io";
@@ -19,9 +19,11 @@ interface Props extends KubeObjectDetailsProps<KubeEvent> {
@observer
export class EventDetails extends React.Component<Props> {
render() {
render(): JSX.Element {
const { object: event } = this.props;
if (!event) return;
if (!event) {
return;
}
const { message, reason, count, type, involvedObject } = event;
const { kind, name, namespace, fieldPath } = involvedObject;
return (
@@ -70,7 +72,7 @@ export class EventDetails extends React.Component<Props> {
</TableRow>
</Table>
</div>
)
);
}
}
@@ -7,20 +7,21 @@ import { KubeObject } from "../../api/kube-object";
import { Pod } from "../../api/endpoints/pods.api";
import { podsStore } from "../+workloads-pods/pods.store";
import { apiManager } from "../../api/api-manager";
import { IReactionDisposer } from "mobx";
@autobind()
export class EventStore extends KubeObjectStore<KubeEvent> {
api = eventApi
limit = 1000
protected bindWatchEventsUpdater() {
protected bindWatchEventsUpdater(): IReactionDisposer {
return super.bindWatchEventsUpdater(5000);
}
protected sortItems(items: KubeEvent[]) {
protected sortItems(items: KubeEvent[]): KubeEvent[] {
return super.sortItems(items, [
event => event.metadata.creationTimestamp
], "desc")
(event): string => event.metadata.creationTimestamp
], "desc");
}
getEventsByObject(obj: KubeObject): KubeEvent[] {
@@ -32,7 +33,7 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
});
}
getWarnings() {
getWarnings(): KubeEvent[] {
const warnings = this.items.filter(event => event.type == "Warning");
const groupsByInvolvedObject = groupBy(warnings, warning => warning.involvedObject.uid);
const eventsWithError = Object.values(groupsByInvolvedObject).map(events => {
@@ -40,7 +41,9 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
const { kind, uid } = recent.involvedObject;
if (kind == Pod.kind) { // Wipe out running pods
const pod = podsStore.items.find(pod => pod.getId() == uid);
if (!pod || (!pod.hasIssues() && pod.spec.priority < 500000)) return;
if (!pod || (!pod.hasIssues() && pod.spec.priority < 500000)) {
return;
}
}
return recent;
});
@@ -3,6 +3,6 @@ import { buildURL } from "../../navigation";
export const eventRoute: RouteProps = {
path: "/events"
}
};
export const eventsURL = buildURL(eventRoute.path);
+20 -19
View File
@@ -9,10 +9,11 @@ import { Trans } from "@lingui/macro";
import { KubeEvent } from "../../api/endpoints/events.api";
import { Tooltip, TooltipContent } from "../tooltip";
import { Link } from "react-router-dom";
import { cssNames, IClassName, stopPropagation } from "../../utils";
import { cssNames, ClassName, stopPropagation } from "../../utils";
import { Icon } from "../icon";
import { getDetailsUrl } from "../../navigation";
import { lookupApiLink } from "../../api/kube-api";
import { ItemObject } from "client/item.store";
enum sortBy {
namespace = "namespace",
@@ -23,7 +24,7 @@ enum sortBy {
}
interface Props extends Partial<KubeObjectListLayoutProps> {
className?: IClassName;
className?: ClassName;
compact?: boolean;
compactLimit?: number;
}
@@ -36,7 +37,7 @@ const defaultProps: Partial<Props> = {
export class Events extends React.Component<Props> {
static defaultProps = defaultProps as object;
render() {
render(): JSX.Element {
const { compact, compactLimit, className, ...layoutProps } = this.props;
const events = (
<KubeObjectListLayout
@@ -45,20 +46,20 @@ export class Events extends React.Component<Props> {
store={eventStore}
isSelectable={false}
sortingCallbacks={{
[sortBy.namespace]: (event: KubeEvent) => event.getNs(),
[sortBy.type]: (event: KubeEvent) => event.involvedObject.kind,
[sortBy.object]: (event: KubeEvent) => event.involvedObject.name,
[sortBy.count]: (event: KubeEvent) => event.count,
[sortBy.age]: (event: KubeEvent) => event.metadata.creationTimestamp,
[sortBy.namespace]: (event: KubeEvent): string => event.getNs(),
[sortBy.type]: (event: KubeEvent): string => event.involvedObject.kind,
[sortBy.object]: (event: KubeEvent): string => event.involvedObject.name,
[sortBy.count]: (event: KubeEvent): number => event.count,
[sortBy.age]: (event: KubeEvent): string => event.metadata.creationTimestamp,
}}
searchFilters={[
(event: KubeEvent) => event.getSearchFields(),
(event: KubeEvent) => event.message,
(event: KubeEvent) => event.getSource(),
(event: KubeEvent) => event.involvedObject.name,
(event: KubeEvent): string[] => event.getSearchFields(),
(event: KubeEvent): string => event.message,
(event: KubeEvent): string => event.getSource(),
(event: KubeEvent): string => event.involvedObject.name,
]}
renderHeaderTitle={<Trans>Events</Trans>}
customizeHeader={({ title, info }) => (
customizeHeader={({ title, info }): React.ReactNode => (
compact ? title : ({
info: (
<>
@@ -82,7 +83,7 @@ export class Events extends React.Component<Props> {
{ title: <Trans>Count</Trans>, className: "count", sortBy: sortBy.count },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(event: KubeEvent) => {
renderTableContents={(event: KubeEvent): (string | React.ReactNode | JSX.Element)[] => {
const { involvedObject, type, message } = event;
const { kind, name } = involvedObject;
const tooltipId = `message-${event.getId()}`;
@@ -106,18 +107,18 @@ export class Events extends React.Component<Props> {
},
event.getNs(),
kind,
<Link to={detailsUrl} title={name} onClick={stopPropagation}>{name}</Link>,
<Link key="name" to={detailsUrl} title={name} onClick={stopPropagation}>{name}</Link>,
event.getSource(),
event.count,
event.getAge(),
]
];
}}
virtual={!compact}
filterItems={[
items => compact ? items.slice(0, compactLimit) : items,
(items): ItemObject[] => compact ? items.slice(0, compactLimit) : items,
]}
/>
)
);
if (compact) {
return events;
}
@@ -125,6 +126,6 @@ export class Events extends React.Component<Props> {
<MainLayout>
{events}
</MainLayout>
)
);
}
}
@@ -6,7 +6,6 @@ import { Trans } from "@lingui/macro";
import { KubeObject } from "../../api/kube-object";
import { DrawerItem, DrawerTitle } from "../drawer";
import { cssNames } from "../../utils";
import { Icon } from "../icon";
import { eventStore } from "./event.store";
interface Props {
@@ -15,11 +14,11 @@ interface Props {
@observer
export class KubeEventDetails extends React.Component<Props> {
async componentDidMount() {
eventStore.loadAll();
async componentDidMount(): Promise<void> {
await eventStore.loadAll();
}
render() {
render(): JSX.Element {
const { object } = this.props;
const events = eventStore.getEventsByObject(object);
if (!events.length) {
@@ -27,7 +26,7 @@ export class KubeEventDetails extends React.Component<Props> {
<DrawerTitle className="flex gaps align-center">
<span><Trans>Events</Trans></span>
</DrawerTitle>
)
);
}
return (
<>
@@ -36,7 +35,7 @@ export class KubeEventDetails extends React.Component<Props> {
</DrawerTitle>
<div className="KubeEventDetails">
{events.map(evt => {
const { message, count, lastTimestamp, involvedObject } = evt
const { message, count, lastTimestamp, involvedObject } = evt;
return (
<div className="event" key={evt.getId()}>
<div className={cssNames("title", { warning: evt.isWarning() })}>
@@ -55,10 +54,10 @@ export class KubeEventDetails extends React.Component<Props> {
{lastTimestamp}
</DrawerItem>
</div>
)
);
})}
</div>
</>
)
);
}
}
@@ -21,11 +21,13 @@ const defaultProps: Partial<Props> = {
export class KubeEventIcon extends React.Component<Props> {
static defaultProps = defaultProps as object;
render() {
render(): JSX.Element {
const { object, showWarningsOnly, filterEvents } = this.props;
const events = eventStore.getEventsByObject(object);
let warnings = events.filter(evt => evt.isWarning());
if (filterEvents) warnings = filterEvents(warnings);
if (filterEvents) {
warnings = filterEvents(warnings);
}
if (!events.length || (showWarningsOnly && !warnings.length)) {
return null;
}
@@ -44,6 +46,6 @@ export class KubeEventIcon extends React.Component<Props> {
</TooltipContent>
)}
/>
)
);
}
}
@@ -1,4 +1,4 @@
import "./add-namespace-dialog.scss"
import "./add-namespace-dialog.scss";
import React from "react";
import { observable } from "mobx";
@@ -23,19 +23,19 @@ export class AddNamespaceDialog extends React.Component<Props> {
@observable static isOpen = false;
@observable namespace = "";
static open() {
static open(): void {
AddNamespaceDialog.isOpen = true;
}
static close() {
static close(): void {
AddNamespaceDialog.isOpen = false;
}
close = () => {
close = (): void => {
AddNamespaceDialog.close();
}
addNamespace = async () => {
addNamespace = async (): Promise<void> => {
const { namespace } = this;
const { onSuccess, onError } = this.props;
try {
@@ -47,7 +47,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
}
}
render() {
render(): JSX.Element {
const { ...dialogProps } = this.props;
const { namespace } = this;
const header = <h5><Trans>Create Namespace</Trans></h5>;
@@ -69,11 +69,14 @@ export class AddNamespaceDialog extends React.Component<Props> {
iconLeft="layers"
placeholder={_i18n._(t`Namespace`)}
validators={systemName}
value={namespace} onChange={v => this.namespace = v.toLowerCase()}
value={namespace}
onChange={(v): void => {
this.namespace = v.toLowerCase();
}}
/>
</WizardStep>
</Wizard>
</Dialog>
)
);
}
}
@@ -1,4 +1,4 @@
export * from "./namespaces.route"
export * from "./namespaces"
export * from "./namespace-details"
export * from "./add-namespace-dialog"
export * from "./namespaces.route";
export * from "./namespaces";
export * from "./namespace-details";
export * from "./add-namespace-dialog";
@@ -20,18 +20,20 @@ interface Props extends KubeObjectDetailsProps<Namespace> {
@observer
export class NamespaceDetails extends React.Component<Props> {
@computed get quotas() {
@computed get quotas(): ResourceQuota[] {
const namespace = this.props.object.getName();
return resourceQuotaStore.getAllByNs(namespace);
}
componentDidMount() {
resourceQuotaStore.loadAll();
async componentDidMount(): Promise<void> {
await resourceQuotaStore.loadAll();
}
render() {
render(): JSX.Element {
const { object: namespace } = this.props;
if (!namespace) return;
if (!namespace) {
return;
}
const status = namespace.getStatus();
return (
<div className="NamespaceDetails">
@@ -1,4 +1,4 @@
import "./namespace-select.scss"
import "./namespace-select.scss";
import React from "react";
import { computed } from "mobx";
@@ -11,7 +11,6 @@ import { namespaceStore } from "./namespace.store";
import { _i18n } from "../../i18n";
import { FilterIcon } from "../item-object-list/filter-icon";
import { FilterType } from "../item-object-list/page-filters.store";
import { isAllowedResource } from "../../api/rbac"
interface Props extends SelectProps {
showIcons?: boolean;
@@ -33,14 +32,14 @@ export class NamespaceSelect extends React.Component<Props> {
static defaultProps = defaultProps as object;
private unsubscribe = noop;
async componentDidMount() {
async componentDidMount(): Promise<void> {
if (!namespaceStore.isLoaded) {
await namespaceStore.loadAll();
}
this.unsubscribe = namespaceStore.subscribe();
}
componentWillUnmount() {
componentWillUnmount(): void {
this.unsubscribe();
}
@@ -54,19 +53,28 @@ export class NamespaceSelect extends React.Component<Props> {
return options;
}
formatOptionLabel = (option: SelectOption) => {
formatOptionLabel = (option: SelectOption): JSX.Element => {
const { showIcons } = this.props;
const { value, label } = option;
return label || (
<>
{showIcons && <Icon small material="layers"/>}
{value}
</>
);
if (label) {
return (
<>
{showIcons && <Icon small material="layers" />}
{value}
</>
);
}
}
render() {
const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props;
render(): JSX.Element {
const {
className,
showIcons: _showIcons,
showClusterOption: _showClusterOption,
clusterOptionLabel: _clusterOptionLabel,
customizeOptions: _customizeOptions,
...selectProps
} = this.props;
return (
<Select
className={cssNames("NamespaceSelect", className)}
@@ -81,19 +89,23 @@ export class NamespaceSelect extends React.Component<Props> {
@observer
export class NamespaceSelectFilter extends React.Component {
render() {
render(): JSX.Element {
const { contextNs, hasContext, toggleContext } = namespaceStore;
let placeholder = <Trans>All namespaces</Trans>;
if (contextNs.length == 1) placeholder = <Trans>Namespace: {contextNs[0]}</Trans>
if (contextNs.length >= 2) placeholder = <Trans>Namespaces: {contextNs.join(", ")}</Trans>
if (contextNs.length == 1) {
placeholder = <Trans>Namespace: {contextNs[0]}</Trans>;
}
if (contextNs.length >= 2) {
placeholder = <Trans>Namespaces: {contextNs.join(", ")}</Trans>;
}
return (
<NamespaceSelect
placeholder={placeholder}
closeMenuOnSelect={false}
isOptionSelected={() => false}
isOptionSelected={(): boolean => false}
controlShouldRenderValue={false}
onChange={({ value: namespace }: SelectOption) => toggleContext(namespace)}
formatOptionLabel={({ value: namespace }: SelectOption) => {
onChange={({ value: namespace }: SelectOption): void => toggleContext(namespace)}
formatOptionLabel={({ value: namespace }: SelectOption): JSX.Element => {
const isSelected = hasContext(namespace);
return (
<div className="flex gaps align-center">
@@ -101,9 +113,9 @@ export class NamespaceSelectFilter extends React.Component {
<span>{namespace}</span>
{isSelected && <Icon small material="check" className="box right"/>}
</div>
)
);
}}
/>
)
);
}
}
@@ -1,8 +1,8 @@
import { action, observable, reaction } from "mobx";
import { autobind, createStorage } from "../../utils";
import { autobind, StorageHelper } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints";
import { IQueryParams, navigation, setQueryParams } from "../../navigation";
import { QueryParams, navigation, setQueryParams } from "../../navigation";
import { apiManager } from "../../api/api-manager";
import { isAllowedResource } from "../..//api/rbac";
@@ -11,9 +11,9 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi;
contextNs = observable.array<string>();
protected storage = createStorage<string[]>("context_ns", this.contextNs);
protected storage = new StorageHelper<string[]>("context_ns", this.contextNs);
get initNamespaces() {
get initNamespaces(): string[] {
const fromUrl = navigation.searchParams.getAsArray("namespaces");
return fromUrl.length ? fromUrl : this.storage.get();
}
@@ -33,36 +33,34 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
});
}
getContextParams(): Partial<IQueryParams> {
getContextParams(): Partial<QueryParams> {
return {
namespaces: this.contextNs
}
};
}
protected updateUrl(namespaces: string[]) {
setQueryParams({ namespaces }, { replace: true })
protected updateUrl(namespaces: string[]): void {
setQueryParams({ namespaces }, { replace: true });
}
protected loadItems(namespaces?: string[]) {
protected loadItems(namespaces?: string[]): Promise<Namespace[]> {
if (!isAllowedResource("namespaces")) {
if (namespaces) {
return Promise.all(namespaces.map(name => this.getDummyNamespace(name)))
}
else {
return Promise.all(namespaces.map(name => this.getDummyNamespace(name)));
} else {
return new Promise<Namespace[]>(() => {
return []
})
return [];
});
}
}
if (namespaces) {
return Promise.all(namespaces.map(name => this.api.get({ name })))
}
else {
return Promise.all(namespaces.map(name => this.api.get({ name })));
} else {
return super.loadItems();
}
}
protected getDummyNamespace(name: string) {
protected getDummyNamespace(name: string): Namespace {
return new Namespace({
kind: "Namespace",
apiVersion: "v1",
@@ -72,25 +70,28 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
resourceVersion: "",
selfLink: `/api/v1/namespaces/${name}`
}
})
});
}
setContext(namespaces: string[]) {
setContext(namespaces: string[]): void {
this.contextNs.replace(namespaces);
}
hasContext(namespace: string | string[]) {
hasContext(namespace: string | string[]): boolean {
const context = Array.isArray(namespace) ? namespace : [namespace];
return context.every(namespace => this.contextNs.includes(namespace));
}
toggleContext(namespace: string) {
if (this.hasContext(namespace)) this.contextNs.remove(namespace);
else this.contextNs.push(namespace);
toggleContext(namespace: string): void {
if (this.hasContext(namespace)) {
this.contextNs.remove(namespace);
} else {
this.contextNs.push(namespace);
}
}
@action
reset() {
reset(): void {
super.reset();
this.contextNs.clear();
}
@@ -1,11 +1,11 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export const namespacesRoute: RouteProps = {
path: "/namespaces"
};
export interface NamespacesRouteParams {
}
export interface INamespacesRouteParams {
}
export const namespacesURL = buildURL<INamespacesRouteParams>(namespacesRoute.path)
export const namespacesURL = buildURL<NamespacesRouteParams>(namespacesRoute.path);
@@ -1,4 +1,4 @@
import "./namespaces.scss"
import "./namespaces.scss";
import * as React from "react";
import { Trans } from "@lingui/macro";
@@ -9,7 +9,7 @@ import { Badge } from "../badge";
import { RouteComponentProps } from "react-router";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object";
import { INamespacesRouteParams } from "./namespaces.route";
import { NamespacesRouteParams } from "./namespaces.route";
import { namespaceStore } from "./namespace.store";
import { apiManager } from "../../api/api-manager";
@@ -20,25 +20,25 @@ enum sortBy {
status = "status",
}
interface Props extends RouteComponentProps<INamespacesRouteParams> {
interface Props extends RouteComponentProps<NamespacesRouteParams> {
}
export class Namespaces extends React.Component<Props> {
render() {
render(): JSX.Element {
return (
<MainLayout>
<KubeObjectListLayout
isClusterScoped
className="Namespaces" store={namespaceStore}
sortingCallbacks={{
[sortBy.name]: (ns: Namespace) => ns.getName(),
[sortBy.labels]: (ns: Namespace) => ns.getLabels(),
[sortBy.age]: (ns: Namespace) => ns.metadata.creationTimestamp,
[sortBy.status]: (ns: Namespace) => ns.getStatus(),
[sortBy.name]: (ns: Namespace): string => ns.getName(),
[sortBy.labels]: (ns: Namespace): string[] => ns.getLabels(),
[sortBy.age]: (ns: Namespace): string => ns.metadata.creationTimestamp,
[sortBy.status]: (ns: Namespace): string => ns.getStatus(),
}}
searchFilters={[
(item: Namespace) => item.getSearchFields(),
(item: Namespace) => item.getStatus()
(item: Namespace): string[] => item.getSearchFields(),
(item: Namespace): string => item.getStatus()
]}
renderHeaderTitle={<Trans>Namespaces</Trans>}
renderTableHeader={[
@@ -47,33 +47,33 @@ export class Namespaces extends React.Component<Props> {
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
]}
renderTableContents={(item: Namespace) => [
renderTableContents={(item: Namespace): (string | JSX.Element[] | number | React.ReactNode)[] => [
item.getName(),
item.getLabels().map(label => <Badge key={label} label={label}/>),
item.getAge(),
{ title: item.getStatus(), className: item.getStatus().toLowerCase() },
]}
renderItemMenu={(item: Namespace) => {
return <NamespaceMenu object={item}/>
renderItemMenu={(item: Namespace): JSX.Element => {
return <NamespaceMenu object={item}/>;
}}
addRemoveButtons={{
addTooltip: <Trans>Add Namespace</Trans>,
onAdd: () => AddNamespaceDialog.open(),
onAdd: (): void => AddNamespaceDialog.open(),
}}
customizeTableRowProps={(item: Namespace) => ({
customizeTableRowProps={(item: Namespace): { disabled: boolean } => ({
disabled: item.getStatus() === NamespaceStatus.TERMINATING,
})}
/>
<AddNamespaceDialog/>
</MainLayout>
)
);
}
}
export function NamespaceMenu(props: KubeObjectMenuProps<Namespace>) {
export function NamespaceMenu(props: KubeObjectMenuProps<Namespace>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(namespacesApi, {
@@ -1,10 +1,9 @@
import "./endpoint-details.scss"
import "./endpoint-details.scss";
import React from "react";
import { observer } from "mobx-react";
import { t, Trans } from "@lingui/macro";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
import { Trans } from "@lingui/macro";
import { DrawerTitle } from "../drawer";
import { KubeEventDetails } from "../+events/kube-event-details";
import { KubeObjectDetailsProps } from "../kube-object";
import { Endpoint, endpointApi } from "../../api/endpoints";
@@ -18,19 +17,19 @@ interface Props extends KubeObjectDetailsProps<Endpoint> {
@observer
export class EndpointDetails extends React.Component<Props> {
render() {
render(): JSX.Element {
const { object: endpoint } = this.props;
if (!endpoint) return;
if (!endpoint) {
return;
}
return (
<div className="EndpointDetails">
<KubeObjectMeta object={endpoint}/>
<DrawerTitle title={<Trans>Subsets</Trans>}/>
{
endpoint.getEndpointSubsets().map((subset) => {
return(
<EndpointSubsetList subset={subset} endpoint={endpoint}/>
)
})
endpoint.getEndpointSubsets().map((subset, index) => (
<EndpointSubsetList key={`${index}`} subset={subset} endpoint={endpoint} />
))
}
<KubeEventDetails object={endpoint}/>
@@ -41,4 +40,4 @@ export class EndpointDetails extends React.Component<Props> {
apiManager.registerViews(endpointApi, {
Details: EndpointDetails,
})
});
@@ -1,10 +1,9 @@
import "./endpoint-subset-list.scss"
import "./endpoint-subset-list.scss";
import React from "react";
import { observer } from "mobx-react";
import { EndpointSubset, Endpoint, EndpointAddress} from "../../api/endpoints";
import { _i18n } from "../../i18n";
import { DrawerItem, DrawerTitle } from "../drawer";
import { Trans } from "@lingui/macro";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { autobind } from "../../utils";
@@ -20,21 +19,21 @@ interface Props {
@observer
export class EndpointSubsetList extends React.Component<Props> {
getAddressTableRow(ip: string) {
getAddressTableRow(ip: string): JSX.Element {
const { subset } = this.props;
const address = subset.getAddresses().find(address => address.getId() == ip);
return this.renderAddressTableRow(address)
return this.renderAddressTableRow(address);
}
@autobind()
getNotReadyAddressTableRow(ip: string) {
getNotReadyAddressTableRow(ip: string): JSX.Element {
const { subset} = this.props;
const address = subset.getNotReadyAddresses().find(address => address.getId() == ip);
return this.renderAddressTableRow(address)
return this.renderAddressTableRow(address);
}
@autobind()
renderAddressTable(addresses: EndpointAddress[], virtual: boolean) {
renderAddressTable(addresses: EndpointAddress[], virtual: boolean): JSX.Element {
return (
<div>
<div className="title flex gaps"><Trans>Addresses</Trans></div>
@@ -56,11 +55,11 @@ export class EndpointSubsetList extends React.Component<Props> {
}
</Table>
</div>
)
);
}
@autobind()
renderAddressTableRow(address: EndpointAddress) {
renderAddressTableRow(address: EndpointAddress): JSX.Element {
const { endpoint } = this.props;
return (
<TableRow
@@ -71,16 +70,16 @@ export class EndpointSubsetList extends React.Component<Props> {
<TableCell className="name">{address.hostname}</TableCell>
<TableCell className="target">
{ address.targetRef && (
<Link to={getDetailsUrl(lookupApiLink(address.getTargetRef(), endpoint))}>
{address.targetRef.name}
</Link>
<Link to={getDetailsUrl(lookupApiLink(address.getTargetRef(), endpoint))}>
{address.targetRef.name}
</Link>
)}
</TableCell>
</TableRow>
);
}
render() {
render(): JSX.Element {
const { subset } = this.props;
const addresses = subset.getAddresses();
const notReadyAddresses = subset.getNotReadyAddresses();
@@ -90,45 +89,45 @@ export class EndpointSubsetList extends React.Component<Props> {
return(
<div className="EndpointSubsetList flex column">
{addresses.length > 0 && (
<div>
<div className="title flex gaps"><Trans>Addresses</Trans></div>
<Table
items={addresses}
selectable={false}
virtual={addressesVirtual}
scrollable={false}
getTableRow={this.getAddressTableRow}
className="box grow"
>
<TableHead>
<TableCell className="ip">IP</TableCell>
<TableCell className="host"><Trans>Hostname</Trans></TableCell>
<TableCell className="target">Target</TableCell>
</TableHead>
{ !addressesVirtual && addresses.map(address => this.getAddressTableRow(address.getId())) }
</Table>
</div>
<div>
<div className="title flex gaps"><Trans>Addresses</Trans></div>
<Table
items={addresses}
selectable={false}
virtual={addressesVirtual}
scrollable={false}
getTableRow={this.getAddressTableRow}
className="box grow"
>
<TableHead>
<TableCell className="ip">IP</TableCell>
<TableCell className="host"><Trans>Hostname</Trans></TableCell>
<TableCell className="target">Target</TableCell>
</TableHead>
{ !addressesVirtual && addresses.map(address => this.getAddressTableRow(address.getId())) }
</Table>
</div>
)}
{notReadyAddresses.length > 0 && (
<div>
<div className="title flex gaps"><Trans>Not Ready Addresses</Trans></div>
<Table
items={notReadyAddresses}
selectable
virtual={notReadyAddressesVirtual}
scrollable={false}
getTableRow={this.getNotReadyAddressTableRow}
className="box grow"
>
<TableHead>
<TableCell className="ip">IP</TableCell>
<TableCell className="host"><Trans>Hostname</Trans></TableCell>
<TableCell className="target">Target</TableCell>
</TableHead>
{ !notReadyAddressesVirtual && notReadyAddresses.map(address => this.getNotReadyAddressTableRow(address.getId())) }
</Table>
</div>
<div>
<div className="title flex gaps"><Trans>Not Ready Addresses</Trans></div>
<Table
items={notReadyAddresses}
selectable
virtual={notReadyAddressesVirtual}
scrollable={false}
getTableRow={this.getNotReadyAddressTableRow}
className="box grow"
>
<TableHead>
<TableCell className="ip">IP</TableCell>
<TableCell className="host"><Trans>Hostname</Trans></TableCell>
<TableCell className="target">Target</TableCell>
</TableHead>
{ !notReadyAddressesVirtual && notReadyAddresses.map(address => this.getNotReadyAddressTableRow(address.getId())) }
</Table>
</div>
)}
<div className="title flex gaps"><Trans>Ports</Trans></div>
@@ -1,11 +1,11 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export const endpointRoute: RouteProps = {
path: "/endpoints"
}
};
export interface EndpointRouteParams {
}
export const endpointURL = buildURL<EndpointRouteParams>(endpointRoute.path)
export const endpointURL = buildURL<EndpointRouteParams>(endpointRoute.path);
@@ -1,10 +1,10 @@
import "./endpoints.scss"
import "./endpoints.scss";
import * as React from "react"
import * as React from "react";
import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router-dom"
import { EndpointRouteParams } from "./endpoints.route"
import { Endpoint, endpointApi } from "../../api/endpoints/endpoint.api"
import { RouteComponentProps } from "react-router-dom";
import { EndpointRouteParams } from "./endpoints.route";
import { Endpoint, endpointApi } from "../../api/endpoints/endpoint.api";
import { endpointStore } from "./endpoints.store";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object";
@@ -22,17 +22,17 @@ interface Props extends RouteComponentProps<EndpointRouteParams> {
@observer
export class Endpoints extends React.Component<Props> {
render() {
render(): JSX.Element {
return (
<KubeObjectListLayout
className="Endpoints" store={endpointStore}
sortingCallbacks={{
[sortBy.name]: (endpoint: Endpoint) => endpoint.getName(),
[sortBy.namespace]: (endpoint: Endpoint) => endpoint.getNs(),
[sortBy.age]: (endpoint: Endpoint) => endpoint.metadata.creationTimestamp,
[sortBy.name]: (endpoint: Endpoint): string => endpoint.getName(),
[sortBy.namespace]: (endpoint: Endpoint): string => endpoint.getNs(),
[sortBy.age]: (endpoint: Endpoint): string => endpoint.metadata.creationTimestamp,
}}
searchFilters={[
(endpoint: Endpoint) => endpoint.getSearchFields()
(endpoint: Endpoint): string[] => endpoint.getSearchFields()
]}
renderHeaderTitle={<Trans>Endpoints</Trans>}
renderTableHeader={[
@@ -41,32 +41,32 @@ export class Endpoints extends React.Component<Props> {
{ title: <Trans>Endpoints</Trans>, className: "endpoints" },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(endpoint: Endpoint) => [
renderTableContents={(endpoint: Endpoint): (string | number)[] => [
endpoint.getName(),
endpoint.getNs(),
endpoint.toString(),
endpoint.getAge(),
]}
renderItemMenu={(item: Endpoint) => {
return <EndpointMenu object={item}/>
renderItemMenu={(item: Endpoint): JSX.Element => {
return <EndpointMenu object={item}/>;
}}
tableProps={{
customRowHeights: (item: Endpoint, lineHeight, paddings) => {
customRowHeights: (item: Endpoint, lineHeight, paddings): number => {
const lines = item.getEndpointSubsets().length || 1;
return lines * lineHeight + paddings;
}
}}
/>
)
);
}
}
export function EndpointMenu(props: KubeObjectMenuProps<Endpoint>) {
export function EndpointMenu(props: KubeObjectMenuProps<Endpoint>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(endpointApi, {
Menu: EndpointMenu
})
});
@@ -1,3 +1,3 @@
export * from "./endpoints.route"
export * from "./endpoints"
export * from "./endpoint-details"
export * from "./endpoints.route";
export * from "./endpoints";
export * from "./endpoint-details";
@@ -1,3 +1,3 @@
export * from "./ingresses.route"
export * from "./ingresses"
export * from "./ingress-details"
export * from "./ingresses.route";
export * from "./ingresses";
export * from "./ingress-details";
@@ -2,21 +2,25 @@ import React, { useContext } from "react";
import { t } from "@lingui/macro";
import { observer } from "mobx-react";
import { ChartOptions, ChartPoint } from "chart.js";
import { IIngressMetrics, Ingress } from "../../api/endpoints";
import { IngressMetrics, Ingress } from "../../api/endpoints";
import { BarChart, memoryOptions } from "../chart";
import { normalizeMetrics, isMetricsEmpty } from "../../api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics";
import { ResourceMetricsContext, IResourceMetricsValue } from "../resource-metrics";
import { ResourceMetricsContext, ResourceMetricsValue } from "../resource-metrics";
import { _i18n } from "../../i18n";
type IContext = IResourceMetricsValue<Ingress, { metrics: IIngressMetrics }>;
type IContext = ResourceMetricsValue<Ingress, { metrics: IngressMetrics }>;
export const IngressCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
const id = object.getId();
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
if (!metrics) {
return null;
}
if (isMetricsEmpty(metrics)) {
return <NoMetrics/>;
}
const values = Object.values(metrics)
.map(normalizeMetrics)
@@ -69,13 +73,13 @@ export const IngressCharts = observer(() => {
scales: {
yAxes: [{
ticks: {
callback: value => value
callback: (value): any => value
}
}]
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
label: ({ datasetIndex, index }, { datasets }): string => {
const { label, data } = datasets[datasetIndex];
const value = data[index] as ChartPoint;
const chartTooltipSec = _i18n._(t`sec`);
@@ -94,4 +98,4 @@ export const IngressCharts = observer(() => {
data={{ datasets: datasets[tabId] }}
/>
);
})
});
@@ -25,13 +25,15 @@ export class IngressDetails extends React.Component<Props> {
ingressStore.reset();
});
componentWillUnmount() {
componentWillUnmount(): void {
ingressStore.reset();
}
renderPaths(ingress: Ingress) {
const { spec: { rules } } = ingress
if (!rules || !rules.length) return null
renderPaths(ingress: Ingress): JSX.Element[] {
const { spec: { rules } } = ingress;
if (!rules || !rules.length) {
return null;
}
return rules.map((rule, index) => {
return (
<div className="rules" key={index}>
@@ -48,7 +50,7 @@ export class IngressDetails extends React.Component<Props> {
</TableHead>
{
rule.http.paths.map((path, index) => {
const backend = `${path.backend.serviceName}:${path.backend.servicePort}`
const backend = `${path.backend.serviceName}:${path.backend.servicePort}`;
return (
<TableRow key={index}>
<TableCell className="path">{path.path || ""}</TableCell>
@@ -56,17 +58,17 @@ export class IngressDetails extends React.Component<Props> {
<p key={backend}>{backend}</p>
</TableCell>
</TableRow>
)
);
})
}
</Table>
)}
</div>
)
})
);
});
}
render() {
render(): JSX.Element {
const { object: ingress } = this.props;
if (!ingress) {
return null;
@@ -74,13 +76,13 @@ export class IngressDetails extends React.Component<Props> {
const { spec } = ingress;
const { metrics } = ingressStore;
const metricTabs = [
<Trans>Network</Trans>,
<Trans>Duration</Trans>,
<Trans key="network">Network</Trans>,
<Trans key="duration">Duration</Trans>,
];
return (
<div className="IngressDetails">
<ResourceMetrics
loader={() => ingressStore.loadMetrics(ingress)}
loader={(): Promise<void> => ingressStore.loadMetrics(ingress)}
tabs={metricTabs} object={ingress} params={{ metrics }}
>
<IngressCharts/>
@@ -104,10 +106,10 @@ export class IngressDetails extends React.Component<Props> {
<KubeEventDetails object={ingress}/>
</div>
)
);
}
}
apiManager.registerViews(ingressApi, {
Details: IngressDetails,
})
});
@@ -1,19 +1,19 @@
import { observable } from "mobx";
import { KubeObjectStore } from "../../kube-object.store";
import { autobind } from "../../utils";
import { IIngressMetrics, Ingress, ingressApi } from "../../api/endpoints";
import { IngressMetrics, Ingress, ingressApi } from "../../api/endpoints";
import { apiManager } from "../../api/api-manager";
@autobind()
export class IngressStore extends KubeObjectStore<Ingress> {
api = ingressApi;
@observable metrics: IIngressMetrics = null;
@observable metrics: IngressMetrics = null;
async loadMetrics(ingress: Ingress) {
async loadMetrics(ingress: Ingress): Promise<void> {
this.metrics = await this.api.getMetrics(ingress.getName(), ingress.getNs());
}
reset() {
reset(): void {
this.metrics = null;
}
}
@@ -1,11 +1,11 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export const ingressRoute: RouteProps = {
path: "/ingresses"
}
};
export interface IngressRouteParams {
}
export const ingressURL = buildURL<IngressRouteParams>(ingressRoute.path)
export const ingressURL = buildURL<IngressRouteParams>(ingressRoute.path);
@@ -1,10 +1,10 @@
import "./ingresses.scss"
import "./ingresses.scss";
import * as React from "react"
import * as React from "react";
import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router-dom"
import { IngressRouteParams } from "./ingresses.route"
import { Ingress, ingressApi } from "../../api/endpoints/ingress.api"
import { RouteComponentProps } from "react-router-dom";
import { IngressRouteParams } from "./ingresses.route";
import { Ingress, ingressApi } from "../../api/endpoints/ingress.api";
import { ingressStore } from "./ingress.store";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object";
@@ -22,18 +22,18 @@ interface Props extends RouteComponentProps<IngressRouteParams> {
@observer
export class Ingresses extends React.Component<Props> {
render() {
render(): JSX.Element {
return (
<KubeObjectListLayout
className="Ingresses" store={ingressStore}
sortingCallbacks={{
[sortBy.name]: (ingress: Ingress) => ingress.getName(),
[sortBy.namespace]: (ingress: Ingress) => ingress.getNs(),
[sortBy.age]: (ingress: Ingress) => ingress.metadata.creationTimestamp,
[sortBy.name]: (ingress: Ingress): string => ingress.getName(),
[sortBy.namespace]: (ingress: Ingress): string => ingress.getNs(),
[sortBy.age]: (ingress: Ingress): string => ingress.metadata.creationTimestamp,
}}
searchFilters={[
(ingress: Ingress) => ingress.getSearchFields(),
(ingress: Ingress) => ingress.getPorts(),
(ingress: Ingress): string[] => ingress.getSearchFields(),
(ingress: Ingress): string => ingress.getPorts(),
]}
renderHeaderTitle={<Trans>Ingresses</Trans>}
renderTableHeader={[
@@ -42,32 +42,32 @@ export class Ingresses extends React.Component<Props> {
{ title: <Trans>Rules</Trans>, className: "rules" },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(ingress: Ingress) => [
renderTableContents={(ingress: Ingress): (string | number | JSX.Element[])[] => [
ingress.getName(),
ingress.getNs(),
ingress.getRoutes().map(route => <p key={route}>{route}</p>),
ingress.getAge(),
]}
renderItemMenu={(item: Ingress) => {
return <IngressMenu object={item}/>
renderItemMenu={(item: Ingress): JSX.Element => {
return <IngressMenu object={item}/>;
}}
tableProps={{
customRowHeights: (item: Ingress, lineHeight, paddings) => {
customRowHeights: (item: Ingress, lineHeight, paddings): number => {
const lines = item.getRoutes().length || 1;
return lines * lineHeight + paddings;
}
}}
/>
)
);
}
}
export function IngressMenu(props: KubeObjectMenuProps<Ingress>) {
export function IngressMenu(props: KubeObjectMenuProps<Ingress>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(ingressApi, {
Menu: IngressMenu
})
});
@@ -1,3 +1,3 @@
export * from "./network-policies.route"
export * from "./network-policies"
export * from "./network-policy-details"
export * from "./network-policies.route";
export * from "./network-policies";
export * from "./network-policy-details";
@@ -1,11 +1,11 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export const networkPoliciesRoute: RouteProps = {
path: "/network-policies"
};
export interface NetworkPoliciesRouteParams {
}
export interface INetworkPoliciesRouteParams {
}
export const networkPoliciesURL = buildURL<INetworkPoliciesRouteParams>(networkPoliciesRoute.path);
export const networkPoliciesURL = buildURL<NetworkPoliciesRouteParams>(networkPoliciesRoute.path);
@@ -1,4 +1,4 @@
import "./network-policies.scss"
import "./network-policies.scss";
import * as React from "react";
import { observer } from "mobx-react";
@@ -7,7 +7,7 @@ import { RouteComponentProps } from "react-router-dom";
import { NetworkPolicy, networkPolicyApi } from "../../api/endpoints/network-policy.api";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object";
import { INetworkPoliciesRouteParams } from "./network-policies.route";
import { NetworkPoliciesRouteParams } from "./network-policies.route";
import { networkPolicyStore } from "./network-policy.store";
import { apiManager } from "../../api/api-manager";
@@ -17,22 +17,22 @@ enum sortBy {
age = "age",
}
interface Props extends RouteComponentProps<INetworkPoliciesRouteParams> {
interface Props extends RouteComponentProps<NetworkPoliciesRouteParams> {
}
@observer
export class NetworkPolicies extends React.Component<Props> {
render() {
render(): JSX.Element {
return (
<KubeObjectListLayout
className="NetworkPolicies" store={networkPolicyStore}
sortingCallbacks={{
[sortBy.name]: (item: NetworkPolicy) => item.getName(),
[sortBy.namespace]: (item: NetworkPolicy) => item.getNs(),
[sortBy.age]: (item: NetworkPolicy) => item.metadata.creationTimestamp,
[sortBy.name]: (item: NetworkPolicy): string => item.getName(),
[sortBy.namespace]: (item: NetworkPolicy): string => item.getNs(),
[sortBy.age]: (item: NetworkPolicy): string => item.metadata.creationTimestamp,
}}
searchFilters={[
(item: NetworkPolicy) => item.getSearchFields(),
(item: NetworkPolicy): string[] => item.getSearchFields(),
]}
renderHeaderTitle={<Trans>Network Policies</Trans>}
renderTableHeader={[
@@ -41,26 +41,26 @@ export class NetworkPolicies extends React.Component<Props> {
{ title: <Trans>Policy Types</Trans>, className: "type" },
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
]}
renderTableContents={(item: NetworkPolicy) => [
renderTableContents={(item: NetworkPolicy): (string | number)[] => [
item.getName(),
item.getNs(),
item.getTypes().join(", "),
item.getAge(),
]}
renderItemMenu={(item: NetworkPolicy) => {
return <NetworkPolicyMenu object={item}/>
renderItemMenu={(item: NetworkPolicy): JSX.Element => {
return <NetworkPolicyMenu object={item}/>;
}}
/>
)
);
}
}
export function NetworkPolicyMenu(props: KubeObjectMenuProps<NetworkPolicy>) {
export function NetworkPolicyMenu(props: KubeObjectMenuProps<NetworkPolicy>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(networkPolicyApi, {
Menu: NetworkPolicyMenu,
})
});
@@ -1,10 +1,10 @@
import "./network-policy-details.scss"
import "./network-policy-details.scss";
import get from "lodash/get";
import React, { Fragment } from "react";
import { t, Trans } from "@lingui/macro";
import { DrawerItem, DrawerTitle } from "../drawer";
import { IPolicyEgress, IPolicyIngress, IPolicyIpBlock, IPolicySelector, NetworkPolicy, networkPolicyApi } from "../../api/endpoints/network-policy.api";
import { PolicyEgress, PolicyIngress, PolicyIpBlock, PolicySelector, NetworkPolicy, networkPolicyApi } from "../../api/endpoints/network-policy.api";
import { Badge } from "../badge";
import { SubTitle } from "../layout/sub-title";
import { KubeEventDetails } from "../+events/kube-event-details";
@@ -19,18 +19,22 @@ interface Props extends KubeObjectDetailsProps<NetworkPolicy> {
@observer
export class NetworkPolicyDetails extends React.Component<Props> {
renderIngressFrom(ingress: IPolicyIngress) {
renderIngressFrom(ingress: PolicyIngress): JSX.Element {
const { from } = ingress;
if (!from) return null;
if (!from) {
return null;
}
return (
<>
<SubTitle title={<Trans>From</Trans>}/>
{from.map(item =>
Object.keys(item).map(key => {
const data = get(item, key)
const data = get(item, key);
if (key === "ipBlock") {
const { cidr, except } = data as IPolicyIpBlock;
if (!cidr) return
const { cidr, except } = data as PolicyIpBlock;
if (!cidr) {
return;
}
return (
<DrawerItem name={key} key={key}>
cidr: {cidr}, {" "}
@@ -38,9 +42,9 @@ export class NetworkPolicyDetails extends React.Component<Props> {
`except: ${except.join(", ")}`
}
</DrawerItem>
)
);
}
const selector: IPolicySelector = data
const selector: PolicySelector = data;
if (selector.matchLabels) {
return (
<DrawerItem name={key} key={key}>
@@ -51,9 +55,8 @@ export class NetworkPolicyDetails extends React.Component<Props> {
.join(", ")
}
</DrawerItem>
)
}
else {
);
} else {
return (<DrawerItem name={key} key={key}>(empty)</DrawerItem>);
}
})
@@ -62,17 +65,23 @@ export class NetworkPolicyDetails extends React.Component<Props> {
);
}
renderEgressTo(egress: IPolicyEgress) {
renderEgressTo(egress: PolicyEgress): JSX.Element | null {
const { to } = egress;
if (!to) return null;
if (!to) {
return null;
}
return (
<>
<SubTitle title={<Trans>To</Trans>}/>
{to.map(item => {
const { ipBlock } = item
if (!ipBlock) return
const { cidr, except } = ipBlock
if (!cidr) return
const { ipBlock } = item;
if (!ipBlock) {
return;
}
const { cidr, except } = ipBlock;
if (!cidr) {
return;
}
return (
<DrawerItem name="ipBlock" key={cidr}>
cidr: {cidr}, {" "}
@@ -80,13 +89,13 @@ export class NetworkPolicyDetails extends React.Component<Props> {
`except: ${except.join(", ")}`
}
</DrawerItem>
)
);
})}
</>
);
}
render() {
render(): JSX.Element | null {
const { object: policy } = this.props;
if (!policy) {
return null;
@@ -116,7 +125,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
</DrawerItem>
{this.renderIngressFrom(ingress)}
</Fragment>
)
);
})}
</>
)}
@@ -133,7 +142,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
</DrawerItem>
{this.renderEgressTo(egress)}
</Fragment>
)
);
})}
</>
)}
@@ -146,4 +155,4 @@ export class NetworkPolicyDetails extends React.Component<Props> {
apiManager.registerViews(networkPolicyApi, {
Details: NetworkPolicyDetails
})
});
@@ -1,3 +1,3 @@
export * from "./services.route"
export * from "./services"
export * from "./service-details"
export * from "./services.route";
export * from "./services";
export * from "./service-details";
@@ -14,36 +14,38 @@ interface Props {
@observer
export class ServiceDetailsEndpoint extends React.Component<Props> {
render() {
const { endpoint } = this.props
if (!endpoint && !endpointStore.isLoaded) return (
<div className="PodDetailsList flex justify-center"><Spinner/></div>
);
render(): JSX.Element {
const { endpoint } = this.props;
if (!endpoint && !endpointStore.isLoaded) {
return (
<div className="PodDetailsList flex justify-center"><Spinner/></div>
);
}
if (!endpoint) {
return null
return null;
}
return (
<div className="EndpointList flex column">
<div className="EndpointList flex column">
<Table
selectable
virtual={false}
scrollable={false}
className="box grow"
selectable
virtual={false}
scrollable={false}
className="box grow"
>
<TableHead>
<TableCell className="name" ><Trans>Name</Trans></TableCell>
<TableCell className="endpoints"><Trans>Endpoints</Trans></TableCell>
</TableHead>
<TableRow
key={endpoint.getId()}
nowrap
onClick={prevDefault(() => showDetails(endpoint.selfLink, false))}
>
<TableCell className="name">{endpoint.getName()}</TableCell>
<TableCell className="endpoints">{ endpoint.toString()}</TableCell>
</TableRow>
<TableHead>
<TableCell className="name" ><Trans>Name</Trans></TableCell>
<TableCell className="endpoints"><Trans>Endpoints</Trans></TableCell>
</TableHead>
<TableRow
key={endpoint.getId()}
nowrap
onClick={prevDefault(() => showDetails(endpoint.selfLink, false))}
>
<TableCell className="name">{endpoint.getName()}</TableCell>
<TableCell className="endpoints">{ endpoint.toString()}</TableCell>
</TableRow>
</Table>
</div>
)
);
}
}
@@ -1,4 +1,4 @@
import "./service-details.scss"
import "./service-details.scss";
import React from "react";
import { observer } from "mobx-react";
@@ -20,18 +20,20 @@ interface Props extends KubeObjectDetailsProps<Service> {
@observer
export class ServiceDetails extends React.Component<Props> {
componentDidMount() {
async componentDidMount(): Promise<void> {
if (!endpointStore.isLoaded) {
endpointStore.loadAll();
await endpointStore.loadAll();
}
endpointApi.watch()
endpointApi.watch();
}
render() {
render(): JSX.Element {
const { object: service } = this.props;
if (!service) return;
if (!service) {
return;
}
const { spec } = service;
const endpoint = endpointStore.getByName(service.getName(), service.getNs())
const endpoint = endpointStore.getByName(service.getName(), service.getNs());
return (
<div className="ServicesDetails">
<KubeObjectMeta object={service}/>
@@ -81,4 +83,4 @@ export class ServiceDetails extends React.Component<Props> {
apiManager.registerViews(serviceApi, {
Details: ServiceDetails,
})
});
@@ -1,15 +1,15 @@
import "./service-ports.scss"
import "./service-ports.scss";
import React from "react";
import { observer } from "mobx-react";
import { t } from "@lingui/macro";
import { Service, ServicePort } from "../../api/endpoints";
import { _i18n } from "../../i18n";
import { apiBase } from "../../api"
import { apiBase } from "../../api";
import { observable } from "mobx";
import { cssNames } from "../../utils";
import { Notifications } from "../notifications";
import { Spinner } from "../spinner"
import { Spinner } from "../spinner";
interface Props {
service: Service;
@@ -19,19 +19,20 @@ interface Props {
export class ServicePorts extends React.Component<Props> {
@observable waiting = false;
async portForward(port: ServicePort) {
async portForward(port: ServicePort): Promise<void> {
const { service } = this.props;
this.waiting = true;
apiBase.post(`/services/${service.getNs()}/${service.getName()}/port-forward/${port.port}`, {})
.catch(error => {
Notifications.error(error);
})
.finally(() => {
this.waiting = false;
});
try {
this.waiting = true;
await apiBase.post(`/services/${service.getNs()}/${service.getName()}/port-forward/${port.port}`, {});
} catch (error) {
Notifications.error(error);
} finally {
this.waiting = false;
}
}
render() {
render(): JSX.Element {
const { service } = this.props;
return (
<div className={cssNames("ServicePorts", { waiting: this.waiting })}>
@@ -39,7 +40,10 @@ export class ServicePorts extends React.Component<Props> {
service.getPorts().map((port) => {
return(
<p key={port.toString()}>
<span title={_i18n._(t`Open in a browser`)} onClick={() => this.portForward(port) }>
<span
title={_i18n._(t`Open in a browser`)}
onClick={(): Promise<void> => this.portForward(port) }
>
{port.toString()}
{this.waiting && (
<Spinner />
@@ -1,11 +1,11 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export const servicesRoute: RouteProps = {
path: "/services"
};
export interface ServicesRouteParams {
}
export interface IServicesRouteParams {
}
export const servicesURL = buildURL<IServicesRouteParams>(servicesRoute.path);
export const servicesURL = buildURL<ServicesRouteParams>(servicesRoute.path);
@@ -1,10 +1,10 @@
import "./services.scss"
import "./services.scss";
import * as React from "react";
import { observer } from "mobx-react";
import { Trans } from "@lingui/macro";
import { RouteComponentProps } from "react-router";
import { IServicesRouteParams } from "./services.route";
import { ServicesRouteParams } from "./services.route";
import { Service, serviceApi } from "../../api/endpoints/service.api";
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
import { KubeObjectListLayout } from "../kube-object";
@@ -23,29 +23,29 @@ enum sortBy {
status = "status",
}
interface Props extends RouteComponentProps<IServicesRouteParams> {
interface Props extends RouteComponentProps<ServicesRouteParams> {
}
@observer
export class Services extends React.Component<Props> {
render() {
render(): JSX.Element {
return (
<KubeObjectListLayout
className="Services" store={serviceStore}
sortingCallbacks={{
[sortBy.name]: (service: Service) => service.getName(),
[sortBy.namespace]: (service: Service) => service.getNs(),
[sortBy.selector]: (service: Service) => service.getSelector(),
[sortBy.ports]: (service: Service) => (service.spec.ports || []).map(({ port }) => port)[0],
[sortBy.clusterIp]: (service: Service) => service.getClusterIp(),
[sortBy.type]: (service: Service) => service.getType(),
[sortBy.age]: (service: Service) => service.metadata.creationTimestamp,
[sortBy.status]: (service: Service) => service.getStatus(),
[sortBy.name]: (service: Service): string => service.getName(),
[sortBy.namespace]: (service: Service): string => service.getNs(),
[sortBy.selector]: (service: Service): string[] => service.getSelector(),
[sortBy.ports]: (service: Service): number => (service.spec.ports || []).map(({ port }) => port)[0],
[sortBy.clusterIp]: (service: Service): string => service.getClusterIp(),
[sortBy.type]: (service: Service): string => service.getType(),
[sortBy.age]: (service: Service): string => service.metadata.creationTimestamp,
[sortBy.status]: (service: Service): string => service.getStatus(),
}}
searchFilters={[
(service: Service) => service.getSearchFields(),
(service: Service) => service.getSelector().join(" "),
(service: Service) => service.getPorts().join(" "),
(service: Service): string[] => service.getSearchFields(),
(service: Service): string => service.getSelector().join(" "),
(service: Service): string => service.getPorts().join(" "),
]}
renderHeaderTitle={<Trans>Services</Trans>}
renderTableHeader={[
@@ -59,31 +59,31 @@ export class Services extends React.Component<Props> {
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
]}
renderTableContents={(service: Service) => [
renderTableContents={(service: Service): (string | number | JSX.Element[] | React.ReactNode)[] => [
service.getName(),
service.getNs(),
service.getType(),
service.getClusterIp(),
service.spec.clusterIP,
service.getPorts().join(", "),
service.getExternalIps().join(", ") || "-",
service.getSelector().map(label => <Badge key={label} label={label}/>),
service.getAge(),
{ title: service.getStatus(), className: service.getStatus().toLowerCase() },
]}
renderItemMenu={(item: Service) => {
return <ServiceMenu object={item}/>
renderItemMenu={(item: Service): JSX.Element => {
return <ServiceMenu object={item}/>;
}}
/>
)
);
}
}
export function ServiceMenu(props: KubeObjectMenuProps<Service>) {
export function ServiceMenu(props: KubeObjectMenuProps<Service>): JSX.Element {
return (
<KubeObjectMenu {...props}/>
)
);
}
apiManager.registerViews(serviceApi, {
Menu: ServiceMenu
})
});
@@ -1,2 +1,2 @@
export * from "./network.route"
export * from "./network"
export * from "./network.route";
export * from "./network";
@@ -1,12 +1,12 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { Network } from "./network";
import { servicesURL } from "../+network-services";
import { IURLParams } from "../../navigation";
import { URLParams } from "../../navigation";
export const networkRoute: RouteProps = {
get path() {
return Network.tabRoutes.map(({ path }) => path).flat()
return Network.tabRoutes.map(({ path }) => path).flat();
}
}
};
export const networkURL = (params?: IURLParams) => servicesURL(params);
export const networkURL = (params?: URLParams): string => servicesURL(params);
@@ -1,4 +1,4 @@
import "./network.scss"
import "./network.scss";
import * as React from "react";
import { observer } from "mobx-react";
@@ -20,7 +20,7 @@ interface Props extends RouteComponentProps<{}> {
@observer
export class Network extends React.Component<Props> {
static get tabRoutes(): TabRoute[] {
const query = namespaceStore.getContextParams()
const query = namespaceStore.getContextParams();
const routes: TabRoute[] = [];
if (isAllowedResource("services")) {
routes.push({
@@ -28,7 +28,7 @@ export class Network extends React.Component<Props> {
component: Services,
url: servicesURL({ query }),
path: servicesRoute.path,
})
});
}
if (isAllowedResource("endpoints")) {
routes.push({
@@ -36,7 +36,7 @@ export class Network extends React.Component<Props> {
component: Endpoints,
url: endpointURL({ query }),
path: endpointRoute.path,
})
});
}
if (isAllowedResource("ingresses")) {
routes.push({
@@ -44,7 +44,7 @@ export class Network extends React.Component<Props> {
component: Ingresses,
url: ingressURL({ query }),
path: ingressRoute.path,
})
});
}
if (isAllowedResource("networkpolicies")) {
routes.push({
@@ -52,12 +52,12 @@ export class Network extends React.Component<Props> {
component: NetworkPolicies,
url: networkPoliciesURL({ query }),
path: networkPoliciesRoute.path,
})
});
}
return routes
return routes;
}
render() {
render(): JSX.Element {
const tabRoutes = Network.tabRoutes;
return (
<MainLayout className="Network" tabs={tabRoutes}>
@@ -66,6 +66,6 @@ export class Network extends React.Component<Props> {
<Redirect to={networkURL({ query: namespaceStore.getContextParams() })}/>
</Switch>
</MainLayout>
)
);
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
export * from "./nodes"
export * from "./nodes.route"
export * from "./node-menu"
export * from "./node-details"
export * from "./nodes";
export * from "./nodes.route";
export * from "./node-menu";
export * from "./node-details";
@@ -1,24 +1,28 @@
import React, { useContext } from "react";
import { t } from "@lingui/macro";
import { IClusterMetrics, Node } from "../../api/endpoints";
import { ClusterMetrics, Node } from "../../api/endpoints";
import { BarChart, cpuOptions, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { ResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { observer } from "mobx-react";
import { ChartOptions, ChartPoint } from "chart.js";
import { themeStore } from "../../theme.store";
import { _i18n } from "../../i18n";
type IContext = IResourceMetricsValue<Node, { metrics: IClusterMetrics }>;
type IContext = ResourceMetricsValue<Node, { metrics: ClusterMetrics }>;
export const NodeCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
const id = object.getId();
const { chartCapacityColor } = themeStore.activeTheme.colors;
if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
if (!metrics) {
return null;
}
if (isMetricsEmpty(metrics)) {
return <NoMetrics/>;
}
const values = Object.values(metrics).map(metric =>
normalizeMetrics(metric).data.result[0].values
@@ -26,11 +30,11 @@ export const NodeCharts = observer(() => {
const [
memoryUsage,
memoryRequests,
memoryLimits,
_memoryLimits,
memoryCapacity,
cpuUsage,
cpuRequests,
cpuLimits,
_cpuLimits,
cpuCapacity,
podUsage,
podCapacity,
@@ -127,20 +131,20 @@ export const NodeCharts = observer(() => {
scales: {
yAxes: [{
ticks: {
callback: value => value
callback: (value): any => value
}
}]
},
tooltips: {
callbacks: {
label: ({ datasetIndex, index }, { datasets }) => {
label: ({ datasetIndex, index }, { datasets }): string => {
const { label, data } = datasets[datasetIndex];
const value = data[index] as ChartPoint;
return `${label}: ${value.y}`;
}
}
}
}
};
const options = [cpuOptions, memoryOptions, memoryOptions, podOptions];
@@ -151,4 +155,4 @@ export const NodeCharts = observer(() => {
data={{ datasets: datasets[tabId] }}
/>
);
})
});
@@ -30,36 +30,38 @@ export class NodeDetails extends React.Component<Props> {
nodesStore.nodeMetrics = null;
});
async componentDidMount() {
async componentDidMount(): Promise<void> {
if (!podsStore.isLoaded) {
podsStore.loadAll();
await podsStore.loadAll();
}
}
componentWillUnmount() {
componentWillUnmount(): void {
nodesStore.nodeMetrics = null;
}
render() {
render(): JSX.Element {
const { object: node } = this.props;
if (!node) return;
const { status } = node
const { nodeInfo, addresses, capacity, allocatable } = status
if (!node) {
return;
}
const { status } = node;
const { nodeInfo, addresses, capacity, allocatable } = status;
const conditions = node.getActiveConditions();
const taints = node.getTaints()
const childPods = podsStore.getPodsByNode(node.getName())
const metrics = nodesStore.nodeMetrics
const taints = node.getTaints();
const childPods = podsStore.getPodsByNode(node.getName());
const metrics = nodesStore.nodeMetrics;
const metricTabs = [
<Trans>CPU</Trans>,
<Trans>Memory</Trans>,
<Trans>Disk</Trans>,
<Trans>Pods</Trans>,
<Trans key="cpu">CPU</Trans>,
<Trans key="memory">Memory</Trans>,
<Trans key="disk">Disk</Trans>,
<Trans key="pods">Pods</Trans>,
];
return (
<div className="NodeDetails">
{podsStore.isLoaded && (
<ResourceMetrics
loader={() => nodesStore.loadMetrics(node.getName())}
loader={(): Promise<void> => nodesStore.loadMetrics(node.getName())}
tabs={metricTabs} object={node} params={{ metrics }}
>
<NodeCharts/>
@@ -121,7 +123,7 @@ export class NodeDetails extends React.Component<Props> {
<DrawerItem name={<Trans>Conditions</Trans>} className="conditions" labelsOnly>
{
conditions.map(condition => {
const { type } = condition
const { type } = condition;
return (
<Badge
key={type}
@@ -138,7 +140,7 @@ export class NodeDetails extends React.Component<Props> {
</TooltipContent>
}
/>
)
);
})
}
</DrawerItem>
@@ -151,7 +153,7 @@ export class NodeDetails extends React.Component<Props> {
/>
<KubeEventDetails object={node}/>
</div>
)
);
}
}
@@ -9,36 +9,38 @@ import { Icon } from "../icon";
import { _i18n } from "../../i18n";
import { hideDetails } from "../../navigation";
export function NodeMenu(props: KubeObjectMenuProps<Node>) {
export function NodeMenu(props: KubeObjectMenuProps<Node>): JSX.Element {
const { object: node, toolbar } = props;
if (!node) return null;
if (!node) {
return null;
}
const nodeName = node.getName();
const sendToTerminal = (command: string) => {
const sendToTerminal = (command: string): void => {
terminalStore.sendCommand(command, {
enter: true,
newTab: true,
});
hideDetails();
}
};
const shell = () => {
const shell = (): void => {
createTerminalTab({
title: _i18n._(t`Node`) + `: ${nodeName}`,
node: nodeName,
});
hideDetails();
}
};
const cordon = () => {
const cordon = (): void => {
sendToTerminal(`kubectl cordon ${nodeName}`);
}
};
const unCordon = () => {
sendToTerminal(`kubectl uncordon ${nodeName}`)
}
const unCordon = (): void => {
sendToTerminal(`kubectl uncordon ${nodeName}`);
};
const drain = () => {
const drain = (): void => {
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
ConfirmDialog.open({
ok: () => sendToTerminal(command),
@@ -48,8 +50,8 @@ export function NodeMenu(props: KubeObjectMenuProps<Node>) {
<Trans>Are you sure you want to drain <b>{nodeName}</b>?</Trans>
</p>
),
})
}
});
};
return (
<KubeObjectMenu {...props}>
@@ -1,11 +1,11 @@
import { RouteProps } from "react-router"
import { RouteProps } from "react-router";
import { buildURL } from "../../navigation";
export const nodesRoute: RouteProps = {
path: "/nodes"
};
export interface NodesRouteParams {
}
export interface INodesRouteParams {
}
export const nodesURL = buildURL<INodesRouteParams>(nodesRoute.path)
export const nodesURL = buildURL<NodesRouteParams>(nodesRoute.path);
@@ -1,5 +1,5 @@
import { action, computed, observable } from "mobx"
import { clusterApi, IClusterMetrics, INodeMetrics, Node, nodesApi } from "../../api/endpoints";
import { action, computed, observable } from "mobx";
import { clusterApi, ClusterMetrics, NodeMetrics, Node, nodesApi } from "../../api/endpoints";
import { autobind } from "../../utils";
import { KubeObjectStore } from "../../kube-object.store";
import { apiManager } from "../../api/api-manager";
@@ -8,13 +8,13 @@ import { apiManager } from "../../api/api-manager";
export class NodesStore extends KubeObjectStore<Node> {
api = nodesApi
@observable metrics: Partial<INodeMetrics> = {};
@observable nodeMetrics: Partial<IClusterMetrics> = null;
@observable metrics: Partial<NodeMetrics> = {};
@observable nodeMetrics: Partial<ClusterMetrics> = null;
@observable metricsLoading = false;
@observable metricsLoaded = false;
@action
async loadUsageMetrics() {
async loadUsageMetrics(): Promise<void> {
this.metricsLoading = true;
try {
this.metrics = await nodesApi.getMetrics();
@@ -25,16 +25,16 @@ export class NodesStore extends KubeObjectStore<Node> {
}
@action
async loadMetrics(nodeName: string) {
async loadMetrics(nodeName: string): Promise<void> {
this.nodeMetrics = await clusterApi.getMetrics([nodeName]);
}
@computed get masterNodes() {
return this.items.filter(node => node.getRoleLabels().includes("master"))
@computed get masterNodes(): Node[] {
return this.items.filter(node => node.getRoleLabels().includes("master"));
}
@computed get workerNodes() {
return this.items.filter(node => !node.getRoleLabels().includes("master"))
@computed get workerNodes(): Node[] {
return this.items.filter(node => !node.getRoleLabels().includes("master"));
}
getLastMetricValues(node: Node, metricNames: string[]): number[] {
@@ -50,7 +50,7 @@ export class NodesStore extends KubeObjectStore<Node> {
result.metric.node,
result.metric.instance,
result.metric.kubernetes_node,
].includes(nodeName)
].includes(nodeName);
});
return result ? parseFloat(result.values.slice(-1)[0][1]) : 0;
} catch (e) {
@@ -59,7 +59,7 @@ export class NodesStore extends KubeObjectStore<Node> {
});
}
reset() {
reset(): void {
super.reset();
this.metrics = {};
this.nodeMetrics = null;
@@ -68,5 +68,5 @@ export class NodesStore extends KubeObjectStore<Node> {
}
}
export const nodesStore = new NodesStore()
export const nodesStore = new NodesStore();
apiManager.registerStore(nodesApi, nodesStore);
+46 -40
View File
@@ -4,12 +4,12 @@ import * as React from "react";
import { observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { t, Trans } from "@lingui/macro";
import { cssNames, interval } from "../../utils";
import { cssNames, IntervalManager } from "../../utils";
import { MainLayout } from "../layout/main-layout";
import { nodesStore } from "./nodes.store";
import { podsStore } from "../+workloads-pods/pods.store";
import { KubeObjectListLayout } from "../kube-object";
import { INodesRouteParams } from "./nodes.route";
import { NodesRouteParams } from "./nodes.route";
import { Node, nodesApi } from "../../api/endpoints/nodes.api";
import { NodeMenu } from "./node-menu";
import { LineProgress } from "../line-progress";
@@ -33,24 +33,26 @@ enum sortBy {
status = "status",
}
interface Props extends RouteComponentProps<INodesRouteParams> {
interface Props extends RouteComponentProps<NodesRouteParams> {
}
@observer
export class Nodes extends React.Component<Props> {
private metricsWatcher = interval(30, () => nodesStore.loadUsageMetrics());
private metricsWatcher = new IntervalManager(30, () => nodesStore.loadUsageMetrics());
componentDidMount() {
componentDidMount(): void {
this.metricsWatcher.start(true);
}
componentWillUnmount() {
componentWillUnmount(): void {
this.metricsWatcher.stop();
}
renderCpuUsage(node: Node) {
renderCpuUsage(node: Node): JSX.Element {
const metrics = nodesStore.getLastMetricValues(node, ["cpuUsage", "cpuCapacity"]);
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
if (!metrics || !metrics[1]) {
return <LineProgress value={0}/>;
}
const usage = metrics[0];
const cores = metrics[1];
return (
@@ -59,12 +61,14 @@ export class Nodes extends React.Component<Props> {
value={usage}
tooltip={_i18n._(t`CPU:`) + ` ${Math.ceil(usage * 100) / cores}\%, ` + _i18n._(t`cores:`) + ` ${cores}`}
/>
)
);
}
renderMemoryUsage(node: Node) {
renderMemoryUsage(node: Node): JSX.Element {
const metrics = nodesStore.getLastMetricValues(node, ["memoryUsage", "memoryCapacity"]);
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
if (!metrics || !metrics[1]) {
return <LineProgress value={0}/>;
}
const usage = metrics[0];
const capacity = metrics[1];
return (
@@ -73,12 +77,14 @@ export class Nodes extends React.Component<Props> {
value={usage}
tooltip={_i18n._(t`Memory:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`}
/>
)
);
}
renderDiskUsage(node: Node): any {
renderDiskUsage(node: Node): JSX.Element {
const metrics = nodesStore.getLastMetricValues(node, ["fsUsage", "fsSize"]);
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
if (!metrics || !metrics[1]) {
return <LineProgress value={0}/>;
}
const usage = metrics[0];
const capacity = metrics[1];
return (
@@ -87,17 +93,17 @@ export class Nodes extends React.Component<Props> {
value={usage}
tooltip={_i18n._(t`Disk:`) + ` ${Math.ceil(usage * 100 / capacity)}%, ${bytesToUnits(usage, 3)}`}
/>
)
);
}
renderConditions(node: Node) {
renderConditions(node: Node): JSX.Element[] {
if (!node.status.conditions) {
return null
return null;
}
const conditions = node.getActiveConditions();
return conditions.map(condition => {
const { type } = condition
const tooltipId = `node-${node.getName()}-condition-${type}`
const { type } = condition;
const tooltipId = `node-${node.getName()}-condition-${type}`;
return (
<div key={type} id={tooltipId} className={cssNames("condition", kebabCase(type))}>
{type}
@@ -111,11 +117,11 @@ export class Nodes extends React.Component<Props> {
)}
</TooltipContent>
</Tooltip>
</div>)
})
</div>);
});
}
render() {
render(): JSX.Element {
return (
<MainLayout>
<KubeObjectListLayout
@@ -125,21 +131,21 @@ export class Nodes extends React.Component<Props> {
dependentStores={[podsStore]}
isSelectable={false}
sortingCallbacks={{
[sortBy.name]: (node: Node) => node.getName(),
[sortBy.cpu]: (node: Node) => nodesStore.getLastMetricValues(node, ["cpuUsage"]),
[sortBy.memory]: (node: Node) => nodesStore.getLastMetricValues(node, ["memoryUsage"]),
[sortBy.disk]: (node: Node) => nodesStore.getLastMetricValues(node, ["fsUsage"]),
[sortBy.conditions]: (node: Node) => node.getNodeConditionText(),
[sortBy.taints]: (node: Node) => node.getTaints().length,
[sortBy.roles]: (node: Node) => node.getRoleLabels(),
[sortBy.age]: (node: Node) => node.metadata.creationTimestamp,
[sortBy.version]: (node: Node) => node.getKubeletVersion(),
[sortBy.name]: (node: Node): string => node.getName(),
[sortBy.cpu]: (node: Node): number[] => nodesStore.getLastMetricValues(node, ["cpuUsage"]),
[sortBy.memory]: (node: Node): number[] => nodesStore.getLastMetricValues(node, ["memoryUsage"]),
[sortBy.disk]: (node: Node): number[] => nodesStore.getLastMetricValues(node, ["fsUsage"]),
[sortBy.conditions]: (node: Node): string => node.getNodeConditionText(),
[sortBy.taints]: (node: Node): number => node.getTaints().length,
[sortBy.roles]: (node: Node): string => node.getRoleLabels(),
[sortBy.age]: (node: Node): string => node.metadata.creationTimestamp,
[sortBy.version]: (node: Node): string => node.getKubeletVersion(),
}}
searchFilters={[
(node: Node) => node.getSearchFields(),
(node: Node) => node.getRoleLabels(),
(node: Node) => node.getKubeletVersion(),
(node: Node) => node.getNodeConditionText(),
(node: Node): string[] => node.getSearchFields(),
(node: Node): string => node.getRoleLabels(),
(node: Node): string => node.getKubeletVersion(),
(node: Node): string => node.getNodeConditionText(),
]}
renderHeaderTitle={<Trans>Nodes</Trans>}
renderTableHeader={[
@@ -153,7 +159,7 @@ export class Nodes extends React.Component<Props> {
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
{ title: <Trans>Conditions</Trans>, className: "conditions", sortBy: sortBy.conditions },
]}
renderTableContents={(node: Node) => {
renderTableContents={(node: Node): (string | number | JSX.Element | JSX.Element[])[] => {
const tooltipId = `node-taints-${node.getId()}`;
return [
node.getName(),
@@ -170,14 +176,14 @@ export class Nodes extends React.Component<Props> {
node.status.nodeInfo.kubeletVersion,
node.getAge(),
this.renderConditions(node),
]
];
}}
renderItemMenu={(item: Node) => {
return <NodeMenu object={item}/>
renderItemMenu={(item: Node): JSX.Element => {
return <NodeMenu object={item}/>;
}}
/>
</MainLayout>
)
);
}
}
Loaded 100 of 470 files, more files were not shown because too many files have changed in this diff. Show more