mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
cleanup Lens repo with tighter linting
Signed-off-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
1 parent
e468105143
commit
b1ff34879a
470 files changed
+8447
-7443
No files matched your search
@@ -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'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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./config.route"
|
||||
export * from "./config"
|
||||
export * from "./config.route";
|
||||
export * from "./config";
|
||||
+9
-7
@@ -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";
|
||||
+18
-16
@@ -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);
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
Reference in new issue
Block a user