mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add eslint rule padding-line-between-statements (#1593)
Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
This commit is contained in:
1 parent
7b77f18376
commit
dcf253e7d5
401 files changed
+2018
-40
No files matched your search
@@ -68,6 +68,7 @@ export class AddCluster extends React.Component {
|
||||
Notifications.error(
|
||||
<div>Can't setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
|
||||
);
|
||||
|
||||
if (throwError) {
|
||||
throw err;
|
||||
}
|
||||
@@ -82,12 +83,14 @@ export class AddCluster extends React.Component {
|
||||
switch (this.sourceTab) {
|
||||
case KubeConfigSourceTab.FILE:
|
||||
const contexts = this.getContexts(this.kubeConfigLocal);
|
||||
|
||||
this.kubeContexts.replace(contexts);
|
||||
break;
|
||||
case KubeConfigSourceTab.TEXT:
|
||||
try {
|
||||
this.error = "";
|
||||
const contexts = this.getContexts(loadConfig(this.customConfig || "{}"));
|
||||
|
||||
this.kubeContexts.replace(contexts);
|
||||
} catch (err) {
|
||||
this.error = String(err);
|
||||
@@ -102,9 +105,11 @@ export class AddCluster extends React.Component {
|
||||
|
||||
getContexts(config: KubeConfig): Map<string, KubeConfig> {
|
||||
const contexts = new Map();
|
||||
|
||||
splitConfig(config).forEach(config => {
|
||||
contexts.set(config.currentContext, config);
|
||||
});
|
||||
|
||||
return contexts;
|
||||
}
|
||||
|
||||
@@ -116,6 +121,7 @@ export class AddCluster extends React.Component {
|
||||
message: _i18n._(t`Select custom kubeconfig file`),
|
||||
buttonLabel: _i18n._(t`Use configuration`),
|
||||
});
|
||||
|
||||
if (!canceled && filePaths.length) {
|
||||
this.setKubeConfig(filePaths[0]);
|
||||
}
|
||||
@@ -129,9 +135,11 @@ export class AddCluster extends React.Component {
|
||||
@action
|
||||
addClusters = () => {
|
||||
let newClusters: ClusterModel[] = [];
|
||||
|
||||
try {
|
||||
if (!this.selectedContexts.length) {
|
||||
this.error = <Trans>Please select at least one cluster context</Trans>;
|
||||
|
||||
return;
|
||||
}
|
||||
this.error = "";
|
||||
@@ -140,12 +148,16 @@ export class AddCluster extends React.Component {
|
||||
newClusters = this.selectedContexts.filter(context => {
|
||||
try {
|
||||
const kubeConfig = this.kubeContexts.get(context);
|
||||
|
||||
validateKubeConfig(kubeConfig);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.error = String(err.message);
|
||||
|
||||
if (err instanceof ExecValidationNotFoundError) {
|
||||
Notifications.error(<Trans>Error while adding cluster(s): {this.error}</Trans>);
|
||||
|
||||
return false;
|
||||
} else {
|
||||
throw new Error(err);
|
||||
@@ -157,6 +169,7 @@ export class AddCluster extends React.Component {
|
||||
const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE
|
||||
? this.kubeConfigPath // save link to original kubeconfig in file-system
|
||||
: ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder
|
||||
|
||||
return {
|
||||
id: clusterId,
|
||||
kubeConfigPath,
|
||||
@@ -171,8 +184,10 @@ export class AddCluster extends React.Component {
|
||||
|
||||
runInAction(() => {
|
||||
clusterStore.addClusters(...newClusters);
|
||||
|
||||
if (newClusters.length === 1) {
|
||||
const clusterId = newClusters[0].id;
|
||||
|
||||
clusterStore.setActive(clusterId);
|
||||
navigate(clusterViewURL({ params: { clusterId } }));
|
||||
} else {
|
||||
@@ -271,6 +286,7 @@ export class AddCluster extends React.Component {
|
||||
const placeholder = this.selectedContexts.length > 0
|
||||
? <Trans>Selected contexts: <b>{this.selectedContexts.length}</b></Trans>
|
||||
: <Trans>Select contexts</Trans>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
@@ -302,8 +318,10 @@ export class AddCluster extends React.Component {
|
||||
|
||||
onKubeConfigInputBlur = () => {
|
||||
const isChanged = this.kubeConfigPath !== userStore.kubeConfigPath;
|
||||
|
||||
if (isChanged) {
|
||||
this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir());
|
||||
|
||||
try {
|
||||
this.setKubeConfig(this.kubeConfigPath, { throwError: true });
|
||||
} catch (err) {
|
||||
@@ -321,6 +339,7 @@ export class AddCluster extends React.Component {
|
||||
protected formatContextLabel = ({ value: context }: SelectOption<string>) => {
|
||||
const isNew = userStore.newContexts.has(context);
|
||||
const isSelected = this.selectedContexts.includes(context);
|
||||
|
||||
return (
|
||||
<div className={cssNames("kube-context flex gaps align-center", context)}>
|
||||
<span>{context}</span>
|
||||
@@ -332,6 +351,7 @@ export class AddCluster extends React.Component {
|
||||
|
||||
render() {
|
||||
const submitDisabled = this.selectedContexts.length === 0;
|
||||
|
||||
return (
|
||||
<DropFileInput onDropFiles={this.onDropKubeConfig}>
|
||||
<PageLayout className="AddClusters" header={<h2>Add Clusters</h2>}>
|
||||
|
||||
@@ -37,6 +37,7 @@ export class HelmChartDetails extends Component<Props> {
|
||||
chartUpdater = autorun(() => {
|
||||
this.selectedChart = null;
|
||||
const { chart: { name, repo, version } } = this.props;
|
||||
|
||||
helmChartsApi.get(repo, name, version).then(result => {
|
||||
this.readme = result.readme;
|
||||
this.chartVersions = result.versions;
|
||||
@@ -56,6 +57,7 @@ export class HelmChartDetails extends Component<Props> {
|
||||
this.chartPromise?.cancel();
|
||||
const { chart: { name, repo } } = this.props;
|
||||
const { readme } = await (this.chartPromise = helmChartsApi.get(repo, name, version));
|
||||
|
||||
this.readme = readme;
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
@@ -71,6 +73,7 @@ export class HelmChartDetails extends Component<Props> {
|
||||
renderIntroduction() {
|
||||
const { selectedChart, chartVersions, onVersionChange } = this;
|
||||
const placeholder = require("./helm-placeholder.svg");
|
||||
|
||||
return (
|
||||
<div className="introduction flex align-flex-start">
|
||||
<img
|
||||
@@ -146,6 +149,7 @@ export class HelmChartDetails extends Component<Props> {
|
||||
render() {
|
||||
const { chart, hideDetails } = this.props;
|
||||
const title = chart ? <Trans>Chart: {chart.getFullName()}</Trans> : "";
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className="HelmChartDetails"
|
||||
|
||||
@@ -30,9 +30,11 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
|
||||
async getVersions(chartName: string, force?: boolean): Promise<IChartVersion[]> {
|
||||
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 => ({
|
||||
@@ -41,17 +43,20 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
if (!this.isLoaded) {
|
||||
await this.loadAll();
|
||||
}
|
||||
const repos = this.items
|
||||
.filter(chart => chart.getName() === chartName)
|
||||
.map(chart => chart.getRepository());
|
||||
|
||||
versions = await Promise.all(repos.map(loadVersions))
|
||||
.then(flatten)
|
||||
.then(this.sortVersions);
|
||||
|
||||
this.versions.set(chartName, versions);
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export class HelmCharts extends Component<Props> {
|
||||
|
||||
get selectedChart() {
|
||||
const { match: { params: { chartName, repo } } } = this.props;
|
||||
|
||||
return helmChartStore.getByName(chartName, repo);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
const { getReleaseSecret } = releaseStore;
|
||||
const { release } = this.props;
|
||||
const secret = getReleaseSecret(release);
|
||||
|
||||
if (this.releaseSecret) {
|
||||
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
|
||||
this.loadDetails();
|
||||
@@ -64,12 +65,14 @@ export class ReleaseDetails extends Component<Props> {
|
||||
|
||||
async loadDetails() {
|
||||
const { release } = this.props;
|
||||
|
||||
this.details = null;
|
||||
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
|
||||
}
|
||||
|
||||
async loadValues() {
|
||||
const { release } = this.props;
|
||||
|
||||
this.values = "";
|
||||
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
|
||||
}
|
||||
@@ -84,7 +87,9 @@ export class ReleaseDetails extends Component<Props> {
|
||||
version: release.getVersion(),
|
||||
values: this.values
|
||||
};
|
||||
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await releaseStore.update(name, namespace, data);
|
||||
Notifications.ok(
|
||||
@@ -98,12 +103,14 @@ export class ReleaseDetails extends Component<Props> {
|
||||
|
||||
upgradeVersion = () => {
|
||||
const { release, hideDetails } = this.props;
|
||||
|
||||
createUpgradeChartTab(release);
|
||||
hideDetails();
|
||||
};
|
||||
|
||||
renderValues() {
|
||||
const { values, saving } = this;
|
||||
|
||||
return (
|
||||
<div className="values">
|
||||
<DrawerTitle title={_i18n._(t`Values`)}/>
|
||||
@@ -127,6 +134,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
renderNotes() {
|
||||
if (!this.details.info?.notes) return null;
|
||||
const { notes } = this.details.info;
|
||||
|
||||
return (
|
||||
<div className="notes">
|
||||
{notes}
|
||||
@@ -136,6 +144,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
|
||||
renderResources() {
|
||||
const { resources } = this.details;
|
||||
|
||||
if (!resources) return null;
|
||||
const groups = groupBy(resources, item => item.kind);
|
||||
const tables = Object.entries(groups).map(([kind, items]) => {
|
||||
@@ -156,6 +165,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
name,
|
||||
namespace,
|
||||
})) : "";
|
||||
|
||||
return (
|
||||
<TableRow key={item.getId()}>
|
||||
<TableCell className="name">
|
||||
@@ -170,6 +180,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="resources">
|
||||
{tables}
|
||||
@@ -180,10 +191,13 @@ export class ReleaseDetails extends Component<Props> {
|
||||
renderContent() {
|
||||
const { release } = this.props;
|
||||
const { details } = this;
|
||||
|
||||
if (!release) return null;
|
||||
|
||||
if (!details) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DrawerItem name={<Trans>Chart</Trans>} className="chart">
|
||||
@@ -229,6 +243,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
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)}
|
||||
|
||||
@@ -24,6 +24,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
|
||||
@autobind()
|
||||
upgrade() {
|
||||
const { release, hideDetails } = this.props;
|
||||
|
||||
createUpgradeChartTab(release);
|
||||
hideDetails && hideDetails();
|
||||
}
|
||||
@@ -35,8 +36,10 @@ export class HelmReleaseMenu extends React.Component<Props> {
|
||||
|
||||
renderContent() {
|
||||
const { release, toolbar } = this.props;
|
||||
|
||||
if (!release) return;
|
||||
const hasRollback = release && release.getRevision() > 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasRollback && (
|
||||
@@ -51,6 +54,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { className, release, ...menuProps } = this.props;
|
||||
|
||||
return (
|
||||
<MenuActions
|
||||
{...menuProps}
|
||||
|
||||
@@ -41,6 +41,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
this.isLoading = true;
|
||||
const currentRevision = this.release.getRevision();
|
||||
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
|
||||
|
||||
releases = releases.filter(item => item.revision !== currentRevision); // remove current
|
||||
releases = orderBy(releases, "revision", "desc"); // sort
|
||||
this.revisions.replace(releases);
|
||||
@@ -50,6 +51,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
|
||||
rollback = async () => {
|
||||
const revisionNumber = this.revision.revision;
|
||||
|
||||
try {
|
||||
await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber);
|
||||
this.close();
|
||||
@@ -64,9 +66,11 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
|
||||
renderContent() {
|
||||
const { revision, revisions } = this;
|
||||
|
||||
if (!revision) {
|
||||
return <p><Trans>No revisions to rollback.</Trans></p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<b><Trans>Revision</Trans></b>
|
||||
@@ -85,6 +89,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
const { ...dialogProps } = this.props;
|
||||
const releaseName = this.release ? this.release.getName() : "";
|
||||
const header = <h5><Trans>Rollback <b>{releaseName}</b></Trans></h5>;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
@@ -26,9 +26,12 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
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;
|
||||
|
||||
return !isEqual(item.getLabels(), secret.getLabels());
|
||||
});
|
||||
|
||||
if (amountChanged || labelsChanged) {
|
||||
this.loadAll();
|
||||
}
|
||||
@@ -49,6 +52,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
owner: "helm",
|
||||
name: release.getName()
|
||||
};
|
||||
|
||||
return secretsStore.getByLabel(labels)
|
||||
.filter(secret => secret.getNs() == release.getNs())[0];
|
||||
}
|
||||
@@ -57,8 +61,10 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
async loadAll() {
|
||||
this.isLoading = true;
|
||||
let items;
|
||||
|
||||
try {
|
||||
const { isAdmin, allowedNamespaces } = getHostedCluster();
|
||||
|
||||
items = await this.loadItems(!isAdmin ? allowedNamespaces : null);
|
||||
} finally {
|
||||
if (items) {
|
||||
@@ -82,19 +88,25 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
|
||||
async create(payload: IReleaseCreatePayload) {
|
||||
const response = await helmReleasesApi.create(payload);
|
||||
|
||||
if (this.isLoaded) this.loadAll();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
||||
const response = await helmReleasesApi.update(name, namespace, payload);
|
||||
|
||||
if (this.isLoaded) this.loadAll();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async rollback(name: string, namespace: string, revision: number) {
|
||||
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
||||
|
||||
if (this.isLoaded) this.loadAll();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -104,6 +116,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
|
||||
async removeSelectedItems() {
|
||||
if (!this.selectedItems.length) return;
|
||||
|
||||
return Promise.all(this.selectedItems.map(this.remove));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export class HelmReleases extends Component<Props> {
|
||||
|
||||
get selectedRelease() {
|
||||
const { match: { params: { name, namespace } } } = this.props;
|
||||
|
||||
return releaseStore.items.find(release => {
|
||||
return release.getName() == name && release.getNs() == namespace;
|
||||
});
|
||||
@@ -66,6 +67,7 @@ export class HelmReleases extends Component<Props> {
|
||||
|
||||
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
|
||||
const releaseNames = selectedItems.map(item => item.getName()).join(", ");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Trans>Remove <b>{releaseNames}</b>?</Trans>
|
||||
@@ -111,6 +113,7 @@ export class HelmReleases extends Component<Props> {
|
||||
]}
|
||||
renderTableContents={(release: HelmRelease) => {
|
||||
const version = release.getVersion();
|
||||
|
||||
return [
|
||||
release.getName(),
|
||||
release.getNs(),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
export class Apps extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
const query = namespaceStore.getContextParams();
|
||||
|
||||
return [
|
||||
{
|
||||
title: <Trans>Charts</Trans>,
|
||||
|
||||
@@ -49,6 +49,7 @@ export class ClusterSettings extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const cluster = this.cluster;
|
||||
|
||||
if (!cluster) return null;
|
||||
const header = (
|
||||
<>
|
||||
@@ -56,6 +57,7 @@ export class ClusterSettings extends React.Component<Props> {
|
||||
<h2>{cluster.preferences.clusterName}</h2>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageLayout className="ClusterSettings" header={header}>
|
||||
<Status cluster={cluster}></Status>
|
||||
|
||||
@@ -25,9 +25,11 @@ export class ClusterIconSetting extends React.Component<Props> {
|
||||
@autobind()
|
||||
async onIconPick([file]: File[]) {
|
||||
const { cluster } = this.props;
|
||||
|
||||
try {
|
||||
if (file) {
|
||||
const buf = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
cluster.preferences.icon = `data:${file.type};base64,${buf.toString("base64")}`;
|
||||
} else {
|
||||
// this has to be done as a seperate branch (and not always) because `cluster`
|
||||
@@ -57,6 +59,7 @@ export class ClusterIconSetting extends React.Component<Props> {
|
||||
{"Browse for new icon..."}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Cluster Icon" />
|
||||
|
||||
@@ -23,6 +23,7 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||
|
||||
@computed get canEditPrometheusPath() {
|
||||
if (this.provider === "" || this.provider === "lens") return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -30,12 +31,15 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||
disposeOnUnmount(this,
|
||||
autorun(() => {
|
||||
const { prometheus, prometheusProvider } = this.props.cluster.preferences;
|
||||
|
||||
if (prometheus) {
|
||||
const prefix = prometheus.prefix || "";
|
||||
|
||||
this.path = `${prometheus.namespace}/${prometheus.service}:${prometheus.port}${prefix}`;
|
||||
} else {
|
||||
this.path = "";
|
||||
}
|
||||
|
||||
if (prometheusProvider) {
|
||||
this.provider = prometheusProvider.type;
|
||||
} else {
|
||||
@@ -51,9 +55,11 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||
}
|
||||
const parsed = this.path.split(/\/|:/, 3);
|
||||
const apiPrefix = this.path.substring(parsed.join("/").length);
|
||||
|
||||
if (!parsed[0] || !parsed[1] || !parsed[2]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
namespace: parsed[0],
|
||||
service: parsed[1],
|
||||
|
||||
@@ -24,6 +24,7 @@ export class InstallFeature extends React.Component<Props> {
|
||||
const statusUpdate = interval(20, () => {
|
||||
feature.updateStatus(cluster);
|
||||
});
|
||||
|
||||
statusUpdate.start(true);
|
||||
|
||||
disposeOnUnmount(this, () => {
|
||||
@@ -42,6 +43,7 @@ export class InstallFeature extends React.Component<Props> {
|
||||
const { cluster, feature } = this.props;
|
||||
const disabled = !cluster.isAdmin || this.loading;
|
||||
const loadingIcon = this.loading ? <Spinner/> : null;
|
||||
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
{feature.status.canUpgrade &&
|
||||
|
||||
@@ -16,6 +16,7 @@ export class RemoveClusterButton extends React.Component<Props> {
|
||||
@autobind()
|
||||
confirmRemoveCluster() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
ConfirmDialog.open({
|
||||
message: <p>Are you sure you want to remove <b>{cluster.preferences.clusterName}</b> from Lens?</p>,
|
||||
labelOk: <Trans>Yes</Trans>,
|
||||
@@ -28,6 +29,7 @@ export class RemoveClusterButton extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
return (
|
||||
<Button accent onClick={this.confirmRemoveCluster} className="button-area" disabled={cluster.isManaged}>
|
||||
Remove Cluster
|
||||
|
||||
@@ -11,6 +11,7 @@ interface Props {
|
||||
export class Features extends React.Component<Props> {
|
||||
render() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Features</h2>
|
||||
|
||||
@@ -14,6 +14,7 @@ export class Status extends React.Component<Props> {
|
||||
@autobind()
|
||||
openKubeconfig() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
shell.showItemInFolder(cluster.kubeConfigPath);
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ export class Status extends React.Component<Props> {
|
||||
["API Address", cluster.apiUrl || "N/A"],
|
||||
["Nodes Count", cluster.metadata.nodes ? String(cluster.metadata.nodes) : "N/A"]
|
||||
];
|
||||
|
||||
return (
|
||||
<Table scrollable={false}>
|
||||
{rows.map(([name, value]) => {
|
||||
|
||||
@@ -44,6 +44,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
// Node bad conditions
|
||||
nodesStore.items.forEach(node => {
|
||||
const { kind, selfLink, getId, getName } = node;
|
||||
|
||||
node.getWarningConditions().forEach(({ message }) => {
|
||||
warnings.push({
|
||||
kind,
|
||||
@@ -57,9 +58,11 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
|
||||
// Warning events for Workloads
|
||||
const events = eventStore.getWarnings();
|
||||
|
||||
events.forEach(error => {
|
||||
const { message, involvedObject } = error;
|
||||
const { uid, name, kind } = involvedObject;
|
||||
|
||||
warnings.push({
|
||||
getId: () => uid,
|
||||
getName: () => name,
|
||||
@@ -77,6 +80,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
const { warnings } = this;
|
||||
const warning = warnings.find(warn => warn.getId() == uid);
|
||||
const { getId, getName, message, kind, selfLink } = warning;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={getId()}
|
||||
@@ -99,11 +103,13 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
|
||||
renderContent() {
|
||||
const { warnings } = this;
|
||||
|
||||
if (!eventStore.isLoaded) {
|
||||
return (
|
||||
<Spinner center/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!warnings.length) {
|
||||
return (
|
||||
<div className="no-issues flex column box grow gaps align-center justify-center">
|
||||
@@ -113,6 +119,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubHeader>
|
||||
|
||||
@@ -14,6 +14,7 @@ export const ClusterMetricSwitchers = observer(() => {
|
||||
const metricsValues = getMetricsValues(metrics);
|
||||
const disableRoles = !masterNodes.length || !workerNodes.length;
|
||||
const disableMetrics = !metricsValues.length;
|
||||
|
||||
return (
|
||||
<div className="ClusterMetricSwitchers flex gaps">
|
||||
<div className="box grow">
|
||||
|
||||
@@ -42,6 +42,7 @@ export const ClusterMetrics = observer(() => {
|
||||
callbacks: {
|
||||
label: ({ index }, data) => {
|
||||
const value = data.datasets[0].data[index] as ChartPoint;
|
||||
|
||||
return value.y.toString();
|
||||
}
|
||||
}
|
||||
@@ -60,6 +61,7 @@ export const ClusterMetrics = observer(() => {
|
||||
callbacks: {
|
||||
label: ({ index }, data) => {
|
||||
const value = data.datasets[0].data[index] as ChartPoint;
|
||||
|
||||
return bytesToUnits(parseInt(value.y as string), 3);
|
||||
}
|
||||
}
|
||||
@@ -71,9 +73,11 @@ export const ClusterMetrics = observer(() => {
|
||||
if ((!metricValues.length || !liveMetricValues.length) && !metricsLoaded) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity) {
|
||||
return <ClusterNoMetrics className="empty"/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BarChart
|
||||
name={`${metricNodeRole}-${metricType}`}
|
||||
|
||||
@@ -141,6 +141,7 @@ export const ClusterPieCharts = observer(() => {
|
||||
`${i18n._(t`Capacity`)}: ${podCapacity}`,
|
||||
]
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="NodeCharts flex justify-center box grow gaps">
|
||||
<div className="chart flex column align-center box grow">
|
||||
@@ -174,6 +175,7 @@ export const ClusterPieCharts = observer(() => {
|
||||
const { masterNodes, workerNodes } = nodesStore;
|
||||
const { metricNodeRole, metricsLoaded } = clusterStore;
|
||||
const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes;
|
||||
|
||||
if (!nodes.length) {
|
||||
return (
|
||||
<div className="empty flex column box grow align-center justify-center">
|
||||
@@ -182,6 +184,7 @@ export const ClusterPieCharts = observer(() => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!metricsLoaded) {
|
||||
return (
|
||||
<div className="flex justify-center align-center box grow empty">
|
||||
@@ -190,9 +193,11 @@ export const ClusterPieCharts = observer(() => {
|
||||
);
|
||||
}
|
||||
const { memoryCapacity, cpuCapacity, podCapacity } = getMetricLastPoints(clusterStore.metrics);
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity) {
|
||||
return <ClusterNoMetrics className="empty"/>;
|
||||
}
|
||||
|
||||
return renderCharts();
|
||||
};
|
||||
|
||||
|
||||
@@ -32,9 +32,11 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
|
||||
// sync user setting with local storage
|
||||
const storage = createStorage("cluster_metric_switchers", {});
|
||||
|
||||
Object.assign(this, storage.get());
|
||||
reaction(() => {
|
||||
const { metricType, metricNodeRole } = this;
|
||||
|
||||
return { metricType, metricNodeRole };
|
||||
},
|
||||
settings => storage.set(settings)
|
||||
@@ -52,6 +54,7 @@ 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;
|
||||
});
|
||||
@@ -61,6 +64,7 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -79,6 +83,7 @@ export class ClusterStore extends KubeObjectStore<Cluster> {
|
||||
const range = 15;
|
||||
const end = Date.now() / 1000;
|
||||
const start = end - range;
|
||||
|
||||
this.liveMetrics = await this.loadMetrics({ start, end, step, range });
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export class Cluster extends React.Component {
|
||||
// todo: refactor
|
||||
async componentDidMount() {
|
||||
const { dependentStores } = this;
|
||||
|
||||
if (!isAllowedResource("nodes")) {
|
||||
dependentStores.splice(dependentStores.indexOf(nodesStore), 1);
|
||||
}
|
||||
@@ -54,6 +55,7 @@ export class Cluster extends React.Component {
|
||||
|
||||
render() {
|
||||
const { isLoaded } = this;
|
||||
|
||||
return (
|
||||
<TabLayout>
|
||||
<div className="Cluster">
|
||||
|
||||
@@ -28,6 +28,7 @@ export class HpaDetails extends React.Component<Props> {
|
||||
switch (metric.type) {
|
||||
case HpaMetricType.Resource:
|
||||
const addition = metric.resource.targetAverageUtilization ? <Trans>(as a percentage of request)</Trans> : "";
|
||||
|
||||
return <Trans>Resource {metric.resource.name} on Pods {addition}</Trans>;
|
||||
|
||||
case HpaMetricType.Pods:
|
||||
@@ -37,6 +38,7 @@ export class HpaDetails extends React.Component<Props> {
|
||||
const { target } = metric.object;
|
||||
const { kind, name } = target;
|
||||
const objectUrl = getDetailsUrl(lookupApiLink(target, hpa));
|
||||
|
||||
return (
|
||||
<Trans>
|
||||
{metric.object.metricName} on{" "}
|
||||
@@ -63,6 +65,7 @@ export class HpaDetails extends React.Component<Props> {
|
||||
hpa.getMetrics().map((metric, index) => {
|
||||
const name = renderName(metric);
|
||||
const values = hpa.getMetricValues(metric);
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="name">{name}</TableCell>
|
||||
@@ -77,8 +80,10 @@ export class HpaDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: hpa } = this.props;
|
||||
|
||||
if (!hpa) return;
|
||||
const { scaleTargetRef } = hpa.spec;
|
||||
|
||||
return (
|
||||
<div className="HpaDetails">
|
||||
<KubeObjectMeta object={hpa}/>
|
||||
@@ -106,6 +111,7 @@ export class HpaDetails extends React.Component<Props> {
|
||||
<DrawerItem name={<Trans>Status</Trans>} labelsOnly>
|
||||
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
||||
if (!isReady) return null;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={type}
|
||||
|
||||
@@ -31,6 +31,7 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -71,6 +72,7 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
||||
hpa.getAge(),
|
||||
hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
||||
if (!isReady) return null;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={type}
|
||||
|
||||
@@ -27,6 +27,7 @@ export class ConfigMapDetails extends React.Component<Props> {
|
||||
disposeOnUnmount(this, [
|
||||
autorun(() => {
|
||||
const { object: configMap } = this.props;
|
||||
|
||||
if (configMap) {
|
||||
this.data.replace(configMap.data); // refresh
|
||||
}
|
||||
@@ -36,6 +37,7 @@ export class ConfigMapDetails extends React.Component<Props> {
|
||||
|
||||
save = async () => {
|
||||
const { object: configMap } = this.props;
|
||||
|
||||
try {
|
||||
this.isSaving = true;
|
||||
await configMapsStore.update(configMap, { ...configMap, data: this.data.toJSON() });
|
||||
@@ -51,8 +53,10 @@ export class ConfigMapDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: configMap } = this.props;
|
||||
|
||||
if (!configMap) return null;
|
||||
const data = Object.entries(this.data.toJSON());
|
||||
|
||||
return (
|
||||
<div className="ConfigMapDetails">
|
||||
<KubeObjectMeta object={configMap}/>
|
||||
|
||||
+2
@@ -18,8 +18,10 @@ export class PodDisruptionBudgetDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: pdb } = this.props;
|
||||
|
||||
if (!pdb) return null;
|
||||
const selectors = pdb.getSelectors();
|
||||
|
||||
return (
|
||||
<div className="PdbDetails">
|
||||
<KubeObjectMeta object={pdb}/>
|
||||
|
||||
@@ -72,6 +72,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
const isStorage = quota.endsWith(".storage") || quota === "persistentvolumeclaims";
|
||||
const isCount = quota.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,
|
||||
@@ -102,8 +103,10 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
const { quotaName, namespace } = this;
|
||||
const quotas = this.quotaEntries.reduce<IResourceQuotaValues>((quotas, [name, value]) => {
|
||||
quotas[name] = value;
|
||||
|
||||
return quotas;
|
||||
}, {});
|
||||
|
||||
await resourceQuotaApi.create({ namespace, name: quotaName }, {
|
||||
spec: {
|
||||
hard: quotas
|
||||
@@ -127,6 +130,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const header = <h5><Trans>Create ResourceQuota</Trans></h5>;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
@@ -58,7 +58,9 @@ function renderQuotas(quota: ResourceQuota): JSX.Element[] {
|
||||
export class ResourceQuotaDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: quota } = this.props;
|
||||
|
||||
if (!quota) return null;
|
||||
|
||||
return (
|
||||
<div className="ResourceQuotaDetails">
|
||||
<KubeObjectMeta object={quota}/>
|
||||
@@ -79,6 +81,7 @@ export class ResourceQuotaDetails extends React.Component<Props> {
|
||||
{
|
||||
quota.getScopeSelector().map((selector, index) => {
|
||||
const { operator, scopeName, values } = selector;
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{operator}</TableCell>
|
||||
|
||||
@@ -81,9 +81,11 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
private getDataFromFields = (fields: ISecretTemplateField[] = [], processValue?: (val: string) => string) => {
|
||||
return fields.reduce<any>((data, field) => {
|
||||
const { key, value } = field;
|
||||
|
||||
if (key) {
|
||||
data[key] = processValue ? processValue(value) : value;
|
||||
}
|
||||
|
||||
return data;
|
||||
}, {});
|
||||
};
|
||||
@@ -101,8 +103,10 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
labels: this.getDataFromFields(labels),
|
||||
} as IKubeObjectMetadata
|
||||
};
|
||||
|
||||
try {
|
||||
const newSecret = await secretsApi.create({ namespace, name }, secret);
|
||||
|
||||
showDetails(newSecret.selfLink);
|
||||
this.reset();
|
||||
this.close();
|
||||
@@ -113,17 +117,20 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
|
||||
addField = (field: ISecretField) => {
|
||||
const fields = this.secret[this.type][field] || [];
|
||||
|
||||
fields.push({ key: "", value: "" });
|
||||
this.secret[this.type][field] = fields;
|
||||
};
|
||||
|
||||
removeField = (field: ISecretField, index: number) => {
|
||||
const fields = this.secret[this.type][field] || [];
|
||||
|
||||
fields.splice(index, 1);
|
||||
};
|
||||
|
||||
renderFields(field: ISecretField) {
|
||||
const fields = this.secret[this.type][field] || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubTitle compact className="fields-title" title={upperFirst(field.toString())}>
|
||||
@@ -137,6 +144,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
<div className="secret-fields">
|
||||
{fields.map((item, index) => {
|
||||
const { key = "", value = "", required } = item;
|
||||
|
||||
return (
|
||||
<div key={index} className="secret-field flex gaps auto align-center">
|
||||
<Input
|
||||
@@ -174,6 +182,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
const { ...dialogProps } = this.props;
|
||||
const { namespace, name, type } = this;
|
||||
const header = <h5><Trans>Create Secret</Trans></h5>;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
@@ -31,6 +31,7 @@ export class SecretDetails extends React.Component<Props> {
|
||||
disposeOnUnmount(this, [
|
||||
autorun(() => {
|
||||
const { object: secret } = this.props;
|
||||
|
||||
if (secret) {
|
||||
this.data = secret.data;
|
||||
this.revealSecret = {};
|
||||
@@ -41,7 +42,9 @@ export class SecretDetails extends React.Component<Props> {
|
||||
|
||||
saveSecret = async () => {
|
||||
const { object: secret } = this.props;
|
||||
|
||||
this.isSaving = true;
|
||||
|
||||
try {
|
||||
await secretsStore.update(secret, { ...secret, data: this.data });
|
||||
Notifications.ok(<Trans>Secret successfully updated.</Trans>);
|
||||
@@ -57,7 +60,9 @@ export class SecretDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: secret } = this.props;
|
||||
|
||||
if (!secret) return null;
|
||||
|
||||
return (
|
||||
<div className="SecretDetails">
|
||||
<KubeObjectMeta object={secret}/>
|
||||
@@ -71,12 +76,14 @@ export class SecretDetails extends React.Component<Props> {
|
||||
Object.entries(this.data).map(([name, value]) => {
|
||||
const revealSecret = this.revealSecret[name];
|
||||
let decodedVal = "";
|
||||
|
||||
try {
|
||||
decodedVal = base64.decode(value);
|
||||
} catch {
|
||||
decodedVal = "";
|
||||
}
|
||||
value = revealSecret ? decodedVal : value;
|
||||
|
||||
return (
|
||||
<div key={name} className="data">
|
||||
<div className="name">{name}</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ export class Config extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
const query = namespaceStore.getContextParams();
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
|
||||
if (isAllowedResource("configmaps")) {
|
||||
routes.push({
|
||||
title: <Trans>ConfigMaps</Trans>,
|
||||
@@ -23,6 +24,7 @@ export class Config extends React.Component {
|
||||
routePath: configMapsRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("secrets")) {
|
||||
routes.push({
|
||||
title: <Trans>Secrets</Trans>,
|
||||
@@ -31,6 +33,7 @@ export class Config extends React.Component {
|
||||
routePath: secretsRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("resourcequotas")) {
|
||||
routes.push({
|
||||
title: <Trans>Resource Quotas</Trans>,
|
||||
@@ -39,6 +42,7 @@ export class Config extends React.Component {
|
||||
routePath: resourceQuotaRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("horizontalpodautoscalers")) {
|
||||
routes.push({
|
||||
title: <Trans>HPA</Trans>,
|
||||
@@ -47,6 +51,7 @@ export class Config extends React.Component {
|
||||
routePath: hpaRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("poddisruptionbudgets")) {
|
||||
routes.push({
|
||||
title: <Trans>Pod Disruption Budgets</Trans>,
|
||||
@@ -55,6 +60,7 @@ export class Config extends React.Component {
|
||||
routePath: pdbRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,12 @@ interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
|
||||
export class CRDDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: crd } = this.props;
|
||||
|
||||
if (!crd) return null;
|
||||
const { plural, singular, kind, listKind } = crd.getNames();
|
||||
const printerColumns = crd.getPrinterColumns();
|
||||
const validation = crd.getValidation();
|
||||
|
||||
return (
|
||||
<div className="CRDDetails">
|
||||
<KubeObjectMeta object={crd}/>
|
||||
@@ -60,6 +62,7 @@ export class CRDDetails extends React.Component<Props> {
|
||||
{
|
||||
crd.getConditions().map(condition => {
|
||||
const { type, message, lastTransitionTime, status } = condition;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={type}
|
||||
@@ -103,6 +106,7 @@ export class CRDDetails extends React.Component<Props> {
|
||||
{
|
||||
printerColumns.map((column, index) => {
|
||||
const { name, type, jsonPath } = column;
|
||||
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="name">{name}</TableCell>
|
||||
|
||||
@@ -30,6 +30,7 @@ export class CrdList extends React.Component {
|
||||
onGroupChange(group: string) {
|
||||
const groups = [...this.groups];
|
||||
const index = groups.findIndex(item => item == group);
|
||||
|
||||
if (index !== -1) groups.splice(index, 1);
|
||||
else groups.push(group);
|
||||
setQueryParams({ groups });
|
||||
@@ -43,6 +44,7 @@ export class CrdList extends React.Component {
|
||||
[sortBy.version]: (crd: CustomResourceDefinition) => crd.getVersion(),
|
||||
[sortBy.scope]: (crd: CustomResourceDefinition) => crd.getScope(),
|
||||
};
|
||||
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
className="CrdList"
|
||||
@@ -58,8 +60,10 @@ export class CrdList extends React.Component {
|
||||
renderHeaderTitle={<Trans>Custom Resources</Trans>}
|
||||
customizeHeader={() => {
|
||||
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(", ")}</>;
|
||||
|
||||
return {
|
||||
// todo: move to global filters
|
||||
filters: (
|
||||
@@ -71,6 +75,7 @@ export class CrdList extends React.Component {
|
||||
controlShouldRenderValue={false}
|
||||
formatOptionLabel={({ value: group }: SelectOption) => {
|
||||
const isSelected = selectedGroups.includes(group);
|
||||
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<Icon small material="folder"/>
|
||||
|
||||
@@ -53,6 +53,7 @@ export class CrdResourceDetails extends React.Component<Props> {
|
||||
|
||||
renderStatus(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {
|
||||
const showStatus = !columns.find(column => column.name == "Status") && crd.status?.conditions;
|
||||
|
||||
if (!showStatus) {
|
||||
return null;
|
||||
}
|
||||
@@ -77,6 +78,7 @@ export class CrdResourceDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { props: { object }, crd } = this;
|
||||
|
||||
if (!object || !crd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export class CrdResources extends React.Component<Props> {
|
||||
disposeOnUnmount(this, [
|
||||
autorun(() => {
|
||||
const { store } = this;
|
||||
|
||||
if (store && !store.isLoading && !store.isLoaded) {
|
||||
store.loadAll();
|
||||
}
|
||||
@@ -37,16 +38,19 @@ export class CrdResources extends React.Component<Props> {
|
||||
|
||||
@computed get crd() {
|
||||
const { group, name } = this.props.match.params;
|
||||
|
||||
return crdStore.getByGroup(group, name);
|
||||
}
|
||||
|
||||
@computed get store() {
|
||||
if (!this.crd) return null;
|
||||
|
||||
return apiManager.getStore(this.crd.getResourceApiBase());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { crd, store } = this;
|
||||
|
||||
if (!crd) return null;
|
||||
const isNamespaced = crd.isNamespaced();
|
||||
const extraColumns = crd.getPrinterColumns(false); // Cols with priority bigger than 0 are shown in details
|
||||
@@ -55,6 +59,7 @@ export class CrdResources extends React.Component<Props> {
|
||||
[sortBy.namespace]: (item: KubeObject) => item.getNs(),
|
||||
[sortBy.age]: (item: KubeObject) => item.metadata.creationTimestamp,
|
||||
};
|
||||
|
||||
extraColumns.forEach(column => {
|
||||
sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, column.jsonPath.slice(1));
|
||||
});
|
||||
@@ -74,6 +79,7 @@ export class CrdResources extends React.Component<Props> {
|
||||
isNamespaced && { title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
|
||||
...extraColumns.map(column => {
|
||||
const { name } = column;
|
||||
|
||||
return {
|
||||
title: name,
|
||||
className: name.toLowerCase(),
|
||||
|
||||
@@ -38,17 +38,22 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
|
||||
|
||||
@computed get groups() {
|
||||
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);
|
||||
}
|
||||
|
||||
getByGroup(group: string, pluralName: string) {
|
||||
const crdInGroup = this.groups[group];
|
||||
|
||||
if (!crdInGroup) return null;
|
||||
|
||||
return crdInGroup.find(crd => crd.getPluralName() === pluralName);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@ interface Props extends KubeObjectDetailsProps<KubeEvent> {
|
||||
export class EventDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: event } = this.props;
|
||||
|
||||
if (!event) return;
|
||||
const { message, reason, count, type, involvedObject } = event;
|
||||
const { kind, name, namespace, fieldPath } = involvedObject;
|
||||
|
||||
return (
|
||||
<div className="EventDetails">
|
||||
<KubeObjectMeta object={event}/>
|
||||
|
||||
@@ -28,6 +28,7 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
|
||||
if(obj.kind == "Node") {
|
||||
return obj.getName() == evt.involvedObject.uid && evt.involvedObject.kind == "Node";
|
||||
}
|
||||
|
||||
return obj.getId() == evt.involvedObject.uid;
|
||||
});
|
||||
}
|
||||
@@ -38,12 +39,16 @@ export class EventStore extends KubeObjectStore<KubeEvent> {
|
||||
const eventsWithError = Object.values(groupsByInvolvedObject).map(events => {
|
||||
const recent = events[0];
|
||||
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;
|
||||
}
|
||||
|
||||
return recent;
|
||||
});
|
||||
|
||||
return compact(eventsWithError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export class Events extends React.Component<Props> {
|
||||
const tooltipId = `message-${event.getId()}`;
|
||||
const isWarning = type === "Warning";
|
||||
const detailsUrl = getDetailsUrl(lookupApiLink(involvedObject, event));
|
||||
|
||||
return [
|
||||
{
|
||||
className: { warning: isWarning },
|
||||
@@ -114,9 +115,11 @@ export class Events extends React.Component<Props> {
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
if (compact) {
|
||||
return events;
|
||||
}
|
||||
|
||||
return (
|
||||
<TabLayout>
|
||||
{events}
|
||||
|
||||
@@ -21,6 +21,7 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||
render() {
|
||||
const { object } = this.props;
|
||||
const events = eventStore.getEventsByObject(object);
|
||||
|
||||
if (!events.length) {
|
||||
return (
|
||||
<DrawerTitle className="flex gaps align-center">
|
||||
@@ -28,6 +29,7 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||
</DrawerTitle>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DrawerTitle className="flex gaps align-center">
|
||||
@@ -36,6 +38,7 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||
<div className="KubeEventDetails">
|
||||
{events.map(evt => {
|
||||
const { message, count, lastTimestamp, involvedObject } = evt;
|
||||
|
||||
return (
|
||||
<div className="event" key={evt.getId()}>
|
||||
<div className={cssNames("title", { warning: evt.isWarning() })}>
|
||||
|
||||
@@ -24,11 +24,14 @@ export class KubeEventIcon extends React.Component<Props> {
|
||||
const { object, showWarningsOnly, filterEvents } = this.props;
|
||||
const events = eventStore.getEventsByObject(object);
|
||||
let warnings = events.filter(evt => evt.isWarning());
|
||||
|
||||
if (filterEvents) warnings = filterEvents(warnings);
|
||||
|
||||
if (!events.length || (showWarningsOnly && !warnings.length)) {
|
||||
return null;
|
||||
}
|
||||
const event = [...warnings, ...events][0]; // get latest event
|
||||
|
||||
return (
|
||||
<Icon
|
||||
material="warning"
|
||||
|
||||
@@ -106,6 +106,7 @@ export class Extensions extends React.Component {
|
||||
|
||||
return Array.from(extensionLoader.userExtensions.values()).filter(ext => {
|
||||
const { name, description } = ext.manifest;
|
||||
|
||||
return [
|
||||
name.toLowerCase().includes(searchText),
|
||||
description?.toLowerCase().includes(searchText),
|
||||
@@ -149,6 +150,7 @@ export class Extensions extends React.Component {
|
||||
|
||||
installFromUrlOrPath = async () => {
|
||||
const { installPath } = this;
|
||||
|
||||
if (!installPath) return;
|
||||
const fileName = path.basename(installPath);
|
||||
|
||||
@@ -158,6 +160,7 @@ export class Extensions extends React.Component {
|
||||
if (InputValidators.isUrl.validate(installPath)) {
|
||||
const { promise: filePromise } = downloadFile({ url: installPath, timeout: 60000 /*1m*/ });
|
||||
const data = await filePromise;
|
||||
|
||||
this.requestInstall({ fileName, data });
|
||||
}
|
||||
// otherwise installing from system path
|
||||
@@ -173,6 +176,7 @@ export class Extensions extends React.Component {
|
||||
|
||||
installOnDrop = (files: File[]) => {
|
||||
logger.info("Install from D&D");
|
||||
|
||||
return this.requestInstall(
|
||||
files.map(file => ({
|
||||
fileName: path.basename(file.path),
|
||||
@@ -190,8 +194,10 @@ export class Extensions extends React.Component {
|
||||
.map(async request => {
|
||||
try {
|
||||
const data = await fse.readFile(request.filePath);
|
||||
|
||||
request.data = data;
|
||||
preloadedRequests.push(request);
|
||||
|
||||
return request;
|
||||
} catch(error) {
|
||||
if (showError) {
|
||||
@@ -209,6 +215,7 @@ export class Extensions extends React.Component {
|
||||
|
||||
// tarball from npm contains single root folder "package/*"
|
||||
const firstFile = tarFiles[0];
|
||||
|
||||
if (!firstFile) {
|
||||
throw new Error(`invalid extension bundle, ${manifestFilename} not found`);
|
||||
}
|
||||
@@ -230,6 +237,7 @@ export class Extensions extends React.Component {
|
||||
if (!manifest.lens && !manifest.renderer) {
|
||||
throw new Error(`${manifestFilename} must specify "main" and/or "renderer" fields`);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@@ -241,6 +249,7 @@ export class Extensions extends React.Component {
|
||||
|
||||
requests.forEach(req => {
|
||||
const tempFile = this.getExtensionPackageTemp(req.fileName);
|
||||
|
||||
fse.writeFileSync(tempFile, req.data);
|
||||
});
|
||||
|
||||
@@ -248,6 +257,7 @@ export class Extensions extends React.Component {
|
||||
await Promise.all(
|
||||
requests.map(async req => {
|
||||
const tempFile = this.getExtensionPackageTemp(req.fileName);
|
||||
|
||||
try {
|
||||
const manifest = await this.validatePackage(tempFile);
|
||||
|
||||
@@ -270,6 +280,7 @@ export class Extensions extends React.Component {
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return validatedRequests;
|
||||
}
|
||||
|
||||
@@ -342,6 +353,7 @@ export class Extensions extends React.Component {
|
||||
Notifications.error(
|
||||
<p>Installing extension <b>{displayName}</b> has failed: <em>{error}</em></p>
|
||||
);
|
||||
|
||||
// Remove install state on install failure
|
||||
if (this.extensionState.get(extensionId)?.state === "installing") {
|
||||
this.extensionState.delete(extensionId);
|
||||
@@ -378,6 +390,7 @@ export class Extensions extends React.Component {
|
||||
Notifications.error(
|
||||
<p>Uninstalling extension <b>{displayName}</b> has failed: <em>{error?.message ?? ""}</em></p>
|
||||
);
|
||||
|
||||
// Remove uninstall state on uninstall failure
|
||||
if (this.extensionState.get(extension.id)?.state === "uninstalling") {
|
||||
this.extensionState.delete(extension.id);
|
||||
|
||||
@@ -14,6 +14,7 @@ export class LandingPage extends React.Component {
|
||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
||||
const noClustersInScope = !clusters.length;
|
||||
const showStartupHint = this.showHint && noClustersInScope;
|
||||
|
||||
return (
|
||||
<div className="LandingPage flex">
|
||||
{showStartupHint && (
|
||||
|
||||
@@ -38,6 +38,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
|
||||
addNamespace = async () => {
|
||||
const { namespace } = this;
|
||||
const { onSuccess, onError } = this.props;
|
||||
|
||||
try {
|
||||
await namespaceStore.create({ name: namespace }).then(onSuccess);
|
||||
this.close();
|
||||
@@ -51,6 +52,7 @@ export class AddNamespaceDialog extends React.Component<Props> {
|
||||
const { ...dialogProps } = this.props;
|
||||
const { namespace } = this;
|
||||
const header = <h5><Trans>Create Namespace</Trans></h5>;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
@@ -22,6 +22,7 @@ interface Props extends KubeObjectDetailsProps<Namespace> {
|
||||
export class NamespaceDetails extends React.Component<Props> {
|
||||
@computed get quotas() {
|
||||
const namespace = this.props.object.getName();
|
||||
|
||||
return resourceQuotaStore.getAllByNs(namespace);
|
||||
}
|
||||
|
||||
@@ -31,8 +32,10 @@ export class NamespaceDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: namespace } = this.props;
|
||||
|
||||
if (!namespace) return;
|
||||
const status = namespace.getStatus();
|
||||
|
||||
return (
|
||||
<div className="NamespaceDetails">
|
||||
<KubeObjectMeta object={namespace}/>
|
||||
|
||||
@@ -46,16 +46,20 @@ export class NamespaceSelect extends React.Component<Props> {
|
||||
@computed get options(): SelectOption[] {
|
||||
const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props;
|
||||
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
||||
|
||||
options = customizeOptions ? customizeOptions(options) : options;
|
||||
|
||||
if (showClusterOption) {
|
||||
options.unshift({ value: null, label: clusterOptionLabel });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
formatOptionLabel = (option: SelectOption) => {
|
||||
const { showIcons } = this.props;
|
||||
const { value, label } = option;
|
||||
|
||||
return label || (
|
||||
<>
|
||||
{showIcons && <Icon small material="layers" />}
|
||||
@@ -66,6 +70,7 @@ export class NamespaceSelect extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props;
|
||||
|
||||
return (
|
||||
<Select
|
||||
className={cssNames("NamespaceSelect", className)}
|
||||
@@ -83,8 +88,10 @@ export class NamespaceSelectFilter extends React.Component {
|
||||
render() {
|
||||
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>;
|
||||
|
||||
return (
|
||||
<NamespaceSelect
|
||||
placeholder={placeholder}
|
||||
@@ -95,6 +102,7 @@ export class NamespaceSelectFilter extends React.Component {
|
||||
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
|
||||
formatOptionLabel={({ value: namespace }: SelectOption) => {
|
||||
const isSelected = hasContext(namespace);
|
||||
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<FilterIcon type={FilterType.NAMESPACE} />
|
||||
|
||||
@@ -15,6 +15,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
|
||||
get initNamespaces() {
|
||||
const fromUrl = navigation.searchParams.getAsArray("namespaces");
|
||||
|
||||
return fromUrl.length ? fromUrl : this.storage.get();
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
|
||||
// restore context namespaces
|
||||
const { initNamespaces: namespaces } = this;
|
||||
|
||||
this.setContext(namespaces);
|
||||
this.updateUrl(namespaces);
|
||||
|
||||
@@ -46,8 +48,10 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
protected async loadItems(namespaces?: string[]) {
|
||||
if (!isAllowedResource("namespaces")) {
|
||||
if (namespaces) return namespaces.map(this.getDummyNamespace);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
if (namespaces) {
|
||||
return Promise.all(namespaces.map(name => this.api.get({ name })));
|
||||
} else {
|
||||
@@ -74,6 +78,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||
|
||||
hasContext(namespace: string | string[]) {
|
||||
const context = Array.isArray(namespace) ? namespace : [namespace];
|
||||
|
||||
return context.every(namespace => this.contextNs.includes(namespace));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ interface Props extends KubeObjectDetailsProps<Endpoint> {
|
||||
export class EndpointDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: endpoint } = this.props;
|
||||
|
||||
if (!endpoint) return;
|
||||
|
||||
return (
|
||||
<div className="EndpointDetails">
|
||||
<KubeObjectMeta object={endpoint}/>
|
||||
|
||||
@@ -21,6 +21,7 @@ export class EndpointSubsetList extends React.Component<Props> {
|
||||
getAddressTableRow(ip: string) {
|
||||
const { subset } = this.props;
|
||||
const address = subset.getAddresses().find(address => address.getId() == ip);
|
||||
|
||||
return this.renderAddressTableRow(address);
|
||||
}
|
||||
|
||||
@@ -28,6 +29,7 @@ export class EndpointSubsetList extends React.Component<Props> {
|
||||
getNotReadyAddressTableRow(ip: string) {
|
||||
const { subset} = this.props;
|
||||
const address = subset.getNotReadyAddresses().find(address => address.getId() == ip);
|
||||
|
||||
return this.renderAddressTableRow(address);
|
||||
}
|
||||
|
||||
@@ -60,6 +62,7 @@ export class EndpointSubsetList extends React.Component<Props> {
|
||||
@autobind()
|
||||
renderAddressTableRow(address: EndpointAddress) {
|
||||
const { endpoint } = this.props;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={address.getId()}
|
||||
|
||||
@@ -51,6 +51,7 @@ export class Endpoints extends React.Component<Props> {
|
||||
tableProps={{
|
||||
customRowHeights: (item: Endpoint, lineHeight, paddings) => {
|
||||
const lines = item.getEndpointSubsets().length || 1;
|
||||
|
||||
return lines * lineHeight + paddings;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -79,6 +79,7 @@ export const IngressCharts = observer(() => {
|
||||
const { label, data } = datasets[datasetIndex];
|
||||
const value = data[index] as ChartPoint;
|
||||
const chartTooltipSec = _i18n._(t`sec`);
|
||||
|
||||
return `${label}: ${parseFloat(value.y as string).toFixed(3)} ${chartTooltipSec}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ export class IngressDetails extends React.Component<Props> {
|
||||
|
||||
renderPaths(ingress: Ingress) {
|
||||
const { spec: { rules } } = ingress;
|
||||
|
||||
if (!rules || !rules.length) return null;
|
||||
|
||||
return rules.map((rule, index) => {
|
||||
return (
|
||||
<div className="rules" key={index}>
|
||||
@@ -71,6 +73,7 @@ export class IngressDetails extends React.Component<Props> {
|
||||
|
||||
renderIngressPoints(ingressPoints: ILoadBalancerIngress[]) {
|
||||
if (!ingressPoints || ingressPoints.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table className="ingress-points">
|
||||
@@ -93,6 +96,7 @@ export class IngressDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: ingress } = this.props;
|
||||
|
||||
if (!ingress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export class Ingresses extends React.Component<Props> {
|
||||
tableProps={{
|
||||
customRowHeights: (item: Ingress, lineHeight, paddings) => {
|
||||
const lines = item.getRoutes().length || 1;
|
||||
|
||||
return lines * lineHeight + paddings;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -21,16 +21,21 @@ interface Props extends KubeObjectDetailsProps<NetworkPolicy> {
|
||||
export class NetworkPolicyDetails extends React.Component<Props> {
|
||||
renderIngressFrom(ingress: IPolicyIngress) {
|
||||
const { from } = ingress;
|
||||
|
||||
if (!from) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubTitle title={<Trans>From</Trans>}/>
|
||||
{from.map(item =>
|
||||
Object.keys(item).map(key => {
|
||||
const data = get(item, key);
|
||||
|
||||
if (key === "ipBlock") {
|
||||
const { cidr, except } = data as IPolicyIpBlock;
|
||||
|
||||
if (!cidr) return;
|
||||
|
||||
return (
|
||||
<DrawerItem name={key} key={key}>
|
||||
cidr: {cidr}, {" "}
|
||||
@@ -41,6 +46,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
const selector: IPolicySelector = data;
|
||||
|
||||
if (selector.matchLabels) {
|
||||
return (
|
||||
<DrawerItem name={key} key={key}>
|
||||
@@ -64,15 +70,20 @@ export class NetworkPolicyDetails extends React.Component<Props> {
|
||||
|
||||
renderEgressTo(egress: IPolicyEgress) {
|
||||
const { to } = egress;
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<DrawerItem name="ipBlock" key={cidr}>
|
||||
cidr: {cidr}, {" "}
|
||||
@@ -88,11 +99,13 @@ export class NetworkPolicyDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: policy } = this.props;
|
||||
|
||||
if (!policy) {
|
||||
return null;
|
||||
}
|
||||
const { ingress, egress } = policy.spec;
|
||||
const selector = policy.getMatchLabels();
|
||||
|
||||
return (
|
||||
<div className="NetworkPolicyDetails">
|
||||
<KubeObjectMeta object={policy}/>
|
||||
@@ -109,6 +122,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
|
||||
<DrawerTitle title={_i18n._(t`Ingress`)}/>
|
||||
{ingress.map((ingress, i) => {
|
||||
const { ports } = ingress;
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<DrawerItem name={<Trans>Ports</Trans>}>
|
||||
@@ -126,6 +140,7 @@ export class NetworkPolicyDetails extends React.Component<Props> {
|
||||
<DrawerTitle title={<Trans>Egress</Trans>}/>
|
||||
{egress.map((egress, i) => {
|
||||
const { ports } = egress;
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<DrawerItem name={<Trans>Ports</Trans>}>
|
||||
|
||||
@@ -16,12 +16,15 @@ interface Props {
|
||||
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>
|
||||
);
|
||||
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="EndpointList flex column">
|
||||
<Table
|
||||
|
||||
@@ -29,9 +29,11 @@ export class ServiceDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: service } = this.props;
|
||||
|
||||
if (!service) return;
|
||||
const { spec } = service;
|
||||
const endpoint = endpointStore.getByName(service.getName(), service.getNs());
|
||||
|
||||
return (
|
||||
<div className="ServicesDetails">
|
||||
<KubeObjectMeta object={service}/>
|
||||
|
||||
@@ -22,7 +22,9 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
|
||||
async portForward() {
|
||||
const { service, port } = this.props;
|
||||
|
||||
this.waiting = true;
|
||||
|
||||
try {
|
||||
await apiBase.post(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}`, {});
|
||||
} catch(error) {
|
||||
@@ -34,6 +36,7 @@ export class ServicePortComponent extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { port } = this.props;
|
||||
|
||||
return (
|
||||
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
||||
<span title={_i18n._(t`Open in a browser`)} onClick={() => this.portForward() }>
|
||||
|
||||
@@ -16,6 +16,7 @@ export class Network extends React.Component {
|
||||
static get tabRoutes(): TabLayoutRoute[] {
|
||||
const query = namespaceStore.getContextParams();
|
||||
const routes: TabLayoutRoute[] = [];
|
||||
|
||||
if (isAllowedResource("services")) {
|
||||
routes.push({
|
||||
title: <Trans>Services</Trans>,
|
||||
@@ -24,6 +25,7 @@ export class Network extends React.Component {
|
||||
routePath: servicesRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("endpoints")) {
|
||||
routes.push({
|
||||
title: <Trans>Endpoints</Trans>,
|
||||
@@ -32,6 +34,7 @@ export class Network extends React.Component {
|
||||
routePath: endpointRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("ingresses")) {
|
||||
routes.push({
|
||||
title: <Trans>Ingresses</Trans>,
|
||||
@@ -40,6 +43,7 @@ export class Network extends React.Component {
|
||||
routePath: ingressRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAllowedResource("networkpolicies")) {
|
||||
routes.push({
|
||||
title: <Trans>Network Policies</Trans>,
|
||||
@@ -48,6 +52,7 @@ export class Network extends React.Component {
|
||||
routePath: networkPoliciesRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export const NodeCharts = observer(() => {
|
||||
label: ({ datasetIndex, index }, { datasets }) => {
|
||||
const { label, data } = datasets[datasetIndex];
|
||||
const value = data[index] as ChartPoint;
|
||||
|
||||
return `${label}: ${value.y}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export class NodeDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: node } = this.props;
|
||||
|
||||
if (!node) return;
|
||||
const { status } = node;
|
||||
const { nodeInfo, addresses, capacity, allocatable } = status;
|
||||
@@ -54,6 +55,7 @@ export class NodeDetails extends React.Component<Props> {
|
||||
<Trans key="disk">Disk</Trans>,
|
||||
<Trans key="pods">Pods</Trans>,
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="NodeDetails">
|
||||
{podsStore.isLoaded && (
|
||||
@@ -121,6 +123,7 @@ export class NodeDetails extends React.Component<Props> {
|
||||
{
|
||||
conditions.map(condition => {
|
||||
const { type } = condition;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={type}
|
||||
|
||||
@@ -16,6 +16,7 @@ export class NodesStore extends KubeObjectStore<Node> {
|
||||
@action
|
||||
async loadUsageMetrics() {
|
||||
this.metricsLoading = true;
|
||||
|
||||
try {
|
||||
this.metrics = await nodesApi.getMetrics();
|
||||
this.metricsLoaded = true;
|
||||
@@ -42,6 +43,7 @@ export class NodesStore extends KubeObjectStore<Node> {
|
||||
return;
|
||||
}
|
||||
const nodeName = node.getName();
|
||||
|
||||
return metricNames.map(metricName => {
|
||||
try {
|
||||
const metric = this.metrics[metricName];
|
||||
@@ -52,6 +54,7 @@ export class NodesStore extends KubeObjectStore<Node> {
|
||||
result.metric.kubernetes_node,
|
||||
].includes(nodeName);
|
||||
});
|
||||
|
||||
return result ? parseFloat(result.values.slice(-1)[0][1]) : 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
|
||||
@@ -48,9 +48,11 @@ export class Nodes extends React.Component<Props> {
|
||||
|
||||
renderCpuUsage(node: Node) {
|
||||
const metrics = nodesStore.getLastMetricValues(node, ["cpuUsage", "cpuCapacity"]);
|
||||
|
||||
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
|
||||
const usage = metrics[0];
|
||||
const cores = metrics[1];
|
||||
|
||||
return (
|
||||
<LineProgress
|
||||
max={cores}
|
||||
@@ -65,9 +67,11 @@ export class Nodes extends React.Component<Props> {
|
||||
|
||||
renderMemoryUsage(node: Node) {
|
||||
const metrics = nodesStore.getLastMetricValues(node, ["memoryUsage", "memoryCapacity"]);
|
||||
|
||||
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
|
||||
const usage = metrics[0];
|
||||
const capacity = metrics[1];
|
||||
|
||||
return (
|
||||
<LineProgress
|
||||
max={capacity}
|
||||
@@ -82,9 +86,11 @@ export class Nodes extends React.Component<Props> {
|
||||
|
||||
renderDiskUsage(node: Node): any {
|
||||
const metrics = nodesStore.getLastMetricValues(node, ["fsUsage", "fsSize"]);
|
||||
|
||||
if (!metrics || !metrics[1]) return <LineProgress value={0}/>;
|
||||
const usage = metrics[0];
|
||||
const capacity = metrics[1];
|
||||
|
||||
return (
|
||||
<LineProgress
|
||||
max={capacity}
|
||||
@@ -102,9 +108,11 @@ export class Nodes extends React.Component<Props> {
|
||||
return null;
|
||||
}
|
||||
const conditions = node.getActiveConditions();
|
||||
|
||||
return conditions.map(condition => {
|
||||
const { type } = condition;
|
||||
const tooltipId = `node-${node.getName()}-condition-${type}`;
|
||||
|
||||
return (
|
||||
<div key={type} id={tooltipId} className={cssNames("condition", kebabCase(type))}>
|
||||
{type}
|
||||
@@ -161,6 +169,7 @@ export class Nodes extends React.Component<Props> {
|
||||
]}
|
||||
renderTableContents={(node: Node) => {
|
||||
const tooltipId = `node-taints-${node.getId()}`;
|
||||
|
||||
return [
|
||||
node.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={node} />,
|
||||
|
||||
@@ -24,6 +24,7 @@ export class PodSecurityPolicyDetails extends React.Component<Props> {
|
||||
}) {
|
||||
if (!group) return;
|
||||
const { rule, ranges } = group;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerTitle title={title}/>
|
||||
@@ -43,6 +44,7 @@ export class PodSecurityPolicyDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: psp } = this.props;
|
||||
|
||||
if (!psp) {
|
||||
return null;
|
||||
}
|
||||
@@ -52,6 +54,7 @@ export class PodSecurityPolicyDetails extends React.Component<Props> {
|
||||
hostIPC, hostNetwork, hostPID, hostPorts, privileged, readOnlyRootFilesystem, requiredDropCapabilities,
|
||||
runAsGroup, runAsUser, runtimeClass, seLinux, supplementalGroups, volumes,
|
||||
} = psp.spec;
|
||||
|
||||
return (
|
||||
<div className="PodSecurityPolicyDetails">
|
||||
<KubeObjectMeta object={psp}/>
|
||||
|
||||
@@ -47,11 +47,13 @@ export class Preferences extends React.Component {
|
||||
@action
|
||||
async loadHelmRepos() {
|
||||
this.helmLoading = true;
|
||||
|
||||
try {
|
||||
if (!this.helmRepos.length) {
|
||||
this.helmRepos = await repoManager.loadAvailableRepos(); // via https://helm.sh
|
||||
}
|
||||
const repos = await repoManager.repositories(); // via helm-cli
|
||||
|
||||
this.helmAddedRepos.clear();
|
||||
repos.forEach(repo => this.helmAddedRepos.set(repo.name, repo));
|
||||
} catch (err) {
|
||||
@@ -82,8 +84,10 @@ export class Preferences extends React.Component {
|
||||
|
||||
onRepoSelect = async ({ value: repo }: SelectOption<HelmRepo>) => {
|
||||
const isAdded = this.helmAddedRepos.has(repo.name);
|
||||
|
||||
if (isAdded) {
|
||||
Notifications.ok(<Trans>Helm branch <b>{repo.name}</b> already in use</Trans>);
|
||||
|
||||
return;
|
||||
}
|
||||
this.helmLoading = true;
|
||||
@@ -93,6 +97,7 @@ export class Preferences extends React.Component {
|
||||
|
||||
formatHelmOptionLabel = ({ value: repo }: SelectOption<HelmRepo>) => {
|
||||
const isAdded = this.helmAddedRepos.has(repo.name);
|
||||
|
||||
return (
|
||||
<div className="flex gaps">
|
||||
<span>{repo.name}</span>
|
||||
@@ -104,6 +109,7 @@ export class Preferences extends React.Component {
|
||||
render() {
|
||||
const { preferences } = userStore;
|
||||
const header = <h2><Trans>Preferences</Trans></h2>;
|
||||
|
||||
return (
|
||||
<PageLayout showOnTop className="Preferences" header={header}>
|
||||
<h2><Trans>Color Theme</Trans></h2>
|
||||
@@ -140,6 +146,7 @@ export class Preferences extends React.Component {
|
||||
<div className="repos flex column">
|
||||
{Array.from(this.helmAddedRepos).map(([name, repo]) => {
|
||||
const tooltipId = `message-${name}`;
|
||||
|
||||
return (
|
||||
<Badge key={name} className="added-repo flex gaps align-center justify-space-between">
|
||||
<span id={tooltipId} className="repo">{name}</span>
|
||||
|
||||
@@ -20,8 +20,10 @@ interface Props extends KubeObjectDetailsProps<StorageClass> {
|
||||
export class StorageClassDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: storageClass } = this.props;
|
||||
|
||||
if (!storageClass) return null;
|
||||
const { provisioner, parameters, mountOptions } = storageClass;
|
||||
|
||||
return (
|
||||
<div className="StorageClassDetails">
|
||||
<KubeObjectMeta object={storageClass}/>
|
||||
|
||||
@@ -35,6 +35,7 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: volumeClaim } = this.props;
|
||||
|
||||
if (!volumeClaim) {
|
||||
return null;
|
||||
}
|
||||
@@ -44,6 +45,7 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
|
||||
const metricTabs = [
|
||||
<Trans key="disk">Disk</Trans>
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="PersistentVolumeClaimDetails">
|
||||
<ResourceMetrics
|
||||
|
||||
@@ -66,6 +66,7 @@ export class PersistentVolumeClaims extends React.Component<Props> {
|
||||
const storageClassDetailsUrl = getDetailsUrl(storageClassApi.getUrl({
|
||||
name: storageClassName
|
||||
}));
|
||||
|
||||
return [
|
||||
pvc.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={pvc} />,
|
||||
|
||||
@@ -21,10 +21,12 @@ interface Props extends KubeObjectDetailsProps<PersistentVolume> {
|
||||
export class PersistentVolumeDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: volume } = this.props;
|
||||
|
||||
if (!volume) {
|
||||
return null;
|
||||
}
|
||||
const { accessModes, capacity, persistentVolumeReclaimPolicy, storageClassName, claimRef, flexVolume, mountOptions, nfs } = volume.spec;
|
||||
|
||||
return (
|
||||
<div className="PersistentVolumeDetails">
|
||||
<KubeObjectMeta object={volume}/>
|
||||
|
||||
@@ -57,6 +57,7 @@ export class PersistentVolumes extends React.Component<Props> {
|
||||
const storageClassDetailsUrl = getDetailsUrl(storageClassApi.getUrl({
|
||||
name: storageClassName
|
||||
}));
|
||||
|
||||
return [
|
||||
volume.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={volume} />,
|
||||
|
||||
@@ -40,6 +40,7 @@ export class Storage extends React.Component {
|
||||
routePath: storageClassesRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
return tabRoutes;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
rolesStore,
|
||||
serviceAccountsStore,
|
||||
];
|
||||
|
||||
this.isLoading = true;
|
||||
await Promise.all(stores.map(store => store.loadAll()));
|
||||
this.isLoading = false;
|
||||
@@ -91,6 +92,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
if (this.roleBinding) {
|
||||
const { name, kind } = this.roleBinding.roleRef;
|
||||
const role = rolesStore.items.find(role => role.kind === kind && role.getName() === name);
|
||||
|
||||
if (role) {
|
||||
this.selectedRoleId = role.getId();
|
||||
this.bindContext = role.getNs() || "";
|
||||
@@ -107,6 +109,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
onBindContextChange = (namespace: string) => {
|
||||
this.bindContext = namespace;
|
||||
const roleContext = this.selectedRole && this.selectedRole.getNs() || "";
|
||||
|
||||
if (this.bindContext && this.bindContext !== roleContext) {
|
||||
this.selectedRoleId = ""; // reset previously selected role for specific context
|
||||
}
|
||||
@@ -123,11 +126,13 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
namespace: item.getNs(),
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
try {
|
||||
let roleBinding: RoleBinding;
|
||||
|
||||
if (this.isEditing) {
|
||||
roleBinding = await roleBindingsStore.updateSubjects({
|
||||
roleBinding: this.roleBinding,
|
||||
@@ -136,6 +141,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
}
|
||||
else {
|
||||
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
|
||||
|
||||
roleBinding = await roleBindingsStore.create({ name, namespace }, {
|
||||
subjects,
|
||||
roleRef: {
|
||||
@@ -153,13 +159,16 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
|
||||
@computed get roleOptions(): BindingSelectOption[] {
|
||||
let roles = rolesStore.items as Role[];
|
||||
|
||||
if (this.bindContext) {
|
||||
// show only cluster-roles or roles for selected context namespace
|
||||
roles = roles.filter(role => !role.getNs() || role.getNs() === this.bindContext);
|
||||
}
|
||||
|
||||
return roles.map(role => {
|
||||
const name = role.getName();
|
||||
const namespace = role.getNs();
|
||||
|
||||
return {
|
||||
value: role.getId(),
|
||||
label: name + (namespace ? ` (${namespace})` : "")
|
||||
@@ -171,6 +180,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
return serviceAccountsStore.items.map(account => {
|
||||
const name = account.getName();
|
||||
const namespace = account.getNs();
|
||||
|
||||
return {
|
||||
item: account,
|
||||
value: name,
|
||||
@@ -181,6 +191,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
|
||||
renderContents() {
|
||||
const unwrapBindings = (options: BindingSelectOption[]) => options.map(option => option.item || option.subject);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubTitle title={<Trans>Context</Trans>}/>
|
||||
@@ -257,6 +268,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
);
|
||||
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
|
||||
const nextLabel = isEditing ? <Trans>Update</Trans> : <Trans>Create</Trans>;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
@@ -36,6 +36,7 @@ export class RoleBindingDetails extends React.Component<Props> {
|
||||
selectSubject(subject: IRoleBindingSubject) {
|
||||
const { selectedSubjects } = this;
|
||||
const isSelected = selectedSubjects.includes(subject);
|
||||
|
||||
selectedSubjects.replace(
|
||||
isSelected
|
||||
? selectedSubjects.filter(sub => sub !== subject) // unselect
|
||||
@@ -47,6 +48,7 @@ export class RoleBindingDetails extends React.Component<Props> {
|
||||
removeSelectedSubjects() {
|
||||
const { object: roleBinding } = this.props;
|
||||
const { selectedSubjects } = this;
|
||||
|
||||
ConfirmDialog.open({
|
||||
ok: () => roleBindingsStore.updateSubjects({ roleBinding, removeSubjects: selectedSubjects }),
|
||||
labelOk: _i18n._(t`Remove`),
|
||||
@@ -59,12 +61,14 @@ export class RoleBindingDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { selectedSubjects } = this;
|
||||
const { object: roleBinding } = this.props;
|
||||
|
||||
if (!roleBinding) {
|
||||
return null;
|
||||
}
|
||||
const name = roleBinding.getName();
|
||||
const { roleRef } = roleBinding;
|
||||
const subjects = roleBinding.getSubjects();
|
||||
|
||||
return (
|
||||
<div className="RoleBindingDetails">
|
||||
<KubeObjectMeta object={roleBinding}/>
|
||||
@@ -96,6 +100,7 @@ export class RoleBindingDetails extends React.Component<Props> {
|
||||
subjects.map((subject, i) => {
|
||||
const { kind, name, namespace } = subject;
|
||||
const isSelected = selectedSubjects.includes(subject);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={i} selected={isSelected}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||
|
||||
protected loadItem(params: { name: string; namespace?: string }) {
|
||||
if (params.namespace) return roleBindingApi.get(params);
|
||||
|
||||
return clusterRoleBindingApi.get(params);
|
||||
}
|
||||
|
||||
@@ -52,6 +53,7 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||
const { roleBinding, addSubjects, removeSubjects } = params;
|
||||
const currentSubjects = roleBinding.getSubjects();
|
||||
let newSubjects = currentSubjects;
|
||||
|
||||
if (addSubjects) {
|
||||
newSubjects = uniqBy(currentSubjects.concat(addSubjects), ({ kind, name, namespace }) => {
|
||||
return [kind, name, namespace].join("-");
|
||||
@@ -59,6 +61,7 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||
} else if (removeSubjects) {
|
||||
newSubjects = difference(currentSubjects, removeSubjects);
|
||||
}
|
||||
|
||||
return this.update(roleBinding, {
|
||||
roleRef: roleBinding.roleRef,
|
||||
subjects: newSubjects
|
||||
|
||||
@@ -40,6 +40,7 @@ export class AddRoleDialog extends React.Component<Props> {
|
||||
createRole = async () => {
|
||||
try {
|
||||
const role = await rolesStore.create({ name: this.roleName });
|
||||
|
||||
showDetails(role.selfLink);
|
||||
this.reset();
|
||||
this.close();
|
||||
@@ -51,6 +52,7 @@ export class AddRoleDialog extends React.Component<Props> {
|
||||
render() {
|
||||
const { ...dialogProps } = this.props;
|
||||
const header = <h5><Trans>Create Role</Trans></h5>;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
@@ -17,8 +17,10 @@ interface Props extends KubeObjectDetailsProps<Role> {
|
||||
export class RoleDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: role } = this.props;
|
||||
|
||||
if (!role) return;
|
||||
const rules = role.getRules();
|
||||
|
||||
return (
|
||||
<div className="RoleDetails">
|
||||
<KubeObjectMeta object={role}/>
|
||||
|
||||
@@ -20,6 +20,7 @@ export class RolesStore extends KubeObjectStore<Role> {
|
||||
|
||||
protected loadItem(params: { name: string; namespace?: string }) {
|
||||
if (params.namespace) return roleApi.get(params);
|
||||
|
||||
return clusterRoleApi.get(params);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -39,8 +39,10 @@ export class CreateServiceAccountDialog extends React.Component<Props> {
|
||||
|
||||
createAccount = async () => {
|
||||
const { name, namespace } = this;
|
||||
|
||||
try {
|
||||
const serviceAccount = await serviceAccountsStore.create({ namespace, name });
|
||||
|
||||
this.name = "";
|
||||
showDetails(serviceAccount.selfLink);
|
||||
this.close();
|
||||
@@ -53,6 +55,7 @@ export class CreateServiceAccountDialog extends React.Component<Props> {
|
||||
const { ...dialogProps } = this.props;
|
||||
const { name, namespace } = this;
|
||||
const header = <h5><Trans>Create Service Account</Trans></h5>;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
+10
@@ -30,6 +30,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
this.secrets = null;
|
||||
this.imagePullSecrets = null;
|
||||
const { object: serviceAccount } = this.props;
|
||||
|
||||
if (!serviceAccount) {
|
||||
return;
|
||||
}
|
||||
@@ -37,18 +38,22 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
const secrets = serviceAccount.getSecrets().map(({ name }) => {
|
||||
return secretsStore.load({ name, namespace });
|
||||
});
|
||||
|
||||
this.secrets = await Promise.all(secrets);
|
||||
const imagePullSecrets = serviceAccount.getImagePullSecrets().map(async({ name }) => {
|
||||
return secretsStore.load({ name, namespace }).catch(() => this.generateDummySecretObject(name));
|
||||
});
|
||||
|
||||
this.imagePullSecrets = await Promise.all(imagePullSecrets);
|
||||
});
|
||||
|
||||
renderSecrets() {
|
||||
const { secrets } = this;
|
||||
|
||||
if (!secrets) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
|
||||
return secrets.map(secret =>
|
||||
<ServiceAccountsSecret key={secret.getId()} secret={secret}/>
|
||||
);
|
||||
@@ -56,9 +61,11 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
|
||||
renderImagePullSecrets() {
|
||||
const { imagePullSecrets } = this;
|
||||
|
||||
if (!imagePullSecrets) {
|
||||
return <Spinner center/>;
|
||||
}
|
||||
|
||||
return this.renderSecretLinks(imagePullSecrets);
|
||||
}
|
||||
|
||||
@@ -75,6 +82,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
|
||||
{secret.getName()}
|
||||
@@ -98,6 +106,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: serviceAccount } = this.props;
|
||||
|
||||
if (!serviceAccount) {
|
||||
return null;
|
||||
}
|
||||
@@ -106,6 +115,7 @@ export class ServiceAccountsDetails extends React.Component<Props> {
|
||||
secret.getAnnotations().some(annot => annot == `kubernetes.io/service-account.name: ${serviceAccount.getName()}`)
|
||||
);
|
||||
const imagePullSecrets = serviceAccount.getImagePullSecrets();
|
||||
|
||||
return (
|
||||
<div className="ServiceAccountsDetails">
|
||||
<KubeObjectMeta object={serviceAccount}/>
|
||||
|
||||
@@ -23,6 +23,7 @@ export class ServiceAccountsSecret extends React.Component<Props, State> {
|
||||
renderSecretValue() {
|
||||
const { secret } = this.props;
|
||||
const { showToken } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!showToken && (
|
||||
@@ -44,6 +45,7 @@ export class ServiceAccountsSecret extends React.Component<Props, State> {
|
||||
|
||||
render() {
|
||||
const { metadata: { name, creationTimestamp }, type } = this.props.secret;
|
||||
|
||||
return (
|
||||
<div className="ServiceAccountsSecret box grow-fixed">
|
||||
<div className="secret-row">
|
||||
|
||||
@@ -9,6 +9,7 @@ export class ServiceAccountsStore extends KubeObjectStore<ServiceAccount> {
|
||||
|
||||
protected async createItem(params: { name: string; namespace?: string }) {
|
||||
await super.createItem(params);
|
||||
|
||||
return this.api.get(params); // hackfix: load freshly created account, cause it doesn't have "secrets" field yet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ export class ServiceAccounts extends React.Component<Props> {
|
||||
|
||||
function ServiceAccountMenu(props: KubeObjectMenuProps<ServiceAccount>) {
|
||||
const { object, toolbar } = props;
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => openServiceAccountKubeConfig(object)}>
|
||||
<Icon material="insert_drive_file" title="Kubeconfig File" interactive={toolbar}/>
|
||||
|
||||
@@ -16,6 +16,7 @@ export class UserManagement extends React.Component {
|
||||
static get tabRoutes() {
|
||||
const tabRoutes: TabLayoutRoute[] = [];
|
||||
const query = namespaceStore.getContextParams();
|
||||
|
||||
tabRoutes.push(
|
||||
{
|
||||
title: <Trans>Service Accounts</Trans>,
|
||||
@@ -36,6 +37,7 @@ export class UserManagement extends React.Component {
|
||||
routePath: rolesRoute.path.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (isAllowedResource("podsecuritypolicies")) {
|
||||
tabRoutes.push({
|
||||
title: <Trans>Pod Security Policies</Trans>,
|
||||
@@ -44,6 +46,7 @@ export class UserManagement extends React.Component {
|
||||
routePath: podSecurityPoliciesRoute.path.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
return tabRoutes;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export class WhatsNew extends React.Component {
|
||||
render() {
|
||||
const logo = require("../../components/icon/lens-logo.svg");
|
||||
const releaseNotes = marked(this.releaseNotes);
|
||||
|
||||
return (
|
||||
<div className="WhatsNew flex column">
|
||||
<div className="content box grow">
|
||||
|
||||
@@ -29,8 +29,10 @@ export class CronJobDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: cronJob } = this.props;
|
||||
|
||||
if (!cronJob) return null;
|
||||
const childJobs = jobStore.getJobsByOwner(cronJob);
|
||||
|
||||
return (
|
||||
<div className="CronJobDetails">
|
||||
<KubeObjectMeta object={cronJob}/>
|
||||
@@ -56,6 +58,7 @@ export class CronJobDetails extends React.Component<Props> {
|
||||
{childJobs.map((job: Job) => {
|
||||
const selectors = job.getSelectors();
|
||||
const condition = job.getCondition();
|
||||
|
||||
return (
|
||||
<div className="job" key={job.getId()}>
|
||||
<div className="title">
|
||||
|
||||
@@ -43,6 +43,7 @@ export class CronJobTriggerDialog extends Component<Props> {
|
||||
|
||||
onOpen = async () => {
|
||||
const { cronjob } = this;
|
||||
|
||||
this.jobName = cronjob ? `${cronjob.getName()}-manual-${Math.random().toString(36).slice(2, 7)}` : "";
|
||||
this.jobName = this.jobName.slice(0, 63);
|
||||
this.ready = true;
|
||||
@@ -55,6 +56,7 @@ export class CronJobTriggerDialog extends Component<Props> {
|
||||
trigger = async () => {
|
||||
const { cronjob } = this;
|
||||
const { close } = this;
|
||||
|
||||
try {
|
||||
const cronjobDefinition = await cronJobApi.get({
|
||||
name: cronjob.getName(),
|
||||
@@ -102,6 +104,7 @@ export class CronJobTriggerDialog extends Component<Props> {
|
||||
<Trans>Trigger CronJob <span>{cronjobName}</span></Trans>
|
||||
</h5>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
@@ -10,6 +10,7 @@ export class CronJobStore extends KubeObjectStore<CronJob> {
|
||||
|
||||
getStatuses(cronJobs?: CronJob[]) {
|
||||
const status = { suspended: 0, scheduled: 0 };
|
||||
|
||||
cronJobs.forEach(cronJob => {
|
||||
if (cronJob.spec.suspend) {
|
||||
status.suspended++;
|
||||
@@ -18,13 +19,16 @@ export class CronJobStore extends KubeObjectStore<CronJob> {
|
||||
status.scheduled++;
|
||||
}
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
getActiveJobsNum(cronJob: CronJob) {
|
||||
// Active jobs are jobs without any condition 'Complete' nor 'Failed'
|
||||
const jobs = jobStore.getJobsByOwner(cronJob);
|
||||
|
||||
if (!jobs.length) return 0;
|
||||
|
||||
return jobs.filter(job => !job.getCondition()).length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ export class CronJobs extends React.Component<Props> {
|
||||
|
||||
export function CronJobMenu(props: KubeObjectMenuProps<CronJob>) {
|
||||
const { object, toolbar } = props;
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => CronJobTriggerDialog.open(object)}>
|
||||
<Icon material="play_circle_filled" title={_i18n._(t`Trigger`)} interactive={toolbar}/>
|
||||
|
||||
@@ -42,6 +42,7 @@ export class DaemonSetDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: daemonSet } = this.props;
|
||||
|
||||
if (!daemonSet) return null;
|
||||
const { spec } = daemonSet;
|
||||
const selectors = daemonSet.getSelectors();
|
||||
@@ -49,6 +50,7 @@ export class DaemonSetDetails extends React.Component<Props> {
|
||||
const nodeSelector = daemonSet.getNodeSelectors();
|
||||
const childPods = daemonSetStore.getChildPods(daemonSet);
|
||||
const metrics = daemonSetStore.metrics;
|
||||
|
||||
return (
|
||||
<div className="DaemonSetDetails">
|
||||
{podsStore.isLoaded && (
|
||||
|
||||
@@ -13,6 +13,7 @@ export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
|
||||
|
||||
async loadMetrics(daemonSet: DaemonSet) {
|
||||
const pods = this.getChildPods(daemonSet);
|
||||
|
||||
this.metrics = await podsApi.getMetrics(pods, daemonSet.getNs(), "");
|
||||
}
|
||||
|
||||
@@ -22,8 +23,10 @@ export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
|
||||
|
||||
getStatuses(daemonSets?: DaemonSet[]) {
|
||||
const status = { failed: 0, pending: 0, running: 0 };
|
||||
|
||||
daemonSets.forEach(daemonSet => {
|
||||
const pods = this.getChildPods(daemonSet);
|
||||
|
||||
if (pods.some(pod => pod.getStatus() === PodStatus.FAILED)) {
|
||||
status.failed++;
|
||||
}
|
||||
@@ -34,6 +37,7 @@ export class DaemonSetStore extends KubeObjectStore<DaemonSet> {
|
||||
status.running++;
|
||||
}
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
if (!podsStore.isLoaded) {
|
||||
podsStore.loadAll();
|
||||
}
|
||||
|
||||
if (!replicaSetStore.isLoaded) {
|
||||
replicaSetStore.loadAll();
|
||||
}
|
||||
@@ -49,6 +50,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: deployment } = this.props;
|
||||
|
||||
if (!deployment) return null;
|
||||
const { status, spec } = deployment;
|
||||
const nodeSelector = deployment.getNodeSelectors();
|
||||
@@ -56,6 +58,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
const childPods = deploymentStore.getChildPods(deployment);
|
||||
const replicaSets = replicaSetStore.getReplicaSetsByOwner(deployment);
|
||||
const metrics = deploymentStore.metrics;
|
||||
|
||||
return (
|
||||
<div className="DeploymentDetails">
|
||||
{podsStore.isLoaded && (
|
||||
@@ -95,6 +98,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
{
|
||||
deployment.getConditions().map(condition => {
|
||||
const { type, message, lastTransitionTime, status } = condition;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
key={type}
|
||||
|
||||
@@ -97,6 +97,7 @@ describe("<DeploymentScaleDialog />", () => {
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = render(<DeploymentScaleDialog />);
|
||||
|
||||
expect(container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
@@ -104,8 +105,10 @@ describe("<DeploymentScaleDialog />", () => {
|
||||
// mock deploymentApi.getReplicas() which will be called
|
||||
// when <DeploymentScaleDialog /> rendered.
|
||||
const initReplicas = 3;
|
||||
|
||||
deploymentApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas);
|
||||
const { getByTestId } = render(<DeploymentScaleDialog />);
|
||||
|
||||
DeploymentScaleDialog.open(dummyDeployment);
|
||||
// we need to wait for the DeploymentScaleDialog to show up
|
||||
// because there is an <Animate /> in <Dialog /> which renders null at start.
|
||||
@@ -114,6 +117,7 @@ describe("<DeploymentScaleDialog />", () => {
|
||||
getByTestId("current-scale"),
|
||||
getByTestId("desired-scale"),
|
||||
]);
|
||||
|
||||
expect(currentScale).toHaveTextContent(`${initReplicas}`);
|
||||
expect(desiredScale).toHaveTextContent(`${initReplicas}`);
|
||||
});
|
||||
@@ -122,15 +126,19 @@ describe("<DeploymentScaleDialog />", () => {
|
||||
|
||||
it("changes the desired scale when clicking the icon buttons +/-", async () => {
|
||||
const initReplicas = 1;
|
||||
|
||||
deploymentApi.getReplicas = jest.fn().mockImplementationOnce(async () => initReplicas);
|
||||
const { getByTestId } = render(<DeploymentScaleDialog />);
|
||||
|
||||
DeploymentScaleDialog.open(dummyDeployment);
|
||||
await waitFor(async () => {
|
||||
const desiredScale = await getByTestId("desired-scale");
|
||||
|
||||
expect(desiredScale).toHaveTextContent(`${initReplicas}`);
|
||||
});
|
||||
const up = await getByTestId("desired-replicas-up");
|
||||
const down = await getByTestId("desired-replicas-down");
|
||||
|
||||
fireEvent.click(up);
|
||||
expect(await getByTestId("desired-scale")).toHaveTextContent(`${initReplicas + 1}`);
|
||||
fireEvent.click(down);
|
||||
@@ -140,6 +148,7 @@ describe("<DeploymentScaleDialog />", () => {
|
||||
fireEvent.click(down);
|
||||
expect(await getByTestId("desired-scale")).toHaveTextContent("1");
|
||||
const times = 120;
|
||||
|
||||
// edge case, desiredScale must < scaleMax (100)
|
||||
for (let i = 0; i < times; i++) {
|
||||
fireEvent.click(up);
|
||||
|
||||
@@ -44,6 +44,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
@computed get scaleMax() {
|
||||
const { currentReplicas } = this;
|
||||
const defaultMax = 50;
|
||||
|
||||
return currentReplicas <= defaultMax
|
||||
? defaultMax * 2
|
||||
: currentReplicas * 2;
|
||||
@@ -51,6 +52,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
|
||||
onOpen = async () => {
|
||||
const { deployment } = this;
|
||||
|
||||
this.currentReplicas = await deploymentApi.getReplicas({
|
||||
namespace: deployment.getNs(),
|
||||
name: deployment.getName(),
|
||||
@@ -70,6 +72,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
scale = async () => {
|
||||
const { deployment } = this;
|
||||
const { currentReplicas, desiredReplicas, close } = this;
|
||||
|
||||
try {
|
||||
if (currentReplicas !== desiredReplicas) {
|
||||
await deploymentApi.scale({
|
||||
@@ -94,6 +97,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
renderContents() {
|
||||
const { currentReplicas, desiredReplicas, onChange, scaleMax } = this;
|
||||
const warning = currentReplicas < 10 && desiredReplicas > 90;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="current-scale" data-testid="current-scale">
|
||||
@@ -137,6 +141,7 @@ export class DeploymentScaleDialog extends Component<Props> {
|
||||
<Trans>Scale Deployment <span>{deploymentName}</span></Trans>
|
||||
</h5>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
|
||||
@@ -18,13 +18,16 @@ export class DeploymentStore extends KubeObjectStore<Deployment> {
|
||||
|
||||
async loadMetrics(deployment: Deployment) {
|
||||
const pods = this.getChildPods(deployment);
|
||||
|
||||
this.metrics = await podsApi.getMetrics(pods, deployment.getNs(), "");
|
||||
}
|
||||
|
||||
getStatuses(deployments?: Deployment[]) {
|
||||
const status = { failed: 0, pending: 0, running: 0 };
|
||||
|
||||
deployments.forEach(deployment => {
|
||||
const pods = this.getChildPods(deployment);
|
||||
|
||||
if (pods.some(pod => pod.getStatus() === PodStatus.FAILED)) {
|
||||
status.failed++;
|
||||
}
|
||||
@@ -35,6 +38,7 @@ export class DeploymentStore extends KubeObjectStore<Deployment> {
|
||||
status.running++;
|
||||
}
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,11 +40,13 @@ interface Props extends RouteComponentProps<IDeploymentsRouteParams> {
|
||||
export class Deployments extends React.Component<Props> {
|
||||
renderPods(deployment: Deployment) {
|
||||
const { replicas, availableReplicas } = deployment.status;
|
||||
|
||||
return `${availableReplicas || 0}/${replicas || 0}`;
|
||||
}
|
||||
|
||||
renderConditions(deployment: Deployment) {
|
||||
const conditions = orderBy(deployment.getConditions(true), "type", "asc");
|
||||
|
||||
return conditions.map(({ type, message }) => (
|
||||
<span key={type} className={cssNames("condition", kebabCase(type))} title={message}>
|
||||
{type}
|
||||
@@ -97,6 +99,7 @@ export class Deployments extends React.Component<Props> {
|
||||
|
||||
export function DeploymentMenu(props: KubeObjectMenuProps<Deployment>) {
|
||||
const { object, toolbar } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={() => DeploymentScaleDialog.open(object)}>
|
||||
|
||||
@@ -34,6 +34,7 @@ export class JobDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: job } = this.props;
|
||||
|
||||
if (!job) return null;
|
||||
const selectors = job.getSelectors();
|
||||
const nodeSelector = job.getNodeSelectors();
|
||||
@@ -41,6 +42,7 @@ export class JobDetails extends React.Component<Props> {
|
||||
const childPods = jobStore.getChildPods(job);
|
||||
const ownerRefs = job.getOwnerRefs();
|
||||
const condition = job.getCondition();
|
||||
|
||||
return (
|
||||
<div className="JobDetails">
|
||||
<KubeObjectMeta object={job}/>
|
||||
@@ -71,6 +73,7 @@ export class JobDetails extends React.Component<Props> {
|
||||
ownerRefs.map(ref => {
|
||||
const { name, kind } = ref;
|
||||
const detailsUrl = getDetailsUrl(lookupApiLink(ref, job));
|
||||
|
||||
return (
|
||||
<p key={name}>
|
||||
{kind} <Link to={detailsUrl}>{name}</Link>
|
||||
|
||||
@@ -22,8 +22,10 @@ export class JobStore extends KubeObjectStore<Job> {
|
||||
|
||||
getStatuses(jobs?: Job[]) {
|
||||
const status = { failed: 0, pending: 0, running: 0, succeeded: 0 };
|
||||
|
||||
jobs.forEach(job => {
|
||||
const pods = this.getChildPods(job);
|
||||
|
||||
if (pods.some(pod => pod.getStatus() === PodStatus.FAILED)) {
|
||||
status.failed++;
|
||||
}
|
||||
@@ -37,6 +39,7 @@ export class JobStore extends KubeObjectStore<Job> {
|
||||
status.succeeded++;
|
||||
}
|
||||
});
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export class Jobs extends React.Component<Props> {
|
||||
]}
|
||||
renderTableContents={(job: Job) => {
|
||||
const condition = job.getCondition();
|
||||
|
||||
return [
|
||||
job.getName(),
|
||||
job.getNs(),
|
||||
|
||||
@@ -28,6 +28,7 @@ export class OverviewStatuses extends React.Component {
|
||||
renderWorkload(resource: KubeResource): React.ReactElement {
|
||||
const store = workloadStores[resource];
|
||||
const items = store.getAllByNs(namespaceStore.contextNs);
|
||||
|
||||
return (
|
||||
<div className="workload" key={resource}>
|
||||
<div className="title">
|
||||
|
||||
@@ -45,12 +45,14 @@ export class OverviewWorkloadStatus extends React.Component<Props> {
|
||||
label: "Empty"
|
||||
}]
|
||||
};
|
||||
|
||||
if (statuses.some(([, val]) => val > 0)) {
|
||||
const dataset: SimpleChartDataSets = {
|
||||
data: [],
|
||||
backgroundColor: [],
|
||||
label: "Status",
|
||||
};
|
||||
|
||||
statuses.forEach(([key, val]) => {
|
||||
if (val !== 0) {
|
||||
dataset.data.push(val);
|
||||
@@ -67,6 +69,7 @@ export class OverviewWorkloadStatus extends React.Component<Props> {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<PieChart data={chartData} options={options}/>
|
||||
);
|
||||
|
||||
Loaded 100 of 401 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user