mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'origin/master' into mobx-6.2
# Conflicts: # extensions/survey/package-lock.json # extensions/survey/package.json # extensions/survey/src/survey-preferences-store.ts # extensions/telemetry/src/telemetry-preferences-store.ts # src/common/user-store.ts # src/extensions/core-api/catalog.ts # src/extensions/extension-discovery.ts # src/extensions/lens-extension.ts # src/main/cluster-manager.ts # src/renderer/bootstrap.tsx # src/renderer/components/+apps-helm-charts/helm-chart.store.ts # src/renderer/components/+apps-releases/release.store.ts # src/renderer/components/+catalog/catalog.tsx # src/renderer/components/+extensions/extension-install.store.ts # src/renderer/components/+extensions/extensions.tsx # src/renderer/components/+preferences/preferences.tsx # src/renderer/components/dock/create-resource.store.ts # src/renderer/components/kube-object/kube-object-list-layout.tsx # src/renderer/theme.store.ts # src/renderer/utils/storageHelper.ts
This commit is contained in:
commit
a0193a2cda
263 files changed
+3401
-31747
No files matched your search
@@ -0,0 +1,91 @@
|
||||
import { CustomResourceDefinition } from "../endpoints";
|
||||
import { IKubeObjectMetadata } from "../kube-object";
|
||||
|
||||
describe("Crds", () => {
|
||||
describe("getVersion", () => {
|
||||
it("should get the first version name from the list of versions", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as IKubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
}
|
||||
]
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the first version name from the list of versions (length 2)", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as IKubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
}
|
||||
]
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the first version name from the list of versions (length 2) even with version field", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as IKubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
version: "abc",
|
||||
versions: [
|
||||
{
|
||||
name: "123",
|
||||
served: false,
|
||||
storage: false,
|
||||
},
|
||||
{
|
||||
name: "1234",
|
||||
served: false,
|
||||
storage: false,
|
||||
}
|
||||
]
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("123");
|
||||
});
|
||||
|
||||
it("should get the first version name from the version field", () => {
|
||||
const crd = new CustomResourceDefinition({
|
||||
apiVersion: "foo",
|
||||
kind: "CustomResourceDefinition",
|
||||
metadata: {} as IKubeObjectMetadata,
|
||||
});
|
||||
|
||||
crd.spec = {
|
||||
version: "abc"
|
||||
} as any;
|
||||
|
||||
expect(crd.getVersion()).toBe("abc");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,10 @@ export class CatalogEntityRegistry {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
return this._items.find((entity) => entity.metadata.uid === id);
|
||||
}
|
||||
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
const items = this._items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
|
||||
|
||||
|
||||
@@ -2,7 +2,15 @@ import { navigate } from "../navigation";
|
||||
import { commandRegistry } from "../../extensions/registries";
|
||||
import { CatalogEntity } from "../../common/catalog-entity";
|
||||
|
||||
export { CatalogEntity, CatalogEntityData, CatalogEntityActionContext, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../common/catalog-entity";
|
||||
export {
|
||||
CatalogCategory,
|
||||
CatalogEntity,
|
||||
CatalogEntityData,
|
||||
CatalogEntityActionContext,
|
||||
CatalogEntityAddMenuContext,
|
||||
CatalogEntityContextMenu,
|
||||
CatalogEntityContextMenuContext
|
||||
} from "../../common/catalog-entity";
|
||||
|
||||
export const catalogEntityRunContext = {
|
||||
navigate: (url: string) => navigate(url),
|
||||
|
||||
@@ -33,7 +33,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
};
|
||||
scope: "Namespaced" | "Cluster" | string;
|
||||
validation?: any;
|
||||
versions: {
|
||||
versions?: {
|
||||
name: string;
|
||||
served: boolean;
|
||||
storage: boolean;
|
||||
@@ -103,7 +103,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
|
||||
getVersion() {
|
||||
// v1 has removed the spec.version property, if it is present it must match the first version
|
||||
return this.spec.versions[0]?.name ?? this.spec.version;
|
||||
return this.spec.versions?.[0]?.name ?? this.spec.version;
|
||||
}
|
||||
|
||||
isNamespaced() {
|
||||
@@ -123,7 +123,7 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
|
||||
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
|
||||
const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
|
||||
const columns = this.spec.versions?.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
|
||||
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
|
||||
?? [];
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { apiBase } from "../index";
|
||||
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store";
|
||||
import { ItemObject } from "../../item.store";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { JsonApiData } from "../json-api";
|
||||
|
||||
interface IReleasePayload {
|
||||
name: string;
|
||||
@@ -68,73 +69,70 @@ const endpoint = compile(`/v2/releases/:namespace?/:name?`) as (
|
||||
}
|
||||
) => string;
|
||||
|
||||
export const helmReleasesApi = {
|
||||
list(namespace?: string) {
|
||||
return apiBase
|
||||
.get<HelmRelease[]>(endpoint({ namespace }))
|
||||
.then(releases => releases.map(HelmRelease.create));
|
||||
},
|
||||
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
|
||||
|
||||
get(name: string, namespace: string) {
|
||||
const path = endpoint({ name, namespace });
|
||||
return releases.map(HelmRelease.create);
|
||||
}
|
||||
|
||||
return apiBase.get<IReleaseRawDetails>(path).then(details => {
|
||||
const items: KubeObject[] = JSON.parse(details.resources).items;
|
||||
const resources = items.map(item => KubeObject.create(item));
|
||||
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
||||
const path = endpoint({ name, namespace });
|
||||
|
||||
return {
|
||||
...details,
|
||||
resources
|
||||
};
|
||||
});
|
||||
},
|
||||
const details = await apiBase.get<IReleaseRawDetails>(path);
|
||||
const items: KubeObject[] = JSON.parse(details.resources).items;
|
||||
const resources = items.map(item => KubeObject.create(item));
|
||||
|
||||
create(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
|
||||
const { repo, ...data } = payload;
|
||||
return {
|
||||
...details,
|
||||
resources
|
||||
};
|
||||
}
|
||||
|
||||
data.chart = `${repo}/${data.chart}`;
|
||||
data.values = jsYaml.safeLoad(data.values);
|
||||
export async function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
|
||||
const { repo, ...data } = payload;
|
||||
|
||||
return apiBase.post(endpoint(), { data });
|
||||
},
|
||||
data.chart = `${repo}/${data.chart}`;
|
||||
data.values = jsYaml.safeLoad(data.values);
|
||||
|
||||
update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
|
||||
const { repo, ...data } = payload;
|
||||
return apiBase.post(endpoint(), { data });
|
||||
}
|
||||
|
||||
data.chart = `${repo}/${data.chart}`;
|
||||
data.values = jsYaml.safeLoad(data.values);
|
||||
export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
|
||||
const { repo, ...data } = payload;
|
||||
|
||||
return apiBase.put(endpoint({ name, namespace }), { data });
|
||||
},
|
||||
data.chart = `${repo}/${data.chart}`;
|
||||
data.values = jsYaml.safeLoad(data.values);
|
||||
|
||||
async delete(name: string, namespace: string) {
|
||||
const path = endpoint({ name, namespace });
|
||||
return apiBase.put(endpoint({ name, namespace }), { data });
|
||||
}
|
||||
|
||||
return apiBase.del(path);
|
||||
},
|
||||
export async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
|
||||
const path = endpoint({ name, namespace });
|
||||
|
||||
getValues(name: string, namespace: string) {
|
||||
const path = `${endpoint({ name, namespace })}/values`;
|
||||
return apiBase.del(path);
|
||||
}
|
||||
|
||||
return apiBase.get<string>(path);
|
||||
},
|
||||
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
|
||||
const path = `${endpoint({ name, namespace })}/values${all? "?all": ""}`;
|
||||
|
||||
getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
||||
const path = `${endpoint({ name, namespace })}/history`;
|
||||
return apiBase.get<string>(path);
|
||||
}
|
||||
|
||||
return apiBase.get(path);
|
||||
},
|
||||
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
|
||||
const path = `${endpoint({ name, namespace })}/history`;
|
||||
|
||||
rollback(name: string, namespace: string, revision: number) {
|
||||
const path = `${endpoint({ name, namespace })}/rollback`;
|
||||
return apiBase.get(path);
|
||||
}
|
||||
|
||||
return apiBase.put(path, {
|
||||
data: {
|
||||
revision
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
|
||||
const path = `${endpoint({ name, namespace })}/rollback`;
|
||||
|
||||
return apiBase.put(path, {
|
||||
data: {
|
||||
revision
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class HelmRelease implements ItemObject {
|
||||
|
||||
@@ -342,7 +342,7 @@ export class Pod extends WorkloadKubeObject {
|
||||
// Returns pod phase or container error if occurred
|
||||
getStatusMessage() {
|
||||
if (this.getReason() === PodStatus.EVICTED) return "Evicted";
|
||||
if (this.getStatus() === PodStatus.RUNNING && this.metadata.deletionTimestamp) return "Terminating";
|
||||
if (this.metadata.deletionTimestamp) return "Terminating";
|
||||
|
||||
const statuses = this.getContainerStatuses(false); // not including initContainers
|
||||
|
||||
|
||||
@@ -7,10 +7,16 @@ import * as ReactRouter from "react-router";
|
||||
import * as ReactRouterDom from "react-router-dom";
|
||||
import * as LensExtensions from "../extensions/extension-api";
|
||||
import { render, unmountComponentAtNode } from "react-dom";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { delay } from "../common/utils";
|
||||
import { isMac, isDevelopment } from "../common/vars";
|
||||
import { HotbarStore } from "../common/hotbar-store";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
import { UserStore } from "../common/user-store";
|
||||
import * as LensExtensions from "../extensions/extension-api";
|
||||
import { ExtensionDiscovery } from "../extensions/extension-discovery";
|
||||
import { ExtensionLoader } from "../extensions/extension-loader";
|
||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { extensionDiscovery } from "../extensions/extension-discovery";
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
import { extensionsStore } from "../extensions/extensions-store";
|
||||
@@ -18,7 +24,8 @@ import { hotbarStore } from "../common/hotbar-store";
|
||||
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { App } from "./components/app";
|
||||
import { LensApp } from "./lens-app";
|
||||
import { themeStore } from "./theme.store";
|
||||
import { ThemeStore } from "./theme.store";
|
||||
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
|
||||
|
||||
/**
|
||||
* If this is a development buid, wait a second to attach
|
||||
@@ -50,8 +57,16 @@ export async function bootstrap(App: AppComponent) {
|
||||
await attachChromeDebugger();
|
||||
rootElem.classList.toggle("is-mac", isMac);
|
||||
|
||||
extensionLoader.init();
|
||||
extensionDiscovery.init();
|
||||
ExtensionLoader.getInstanceOrCreate().init();
|
||||
ExtensionDiscovery.getInstanceOrCreate().init();
|
||||
|
||||
const userStore = UserStore.getInstanceOrCreate();
|
||||
const clusterStore = ClusterStore.getInstanceOrCreate();
|
||||
const extensionsStore = ExtensionsStore.getInstanceOrCreate();
|
||||
const filesystemStore = FilesystemProvisionerStore.getInstanceOrCreate();
|
||||
const themeStore = ThemeStore.getInstanceOrCreate();
|
||||
const hotbarStore = HotbarStore.getInstanceOrCreate();
|
||||
const helmRepoManager = HelmRepoManager.getInstanceOrCreate();
|
||||
|
||||
// preload common stores
|
||||
await Promise.all([
|
||||
@@ -59,8 +74,9 @@ export async function bootstrap(App: AppComponent) {
|
||||
hotbarStore.load(),
|
||||
clusterStore.load(),
|
||||
extensionsStore.load(),
|
||||
filesystemProvisionerStore.load(),
|
||||
filesystemStore.load(),
|
||||
themeStore.init(),
|
||||
helmRepoManager.init(),
|
||||
]);
|
||||
|
||||
// Register additional store listeners
|
||||
@@ -72,8 +88,8 @@ export async function bootstrap(App: AppComponent) {
|
||||
}
|
||||
window.addEventListener("message", (ev: MessageEvent) => {
|
||||
if (ev.data === "teardown") {
|
||||
userStore.unregisterIpcListener();
|
||||
clusterStore.unregisterIpcListener();
|
||||
UserStore.getInstance(false)?.unregisterIpcListener();
|
||||
ClusterStore.getInstance(false)?.unregisterIpcListener();
|
||||
unmountComponentAtNode(rootElem);
|
||||
window.location.href = "about:blank";
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import { AceEditor } from "../ace-editor";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers";
|
||||
import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterModel, ClusterStore } from "../../../common/cluster-store";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { navigate } from "../../navigation";
|
||||
import { userStore } from "../../../common/user-store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
@@ -49,13 +49,13 @@ export class AddCluster extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
clusterStore.setActive(null);
|
||||
this.setKubeConfig(userStore.kubeConfigPath);
|
||||
ClusterStore.getInstance().setActive(null);
|
||||
this.setKubeConfig(UserStore.getInstance().kubeConfigPath);
|
||||
appEventBus.emit({ name: "cluster-add", action: "start" });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
userStore.markNewContextsAsSeen();
|
||||
UserStore.getInstance().markNewContextsAsSeen();
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -65,11 +65,13 @@ export class AddCluster extends React.Component {
|
||||
validateConfig(this.kubeConfigLocal);
|
||||
this.refreshContexts();
|
||||
this.kubeConfigPath = filePath;
|
||||
userStore.kubeConfigPath = filePath; // save to store
|
||||
UserStore.getInstance().kubeConfigPath = filePath; // save to store
|
||||
} catch (err) {
|
||||
Notifications.error(
|
||||
<div>Can't setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
|
||||
);
|
||||
if (!UserStore.getInstance().isDefaultKubeConfigPath) {
|
||||
Notifications.error(
|
||||
<div>Can't setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (throwError) {
|
||||
throw err;
|
||||
@@ -184,7 +186,7 @@ export class AddCluster extends React.Component {
|
||||
});
|
||||
|
||||
runInAction(() => {
|
||||
clusterStore.addClusters(...newClusters);
|
||||
ClusterStore.getInstance().addClusters(...newClusters);
|
||||
|
||||
Notifications.ok(
|
||||
<>Successfully imported <b>{newClusters.length}</b> cluster(s)</>
|
||||
@@ -311,7 +313,7 @@ export class AddCluster extends React.Component {
|
||||
}
|
||||
|
||||
onKubeConfigInputBlur = () => {
|
||||
const isChanged = this.kubeConfigPath !== userStore.kubeConfigPath;
|
||||
const isChanged = this.kubeConfigPath !== UserStore.getInstance().kubeConfigPath;
|
||||
|
||||
if (isChanged) {
|
||||
this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir());
|
||||
@@ -319,7 +321,7 @@ export class AddCluster extends React.Component {
|
||||
try {
|
||||
this.setKubeConfig(this.kubeConfigPath, { throwError: true });
|
||||
} catch (err) {
|
||||
this.setKubeConfig(userStore.kubeConfigPath); // revert to previous valid path
|
||||
this.setKubeConfig(UserStore.getInstance().kubeConfigPath); // revert to previous valid path
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -331,7 +333,7 @@ export class AddCluster extends React.Component {
|
||||
};
|
||||
|
||||
protected formatContextLabel = ({ value: context }: SelectOption<string>) => {
|
||||
const isNew = userStore.newContexts.has(context);
|
||||
const isNew = UserStore.getInstance().newContexts.has(context);
|
||||
const isSelected = this.selectedContexts.includes(context);
|
||||
|
||||
return (
|
||||
@@ -348,7 +350,7 @@ export class AddCluster extends React.Component {
|
||||
|
||||
return (
|
||||
<DropFileInput onDropFiles={this.onDropKubeConfig}>
|
||||
<PageLayout className="AddClusters" header={<><Icon svg="logo-lens" big /> <h2>Add Clusters</h2></>} showOnTop={true}>
|
||||
<PageLayout className="AddClusters" showOnTop={true}>
|
||||
<h2>Add Clusters from Kubeconfig</h2>
|
||||
{this.renderInfo()}
|
||||
{this.renderKubeConfigSource()}
|
||||
|
||||
@@ -18,6 +18,19 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
async loadAll() {
|
||||
try {
|
||||
const res = await this.loadItems(() => helmChartsApi.list());
|
||||
|
||||
this.failedLoading = false;
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
this.failedLoading = true;
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
loadAll() {
|
||||
return this.loadItems(() => helmChartsApi.list());
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import isEqual from "lodash/isEqual";
|
||||
import { observable, reaction, toJS, makeObservable } from "mobx";
|
||||
import { Link } from "react-router-dom";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { HelmRelease, helmReleasesApi, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
|
||||
import { getRelease, getReleaseValues, HelmRelease, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
|
||||
import { HelmReleaseMenu } from "./release-menu";
|
||||
import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
@@ -19,12 +19,13 @@ import { Button } from "../button";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { Notifications } from "../notifications";
|
||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { Secret } from "../../api/endpoints";
|
||||
import { getDetailsUrl } from "../kube-object";
|
||||
import { Checkbox } from "../checkbox";
|
||||
|
||||
interface Props {
|
||||
release: HelmRelease;
|
||||
@@ -35,6 +36,8 @@ interface Props {
|
||||
export class ReleaseDetails extends Component<Props> {
|
||||
@observable details: IReleaseDetails;
|
||||
@observable values = "";
|
||||
@observable valuesLoading = false;
|
||||
@observable userSuppliedOnly = false;
|
||||
@observable saving = false;
|
||||
@observable releaseSecret: Secret;
|
||||
|
||||
@@ -70,14 +73,16 @@ export class ReleaseDetails extends Component<Props> {
|
||||
const { release } = this.props;
|
||||
|
||||
this.details = null;
|
||||
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
|
||||
this.details = await getRelease(release.getName(), release.getNs());
|
||||
}
|
||||
|
||||
async loadValues() {
|
||||
const { release } = this.props;
|
||||
|
||||
this.values = "";
|
||||
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
|
||||
this.valuesLoading = true;
|
||||
this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.userSuppliedOnly)) ?? "";
|
||||
this.valuesLoading = false;
|
||||
}
|
||||
|
||||
updateValues = async () => {
|
||||
@@ -112,21 +117,34 @@ export class ReleaseDetails extends Component<Props> {
|
||||
};
|
||||
|
||||
renderValues() {
|
||||
const { values, saving } = this;
|
||||
const { values, valuesLoading, saving } = this;
|
||||
|
||||
return (
|
||||
<div className="values">
|
||||
<DrawerTitle title="Values"/>
|
||||
<div className="flex column gaps">
|
||||
<AceEditor
|
||||
mode="yaml"
|
||||
value={values}
|
||||
onChange={values => this.values = values}
|
||||
<Checkbox
|
||||
label="User-supplied values only"
|
||||
value={this.userSuppliedOnly}
|
||||
onChange={values => {
|
||||
this.userSuppliedOnly = values;
|
||||
this.loadValues();
|
||||
}}
|
||||
disabled={valuesLoading}
|
||||
/>
|
||||
{valuesLoading
|
||||
? <Spinner />
|
||||
: <AceEditor
|
||||
mode="yaml"
|
||||
value={values}
|
||||
onChange={values => this.values = values}
|
||||
/>
|
||||
}
|
||||
<Button
|
||||
primary
|
||||
label="Save"
|
||||
waiting={saving}
|
||||
disabled={valuesLoading}
|
||||
onClick={this.updateValues}
|
||||
/>
|
||||
</div>
|
||||
@@ -246,7 +264,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
|
||||
className={cssNames("ReleaseDetails", ThemeStore.getInstance().activeTheme.type)}
|
||||
usePortal={true}
|
||||
open={!!release}
|
||||
title={title}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { HelmRelease, helmReleasesApi, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
|
||||
import { getReleaseHistory, HelmRelease, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { Notifications } from "../notifications";
|
||||
@@ -44,7 +44,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
onOpen = async () => {
|
||||
this.isLoading = true;
|
||||
const currentRevision = this.release.getRevision();
|
||||
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
|
||||
let releases = await getReleaseHistory(this.release.getName(), this.release.getNs());
|
||||
|
||||
releases = releases.filter(item => item.revision !== currentRevision); // remove current
|
||||
releases = orderBy(releases, "revision", "desc"); // sort
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { action, IReactionDisposer, makeObservable, observable, reaction, toJS, when, } from "mobx";
|
||||
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
|
||||
import { action, observable, reaction, when } from "mobx";
|
||||
import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../api/endpoints/helm-releases.api";
|
||||
import { ItemStore } from "../../item.store";
|
||||
import { Secret } from "../../api/endpoints";
|
||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
@@ -8,65 +10,65 @@ import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
import { Notifications } from "../notifications";
|
||||
|
||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
@observable releaseSecrets: Secret[] = [];
|
||||
@observable secretWatcher: IReactionDisposer;
|
||||
releaseSecrets = observable.map<string, Secret>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
makeObservable(this);
|
||||
when(() => secretsStore.isLoaded, () => {
|
||||
this.releaseSecrets = this.getReleaseSecrets();
|
||||
this.releaseSecrets.replace(this.getReleaseSecrets());
|
||||
});
|
||||
}
|
||||
|
||||
watch() {
|
||||
this.secretWatcher = reaction(() => toJS(secretsStore.items), () => {
|
||||
watchAssociatedSecrets(): (() => void) {
|
||||
return reaction(() => toJS(secretsStore.items), () => {
|
||||
if (this.isLoading) return;
|
||||
const secrets = this.getReleaseSecrets();
|
||||
const amountChanged = secrets.length !== this.releaseSecrets.length;
|
||||
const labelsChanged = this.releaseSecrets.some(item => {
|
||||
const secret = secrets.find(secret => secret.getId() == item.getId());
|
||||
|
||||
if (!secret) return;
|
||||
|
||||
return !isEqual(item.getLabels(), secret.getLabels());
|
||||
});
|
||||
const newSecrets = this.getReleaseSecrets();
|
||||
const amountChanged = newSecrets.length !== this.releaseSecrets.size;
|
||||
const labelsChanged = newSecrets.some(([id, secret]) => (
|
||||
!isEqual(secret.getLabels(), this.releaseSecrets.get(id)?.getLabels())
|
||||
));
|
||||
|
||||
if (amountChanged || labelsChanged) {
|
||||
this.loadFromContextNamespaces();
|
||||
}
|
||||
this.releaseSecrets = [...secrets];
|
||||
this.releaseSecrets.replace(newSecrets);
|
||||
});
|
||||
}
|
||||
|
||||
unwatch() {
|
||||
this.secretWatcher();
|
||||
watchSelecteNamespaces(): (() => void) {
|
||||
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
|
||||
this.loadAll(namespaces);
|
||||
});
|
||||
}
|
||||
|
||||
getReleaseSecrets() {
|
||||
return secretsStore.getByLabel({ owner: "helm" });
|
||||
private getReleaseSecrets() {
|
||||
return secretsStore
|
||||
.getByLabel({ owner: "helm" })
|
||||
.map(s => [s.getId(), s] as const);
|
||||
}
|
||||
|
||||
getReleaseSecret(release: HelmRelease) {
|
||||
const labels = {
|
||||
return secretsStore.getByLabel({
|
||||
owner: "helm",
|
||||
name: release.getName()
|
||||
};
|
||||
|
||||
return secretsStore.getByLabel(labels)
|
||||
.filter(secret => secret.getNs() == release.getNs())[0];
|
||||
})
|
||||
.find(secret => secret.getNs() == release.getNs());
|
||||
}
|
||||
|
||||
@action
|
||||
async loadAll(namespaces: string[]) {
|
||||
this.isLoading = true;
|
||||
this.isLoaded = false;
|
||||
|
||||
try {
|
||||
const items = await this.loadItems(namespaces);
|
||||
|
||||
this.items.replace(this.sortItems(items));
|
||||
this.isLoaded = true;
|
||||
this.failedLoading = false;
|
||||
} catch (error) {
|
||||
this.failedLoading = true;
|
||||
console.error("Loading Helm Chart releases has failed", error);
|
||||
|
||||
if (error.error) {
|
||||
@@ -78,24 +80,25 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
}
|
||||
|
||||
async loadFromContextNamespaces(): Promise<void> {
|
||||
return this.loadAll(namespaceStore.contextNamespaces);
|
||||
return this.loadAll(namespaceStore.context.contextNamespaces);
|
||||
}
|
||||
|
||||
async loadItems(namespaces: string[]) {
|
||||
const isLoadingAll = namespaceStore.allowedNamespaces.every(ns => namespaces.includes(ns));
|
||||
const noAccessibleNamespaces = namespaceStore.context.cluster.accessibleNamespaces.length === 0;
|
||||
const isLoadingAll = namespaceStore.context.allNamespaces?.length > 1
|
||||
&& namespaceStore.context.cluster.accessibleNamespaces.length === 0
|
||||
&& namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
|
||||
|
||||
if (isLoadingAll && noAccessibleNamespaces) {
|
||||
return helmReleasesApi.list();
|
||||
} else {
|
||||
return Promise
|
||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||
.then(items => items.flat());
|
||||
if (isLoadingAll) {
|
||||
return listReleases();
|
||||
}
|
||||
|
||||
return Promise // load resources per namespace
|
||||
.all(namespaces.map(namespace => listReleases(namespace)))
|
||||
.then(items => items.flat());
|
||||
}
|
||||
|
||||
async create(payload: IReleaseCreatePayload) {
|
||||
const response = await helmReleasesApi.create(payload);
|
||||
const response = await createRelease(payload);
|
||||
|
||||
if (this.isLoaded) this.loadFromContextNamespaces();
|
||||
|
||||
@@ -103,7 +106,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
}
|
||||
|
||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
||||
const response = await helmReleasesApi.update(name, namespace, payload);
|
||||
const response = await updateRelease(name, namespace, payload);
|
||||
|
||||
if (this.isLoaded) this.loadFromContextNamespaces();
|
||||
|
||||
@@ -111,7 +114,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
}
|
||||
|
||||
async rollback(name: string, namespace: string, revision: number) {
|
||||
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
||||
const response = await rollbackRelease(name, namespace, revision);
|
||||
|
||||
if (this.isLoaded) this.loadFromContextNamespaces();
|
||||
|
||||
@@ -119,7 +122,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
}
|
||||
|
||||
async remove(release: HelmRelease) {
|
||||
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.getNs()));
|
||||
return super.removeItem(release, () => deleteRelease(release.getName(), release.getNs()));
|
||||
}
|
||||
|
||||
async removeSelectedItems() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import "./releases.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import kebabCase from "lodash/kebabCase";
|
||||
import { observer } from "mobx-react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { releaseStore } from "./release.store";
|
||||
import { IReleaseRouteParams, releaseURL } from "./release.route";
|
||||
@@ -30,14 +30,11 @@ interface Props extends RouteComponentProps<IReleaseRouteParams> {
|
||||
|
||||
@observer
|
||||
export class HelmReleases extends Component<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
// Watch for secrets associated with releases and react to their changes
|
||||
releaseStore.watch();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
releaseStore.unwatch();
|
||||
disposeOnUnmount(this, [
|
||||
releaseStore.watchAssociatedSecrets(),
|
||||
releaseStore.watchSelecteNamespaces(),
|
||||
]);
|
||||
}
|
||||
|
||||
get selectedRelease() {
|
||||
@@ -49,21 +46,16 @@ export class HelmReleases extends Component<Props> {
|
||||
}
|
||||
|
||||
showDetails = (item: HelmRelease) => {
|
||||
if (!item) {
|
||||
navigation.merge(releaseURL());
|
||||
}
|
||||
else {
|
||||
navigation.merge(releaseURL({
|
||||
params: {
|
||||
name: item.getName(),
|
||||
namespace: item.getNs()
|
||||
}
|
||||
}));
|
||||
}
|
||||
navigation.merge(releaseURL({
|
||||
params: {
|
||||
name: item.getName(),
|
||||
namespace: item.getNs()
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
hideDetails = () => {
|
||||
this.showDetails(null);
|
||||
navigation.merge(releaseURL());
|
||||
};
|
||||
|
||||
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
|
||||
@@ -114,30 +106,22 @@ export class HelmReleases extends Component<Props> {
|
||||
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
|
||||
{ title: "Updated", className: "updated", sortBy: columnId.updated, id: columnId.updated },
|
||||
]}
|
||||
renderTableContents={(release: HelmRelease) => {
|
||||
const version = release.getVersion();
|
||||
|
||||
return [
|
||||
release.getName(),
|
||||
release.getNs(),
|
||||
release.getChart(),
|
||||
release.getRevision(),
|
||||
<>
|
||||
{version}
|
||||
</>,
|
||||
release.appVersion,
|
||||
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
|
||||
release.getUpdated(),
|
||||
];
|
||||
}}
|
||||
renderItemMenu={(release: HelmRelease) => {
|
||||
return (
|
||||
<HelmReleaseMenu
|
||||
release={release}
|
||||
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
renderTableContents={(release: HelmRelease) => [
|
||||
release.getName(),
|
||||
release.getNs(),
|
||||
release.getChart(),
|
||||
release.getRevision(),
|
||||
release.getVersion(),
|
||||
release.appVersion,
|
||||
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
|
||||
release.getUpdated(),
|
||||
]}
|
||||
renderItemMenu={(release: HelmRelease) => (
|
||||
<HelmReleaseMenu
|
||||
release={release}
|
||||
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
|
||||
/>
|
||||
)}
|
||||
customizeRemoveDialog={(selectedItems: HelmRelease[]) => ({
|
||||
message: this.renderRemoveDialogMessage(selectedItems)
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.CatalogAddButton {
|
||||
position: absolute;
|
||||
right: 40px;
|
||||
bottom: 30px;
|
||||
|
||||
.MuiFab-primary {
|
||||
background-color: var(--blue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import "./catalog-add-button.scss";
|
||||
import React from "react";
|
||||
import { SpeedDial, SpeedDialAction } from "@material-ui/lab";
|
||||
import { Icon } from "../icon";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { autobind } from "../../../common/utils";
|
||||
import { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityContextMenu } from "../../api/catalog-entity";
|
||||
import { EventEmitter } from "events";
|
||||
import { navigate } from "../../navigation";
|
||||
|
||||
export type CatalogAddButtonProps = {
|
||||
category: CatalogCategory
|
||||
};
|
||||
|
||||
@observer
|
||||
export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
|
||||
@observable protected isOpen = false;
|
||||
protected menuItems = observable.array<CatalogEntityContextMenu>([]);
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.category, (category) => {
|
||||
this.menuItems.clear();
|
||||
|
||||
if (category && category instanceof EventEmitter) {
|
||||
const context: CatalogEntityAddMenuContext = {
|
||||
navigate: (url: string) => navigate(url),
|
||||
menuItems: this.menuItems
|
||||
};
|
||||
|
||||
category.emit("onCatalogAddMenu", context);
|
||||
}
|
||||
}, { fireImmediately: true })
|
||||
]);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onOpen() {
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onClose() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.menuItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SpeedDial
|
||||
className="CatalogAddButton"
|
||||
ariaLabel="SpeedDial CatalogAddButton"
|
||||
open={this.isOpen}
|
||||
onOpen={this.onOpen}
|
||||
onClose={this.onClose}
|
||||
icon={<Icon material="add" />}
|
||||
direction="up"
|
||||
>
|
||||
{ this.menuItems.map((menuItem, index) => {
|
||||
return <SpeedDialAction
|
||||
key={index}
|
||||
icon={<Icon material={menuItem.icon} />}
|
||||
tooltipTitle={menuItem.title}
|
||||
onClick={() => menuItem.onClick()}
|
||||
/>;
|
||||
})}
|
||||
</SpeedDial>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,6 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
|
||||
@computed get entities() {
|
||||
if (!this.activeCategory) return [];
|
||||
|
||||
console.log("computing entities", this.activeCategory);
|
||||
|
||||
return catalogEntityRegistry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
> .sidebar {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
|
||||
.sidebarTabs {
|
||||
margin-top: 5px;
|
||||
|
||||
@@ -2,7 +2,7 @@ import "./catalog.scss";
|
||||
import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { ItemListLayout } from "../item-object-list";
|
||||
import { observable, reaction, makeObservable } from "mobx";
|
||||
import { action, observable, reaction, makeObservable } from "mobx";
|
||||
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
||||
import { navigate } from "../../navigation";
|
||||
import { kebabCase } from "lodash";
|
||||
@@ -11,13 +11,13 @@ import { MenuItem, MenuActions } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
|
||||
import { Badge } from "../badge";
|
||||
import { hotbarStore } from "../../../common/hotbar-store";
|
||||
import { addClusterURL } from "../+add-cluster";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { autobind } from "../../utils";
|
||||
import { Notifications } from "../notifications";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Tab, Tabs } from "../tabs";
|
||||
import { catalogCategoryRegistry } from "../../../common/catalog-category-registry";
|
||||
import { CatalogAddButton } from "./catalog-add-button";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
@@ -62,7 +62,7 @@ export class Catalog extends React.Component {
|
||||
}
|
||||
|
||||
addToHotbar(item: CatalogEntityItem) {
|
||||
const hotbar = hotbarStore.getByName("default"); // FIXME
|
||||
const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME
|
||||
|
||||
if (!hotbar) {
|
||||
return;
|
||||
@@ -72,7 +72,7 @@ export class Catalog extends React.Component {
|
||||
}
|
||||
|
||||
removeFromHotbar(item: CatalogEntityItem) {
|
||||
const hotbar = hotbarStore.getByName("default"); // FIXME
|
||||
const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME
|
||||
|
||||
if (!hotbar) {
|
||||
return;
|
||||
@@ -106,6 +106,7 @@ export class Catalog extends React.Component {
|
||||
return catalogCategoryRegistry.items;
|
||||
}
|
||||
|
||||
@action
|
||||
onTabChange = (tabId: string) => {
|
||||
this.activeTab = tabId;
|
||||
|
||||
@@ -131,6 +132,7 @@ export class Catalog extends React.Component {
|
||||
|
||||
@autobind()
|
||||
renderItemMenu(item: CatalogEntityItem) {
|
||||
const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
|
||||
const onOpen = async () => {
|
||||
await item.onContextMenuOpen(this.contextMenu);
|
||||
};
|
||||
@@ -143,7 +145,7 @@ export class Catalog extends React.Component {
|
||||
<MenuItem key="remove-from-hotbar" onClick={() => this.removeFromHotbar(item) }>
|
||||
<Icon material="clear" small interactive={true} title="Remove from hotbar"/> Remove from Hotbar
|
||||
</MenuItem>
|
||||
{ this.contextMenu.menuItems.map((menuItem, index) => {
|
||||
{ menuItems.map((menuItem, index) => {
|
||||
return (
|
||||
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||
<Icon material={menuItem.icon} small interactive={true} title={menuItem.title}/> {menuItem.title}
|
||||
@@ -154,6 +156,7 @@ export class Catalog extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
if (!this.catalogEntityStore) {
|
||||
return null;
|
||||
@@ -166,6 +169,7 @@ export class Catalog extends React.Component {
|
||||
provideBackButtonNavigation={false}
|
||||
contentGaps={false}>
|
||||
<ItemListLayout
|
||||
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name}
|
||||
isClusterScoped
|
||||
isSearchable={true}
|
||||
isSelectable={false}
|
||||
@@ -191,11 +195,8 @@ export class Catalog extends React.Component {
|
||||
]}
|
||||
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
|
||||
renderItemMenu={this.renderItemMenu}
|
||||
addRemoveButtons={{
|
||||
addTooltip: "Add Kubernetes Cluster",
|
||||
onAdd: () => navigate(addClusterURL()),
|
||||
}}
|
||||
/>
|
||||
<CatalogAddButton category={this.catalogEntityStore.activeCategory} />
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
|
||||
}
|
||||
|
||||
export const clusterSettingsRoute: RouteProps = {
|
||||
path: `/cluster/:clusterId/settings`,
|
||||
};
|
||||
|
||||
export const clusterSettingsURL = buildURL<IClusterSettingsRouteParams>(clusterSettingsRoute.path);
|
||||
@@ -1,51 +0,0 @@
|
||||
.ClusterSettings {
|
||||
$spacing: $padding * 3;
|
||||
|
||||
> .content-wrapper {
|
||||
--flex-gap: #{$spacing};
|
||||
}
|
||||
|
||||
// TODO: move sub-component styles to separate files
|
||||
.admin-note {
|
||||
font-size: small;
|
||||
opacity: 0.5;
|
||||
margin-left: $margin;
|
||||
}
|
||||
|
||||
.button-area {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
|
||||
.file-loader {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
|
||||
.status-table {
|
||||
margin: $spacing 0;
|
||||
|
||||
.Table {
|
||||
border: 1px solid var(--drawerSubtitleBackground);
|
||||
border-radius: $radius;
|
||||
|
||||
.TableRow {
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid var(--drawerSubtitleBackground);
|
||||
}
|
||||
|
||||
.value {
|
||||
flex-grow: 2;
|
||||
word-break: break-word;
|
||||
color: var(--textColorSecondary);
|
||||
}
|
||||
|
||||
.link {
|
||||
@include pseudo-link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Input, .Select {
|
||||
margin-top: $padding;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import "./cluster-settings.scss";
|
||||
|
||||
import React from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { Status } from "./status";
|
||||
import { General } from "./general";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { requestMain } from "../../../common/ipc";
|
||||
import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc";
|
||||
import { navigation } from "../../navigation";
|
||||
|
||||
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterSettings extends React.Component<Props> {
|
||||
get clusterId() {
|
||||
return this.props.match.params.clusterId;
|
||||
}
|
||||
|
||||
get cluster(): Cluster {
|
||||
return clusterStore.getById(this.clusterId);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { hash } = navigation.location;
|
||||
|
||||
document.getElementById(hash.slice(1))?.scrollIntoView();
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.cluster, this.refreshCluster, {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
|
||||
fireImmediately: true,
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
refreshCluster = async () => {
|
||||
if (this.cluster) {
|
||||
await requestMain(clusterActivateHandler, this.cluster.id);
|
||||
await requestMain(clusterRefreshHandler, this.cluster.id);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const cluster = this.cluster;
|
||||
|
||||
if (!cluster) return null;
|
||||
const header = (
|
||||
<>
|
||||
<h2>{cluster.preferences.clusterName}</h2>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageLayout className="ClusterSettings" header={header} showOnTop={true}>
|
||||
<Status cluster={cluster}></Status>
|
||||
<General cluster={cluster}></General>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { ClusterNameSetting } from "./components/cluster-name-setting";
|
||||
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
||||
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
||||
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
||||
import { ClusterAccessibleNamespaces } from "./components/cluster-accessible-namespaces";
|
||||
import { ClusterMetricsSetting } from "./components/cluster-metrics-setting";
|
||||
import { ShowMetricsSetting } from "./components/show-metrics";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export class General extends React.Component<Props> {
|
||||
render() {
|
||||
return <div>
|
||||
<h2>General</h2>
|
||||
<ClusterNameSetting cluster={this.props.cluster} />
|
||||
<ClusterProxySetting cluster={this.props.cluster} />
|
||||
<ClusterPrometheusSetting cluster={this.props.cluster} />
|
||||
<ClusterHomeDirSetting cluster={this.props.cluster} />
|
||||
<ClusterAccessibleNamespaces cluster={this.props.cluster} />
|
||||
<ClusterMetricsSetting cluster={this.props.cluster}/>
|
||||
<ShowMetricsSetting cluster={this.props.cluster}/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Table, TableCell, TableRow } from "../table";
|
||||
import { autobind } from "../../utils";
|
||||
import { shell } from "electron";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
export class Status extends React.Component<Props> {
|
||||
|
||||
@autobind()
|
||||
openKubeconfig() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
shell.showItemInFolder(cluster.kubeConfigPath);
|
||||
}
|
||||
|
||||
renderStatusRows() {
|
||||
const { cluster } = this.props;
|
||||
const rows = [
|
||||
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
|
||||
["Distribution", cluster.metadata.distribution ? String(cluster.metadata.distribution) : "N/A"],
|
||||
["Kernel Version", cluster.metadata.version ? String(cluster.metadata.version) : "N/A"],
|
||||
["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]) => {
|
||||
return (
|
||||
<TableRow key={name}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell className="value">{value}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
<TableRow>
|
||||
<TableCell>Kubeconfig</TableCell>
|
||||
<TableCell className="link value" onClick={this.openKubeconfig}>{cluster.kubeConfigPath}</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<h2>Status</h2>
|
||||
<SubTitle title="Cluster Status"/>
|
||||
<p>
|
||||
Cluster status information including: detected distribution, kernel version, and online status.
|
||||
</p>
|
||||
<div className="status-table">
|
||||
{this.renderStatusRows()}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { eventStore } from "../+events/event.store";
|
||||
import { autobind, cssNames, prevDefault } from "../../utils";
|
||||
import { ItemObject } from "../../item.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { kubeSelectedUrlParam, showDetails } from "../kube-object";
|
||||
|
||||
@@ -24,6 +24,7 @@ interface IWarning extends ItemObject {
|
||||
message: string;
|
||||
selfLink: string;
|
||||
age: string | number;
|
||||
timeDiffFromNow: number;
|
||||
}
|
||||
|
||||
enum sortBy {
|
||||
@@ -37,7 +38,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
private sortCallbacks = {
|
||||
[sortBy.type]: (warning: IWarning) => warning.kind,
|
||||
[sortBy.object]: (warning: IWarning) => warning.getName(),
|
||||
[sortBy.age]: (warning: IWarning) => warning.age || "",
|
||||
[sortBy.age]: (warning: IWarning) => warning.timeDiffFromNow,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -50,13 +51,14 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
|
||||
// Node bad conditions
|
||||
nodesStore.items.forEach(node => {
|
||||
const { kind, selfLink, getId, getName, getAge } = node;
|
||||
const { kind, selfLink, getId, getName, getAge, getTimeDiffFromNow } = node;
|
||||
|
||||
node.getWarningConditions().forEach(({ message }) => {
|
||||
warnings.push({
|
||||
age: getAge(),
|
||||
getId,
|
||||
getName,
|
||||
timeDiffFromNow: getTimeDiffFromNow(),
|
||||
kind,
|
||||
message,
|
||||
selfLink,
|
||||
@@ -68,12 +70,13 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
const events = eventStore.getWarnings();
|
||||
|
||||
events.forEach(error => {
|
||||
const { message, involvedObject, getAge } = error;
|
||||
const { message, involvedObject, getAge, getTimeDiffFromNow } = error;
|
||||
const { uid, name, kind } = involvedObject;
|
||||
|
||||
warnings.push({
|
||||
getId: () => uid,
|
||||
getName: () => name,
|
||||
timeDiffFromNow: getTimeDiffFromNow(),
|
||||
age: getAge(),
|
||||
message,
|
||||
kind,
|
||||
@@ -147,7 +150,7 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
|
||||
sortSyncWithUrl={false}
|
||||
getTableRow={this.getTableRow}
|
||||
className={cssNames("box grow", themeStore.activeTheme.type)}
|
||||
className={cssNames("box grow", ThemeStore.getInstance().activeTheme.type)}
|
||||
>
|
||||
<TableHead nowrap>
|
||||
<TableCell className="message">Message</TableCell>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { podsStore } from "../+workloads-pods/pods.store";
|
||||
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
||||
import { ClusterStore, getHostedCluster } from "../../../common/cluster-store";
|
||||
import { interval } from "../../utils";
|
||||
import { TabLayout } from "../layout/tab-layout";
|
||||
import { Spinner } from "../spinner";
|
||||
@@ -13,7 +13,7 @@ import { ClusterIssues } from "./cluster-issues";
|
||||
import { ClusterMetrics } from "./cluster-metrics";
|
||||
import { clusterOverviewStore } from "./cluster-overview.store";
|
||||
import { ClusterPieCharts } from "./cluster-pie-charts";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
|
||||
@observer
|
||||
export class ClusterOverview extends React.Component {
|
||||
@@ -66,7 +66,7 @@ export class ClusterOverview extends React.Component {
|
||||
|
||||
render() {
|
||||
const isLoaded = nodesStore.isLoaded && podsStore.isLoaded;
|
||||
const isMetricsHidden = clusterStore.isMetricHidden(ResourceType.Cluster);
|
||||
const isMetricsHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Cluster);
|
||||
|
||||
return (
|
||||
<TabLayout>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { nodesStore } from "../+nodes/nodes.store";
|
||||
import { ChartData, PieChart } from "../chart";
|
||||
import { ClusterNoMetrics } from "./cluster-no-metrics";
|
||||
import { bytesToUnits } from "../../utils";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
|
||||
|
||||
export const ClusterPieCharts = observer(() => {
|
||||
@@ -29,7 +29,7 @@ export const ClusterPieCharts = observer(() => {
|
||||
const { podUsage, podCapacity } = data;
|
||||
const cpuLimitsOverload = cpuLimits > cpuCapacity;
|
||||
const memoryLimitsOverload = memoryLimits > memoryCapacity;
|
||||
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
|
||||
const defaultColor = ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor;
|
||||
|
||||
if (!memoryCapacity || !cpuCapacity || !podCapacity) return null;
|
||||
const cpuData: ChartData = {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../../../common/utils/buildUrl";
|
||||
|
||||
export interface EntitySettingsRouteParams {
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
export const entitySettingsRoute: RouteProps = {
|
||||
path: `/entity/:entityId/settings`,
|
||||
};
|
||||
|
||||
export const entitySettingsURL = buildURL<EntitySettingsRouteParams>(entitySettingsRoute.path);
|
||||
@@ -0,0 +1,23 @@
|
||||
.EntitySettings {
|
||||
$spacing: $padding * 3;
|
||||
|
||||
|
||||
// TODO: move sub-component styles to separate files
|
||||
.admin-note {
|
||||
font-size: small;
|
||||
opacity: 0.5;
|
||||
margin-left: $margin;
|
||||
}
|
||||
|
||||
.button-area {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
|
||||
.file-loader {
|
||||
margin-top: $margin * 2;
|
||||
}
|
||||
|
||||
.Input, .Select {
|
||||
margin-top: $padding;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import "./entity-settings.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observable } from "mobx";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { observer } from "mobx-react";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { navigation } from "../../navigation";
|
||||
import { Tabs, Tab } from "../tabs";
|
||||
import { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { entitySettingRegistry } from "../../../extensions/registries";
|
||||
import { EntitySettingsRouteParams } from "./entity-settings.route";
|
||||
|
||||
interface Props extends RouteComponentProps<EntitySettingsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class EntitySettings extends React.Component<Props> {
|
||||
@observable activeTab: string;
|
||||
|
||||
get entityId() {
|
||||
return this.props.match.params.entityId;
|
||||
}
|
||||
|
||||
get entity(): CatalogEntity {
|
||||
return catalogEntityRegistry.getById(this.entityId);
|
||||
}
|
||||
|
||||
get menuItems() {
|
||||
if (!this.entity) return [];
|
||||
|
||||
return entitySettingRegistry.getItemsForKind(this.entity.kind, this.entity.apiVersion, this.entity.metadata.source);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { hash } = navigation.location;
|
||||
|
||||
this.ensureActiveTab();
|
||||
|
||||
document.getElementById(hash.slice(1))?.scrollIntoView();
|
||||
}
|
||||
|
||||
onTabChange = (tabId: string) => {
|
||||
this.activeTab = tabId;
|
||||
};
|
||||
|
||||
renderNavigation() {
|
||||
return (
|
||||
<>
|
||||
<h2>{this.entity.metadata.name}</h2>
|
||||
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
||||
<div className="header">Settings</div>
|
||||
{ this.menuItems.map((setting) => (
|
||||
<Tab
|
||||
key={setting.id}
|
||||
value={setting.id}
|
||||
label={setting.title}
|
||||
data-testid={`${setting.id}-tab`}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ensureActiveTab() {
|
||||
if (!this.activeTab) {
|
||||
this.activeTab = this.menuItems[0]?.id;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.entity) {
|
||||
console.error("entity not found", this.entityId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
this.ensureActiveTab();
|
||||
const activeSetting = this.menuItems.find((setting) => setting.id === this.activeTab);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
className="CatalogEntitySettings"
|
||||
navigation={this.renderNavigation()}
|
||||
showOnTop={true}
|
||||
contentGaps={false}
|
||||
>
|
||||
<section>
|
||||
<h2 data-testid={`${activeSetting.id}-header`}>{activeSetting.title}</h2>
|
||||
<section>
|
||||
<activeSetting.components.View entity={this.entity} />
|
||||
</section>
|
||||
</section>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import "../cluster-settings";
|
||||
|
||||
export * from "./entity-settings.route";
|
||||
export * from "./entity-settings";
|
||||
@@ -11,6 +11,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<KubeEvent> {
|
||||
}
|
||||
@@ -38,10 +39,10 @@ export class EventDetails extends React.Component<Props> {
|
||||
{event.getSource()}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="First seen">
|
||||
{event.getFirstSeenTime()} ago {event.firstTimestamp}
|
||||
{event.getFirstSeenTime()} ago (<LocaleDate date={event.firstTimestamp} />)
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Last seen">
|
||||
{event.getLastSeenTime()} ago {event.lastTimestamp}
|
||||
{event.getLastSeenTime()} ago (<LocaleDate date={event.lastTimestamp} />)
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Count">
|
||||
{count}
|
||||
|
||||
@@ -2,10 +2,12 @@ import "@testing-library/jest-dom/extend-expect";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import fse from "fs-extra";
|
||||
import React from "react";
|
||||
import { extensionDiscovery } from "../../../../extensions/extension-discovery";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import { ExtensionDiscovery } from "../../../../extensions/extension-discovery";
|
||||
import { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { ExtensionStateStore } from "../extension-install.store";
|
||||
import { Extensions } from "../extensions";
|
||||
|
||||
jest.mock("fs-extra");
|
||||
@@ -18,46 +20,52 @@ jest.mock("../../../../common/utils", () => ({
|
||||
extractTar: jest.fn(() => Promise.resolve())
|
||||
}));
|
||||
|
||||
jest.mock("../../../../extensions/extension-discovery", () => ({
|
||||
...jest.requireActual("../../../../extensions/extension-discovery"),
|
||||
extensionDiscovery: {
|
||||
localFolderPath: "/fake/path",
|
||||
uninstallExtension: jest.fn(() => Promise.resolve()),
|
||||
isLoaded: true
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock("../../../../extensions/extension-loader", () => ({
|
||||
...jest.requireActual("../../../../extensions/extension-loader"),
|
||||
extensionLoader: {
|
||||
userExtensions: new Map([
|
||||
["extensionId", {
|
||||
id: "extensionId",
|
||||
manifest: {
|
||||
name: "test",
|
||||
version: "1.2.3"
|
||||
},
|
||||
absolutePath: "/absolute/path",
|
||||
manifestPath: "/symlinked/path/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
}]
|
||||
])
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock("../../notifications", () => ({
|
||||
ok: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn()
|
||||
}));
|
||||
|
||||
jest.mock("electron", () => {
|
||||
return {
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: (): void => void 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe("Extensions", () => {
|
||||
beforeEach(() => {
|
||||
ExtensionStateStore.resetInstance();
|
||||
beforeEach(async () => {
|
||||
UserStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
|
||||
await UserStore.getInstanceOrCreate().load();
|
||||
await ThemeStore.getInstanceOrCreate().init();
|
||||
|
||||
ExtensionLoader.resetInstance();
|
||||
ExtensionDiscovery.resetInstance();
|
||||
Extensions.installStates.clear();
|
||||
|
||||
ExtensionDiscovery.getInstanceOrCreate().uninstallExtension = jest.fn(() => Promise.resolve());
|
||||
|
||||
ExtensionLoader.getInstanceOrCreate().addExtension({
|
||||
id: "extensionId",
|
||||
manifest: {
|
||||
name: "test",
|
||||
version: "1.2.3"
|
||||
},
|
||||
absolutePath: "/absolute/path",
|
||||
manifestPath: "/symlinked/path/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true
|
||||
});
|
||||
});
|
||||
|
||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||
ExtensionDiscovery.getInstance().isLoaded = true;
|
||||
render(<><Extensions /><ConfirmDialog/></>);
|
||||
|
||||
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
|
||||
@@ -68,13 +76,14 @@ describe("Extensions", () => {
|
||||
// Approve confirm dialog
|
||||
fireEvent.click(screen.getByText("Yes"));
|
||||
|
||||
expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled();
|
||||
expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
|
||||
expect(screen.getByText("Disable").closest("button")).toBeDisabled();
|
||||
expect(screen.getByText("Uninstall").closest("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("displays error notification on uninstall error", () => {
|
||||
(extensionDiscovery.uninstallExtension as any).mockImplementationOnce(() =>
|
||||
ExtensionDiscovery.getInstance().isLoaded = true;
|
||||
(ExtensionDiscovery.getInstance().uninstallExtension as any).mockImplementationOnce(() =>
|
||||
Promise.reject()
|
||||
);
|
||||
render(<><Extensions /><ConfirmDialog/></>);
|
||||
@@ -115,14 +124,14 @@ describe("Extensions", () => {
|
||||
});
|
||||
|
||||
it("displays spinner while extensions are loading", () => {
|
||||
extensionDiscovery.isLoaded = false;
|
||||
ExtensionDiscovery.getInstance().isLoaded = false;
|
||||
const { container } = render(<Extensions />);
|
||||
|
||||
expect(container.querySelector(".Spinner")).toBeInTheDocument();
|
||||
|
||||
extensionDiscovery.isLoaded = true;
|
||||
ExtensionDiscovery.getInstance().isLoaded = true;
|
||||
|
||||
waitFor(() =>
|
||||
waitFor(() =>
|
||||
expect(container.querySelector(".Spinner")).not.toBeInTheDocument()
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { observable } from "mobx";
|
||||
import { Singleton } from "../../utils";
|
||||
|
||||
interface ExtensionState {
|
||||
displayName: string;
|
||||
// Possible states the extension can be
|
||||
state: "installing" | "uninstalling";
|
||||
}
|
||||
|
||||
export class ExtensionStateStore extends Singleton {
|
||||
extensionState = observable.map<string, ExtensionState>();
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import path from "path";
|
||||
import React from "react";
|
||||
import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils";
|
||||
import { docsUrl } from "../../../common/vars";
|
||||
import { extensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
|
||||
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||
import { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
|
||||
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||
import { extensionDisplayName, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
|
||||
import logger from "../../../main/logger";
|
||||
import { prevDefault } from "../../utils";
|
||||
@@ -21,7 +21,6 @@ import { SubTitle } from "../layout/sub-title";
|
||||
import { Notifications } from "../notifications";
|
||||
import { Spinner } from "../spinner/spinner";
|
||||
import { TooltipPosition } from "../tooltip";
|
||||
import { ExtensionStateStore } from "./extension-install.store";
|
||||
import "./extensions.scss";
|
||||
|
||||
interface InstallRequest {
|
||||
@@ -39,6 +38,12 @@ interface InstallRequestValidated extends InstallRequestPreloaded {
|
||||
tempFile: string; // temp system path to packed extension for unpacking
|
||||
}
|
||||
|
||||
interface ExtensionState {
|
||||
displayName: string;
|
||||
// Possible states the extension can be
|
||||
state: "installing" | "uninstalling";
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Extensions extends React.Component {
|
||||
private static supportedFormats = ["tar", "tgz"];
|
||||
@@ -50,6 +55,8 @@ export class Extensions extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
static installStates = observable.map<string, ExtensionState>();
|
||||
|
||||
constructor(props: object) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
@@ -69,7 +76,7 @@ export class Extensions extends React.Component {
|
||||
* Extensions that were removed from extensions but are still in "uninstalling" state
|
||||
*/
|
||||
@computed get removedUninstalling() {
|
||||
return Array.from(this.extensionStateStore.extensionState.entries())
|
||||
return Array.from(Extensions.installStates.entries())
|
||||
.filter(([id, extension]) =>
|
||||
extension.state === "uninstalling"
|
||||
&& !this.extensions.find(extension => extension.id === id)
|
||||
@@ -81,7 +88,7 @@ export class Extensions extends React.Component {
|
||||
* Extensions that were added to extensions but are still in "installing" state
|
||||
*/
|
||||
@computed get addedInstalling() {
|
||||
return Array.from(this.extensionStateStore.extensionState.entries())
|
||||
return Array.from(Extensions.installStates.entries())
|
||||
.filter(([id, extension]) =>
|
||||
extension.state === "installing"
|
||||
&& this.extensions.find(extension => extension.id === id)
|
||||
@@ -96,7 +103,7 @@ export class Extensions extends React.Component {
|
||||
Notifications.ok(
|
||||
<p>Extension <b>{displayName}</b> successfully uninstalled!</p>
|
||||
);
|
||||
this.extensionStateStore.extensionState.delete(id);
|
||||
Extensions.installStates.delete(id);
|
||||
});
|
||||
|
||||
this.addedInstalling.forEach(({ id, displayName }) => {
|
||||
@@ -109,7 +116,7 @@ export class Extensions extends React.Component {
|
||||
Notifications.ok(
|
||||
<p>Extension <b>{displayName}</b> successfully installed!</p>
|
||||
);
|
||||
this.extensionStateStore.extensionState.delete(id);
|
||||
Extensions.installStates.delete(id);
|
||||
this.installPath = "";
|
||||
|
||||
// Enable installed extensions by default.
|
||||
@@ -122,7 +129,7 @@ export class Extensions extends React.Component {
|
||||
@computed get extensions() {
|
||||
const searchText = this.search.toLowerCase();
|
||||
|
||||
return Array.from(extensionLoader.userExtensions.values())
|
||||
return Array.from(ExtensionLoader.getInstance().userExtensions.values())
|
||||
.filter(({ manifest: { name, description }}) => (
|
||||
name.toLowerCase().includes(searchText)
|
||||
|| description?.toLowerCase().includes(searchText)
|
||||
@@ -130,7 +137,7 @@ export class Extensions extends React.Component {
|
||||
}
|
||||
|
||||
get extensionsPath() {
|
||||
return extensionDiscovery.localFolderPath;
|
||||
return ExtensionDiscovery.getInstance().localFolderPath;
|
||||
}
|
||||
|
||||
getExtensionPackageTemp(fileName = "") {
|
||||
@@ -347,11 +354,11 @@ export class Extensions extends React.Component {
|
||||
|
||||
async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) {
|
||||
const displayName = extensionDisplayName(name, version);
|
||||
const extensionId = path.join(extensionDiscovery.nodeModulesPath, name, "package.json");
|
||||
const extensionId = path.join(ExtensionDiscovery.getInstance().nodeModulesPath, name, "package.json");
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
this.extensionStateStore.extensionState.set(extensionId, {
|
||||
Extensions.installStates.set(extensionId, {
|
||||
state: "installing",
|
||||
displayName
|
||||
});
|
||||
@@ -386,8 +393,8 @@ export class Extensions extends React.Component {
|
||||
);
|
||||
|
||||
// Remove install state on install failure
|
||||
if (this.extensionStateStore.extensionState.get(extensionId)?.state === "installing") {
|
||||
this.extensionStateStore.extensionState.delete(extensionId);
|
||||
if (Extensions.installStates.get(extensionId)?.state === "installing") {
|
||||
Extensions.installStates.delete(extensionId);
|
||||
}
|
||||
} finally {
|
||||
// clean up
|
||||
@@ -411,20 +418,20 @@ export class Extensions extends React.Component {
|
||||
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
|
||||
|
||||
try {
|
||||
this.extensionStateStore.extensionState.set(extension.id, {
|
||||
Extensions.installStates.set(extension.id, {
|
||||
state: "uninstalling",
|
||||
displayName
|
||||
});
|
||||
|
||||
await extensionDiscovery.uninstallExtension(extension);
|
||||
await ExtensionDiscovery.getInstance().uninstallExtension(extension);
|
||||
} catch (error) {
|
||||
Notifications.error(
|
||||
<p>Uninstalling extension <b>{displayName}</b> has failed: <em>{error?.message ?? ""}</em></p>
|
||||
);
|
||||
|
||||
// Remove uninstall state on uninstall failure
|
||||
if (this.extensionStateStore.extensionState.get(extension.id)?.state === "uninstalling") {
|
||||
this.extensionStateStore.extensionState.delete(extension.id);
|
||||
if (Extensions.installStates.get(extension.id)?.state === "uninstalling") {
|
||||
Extensions.installStates.delete(extension.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,7 +457,7 @@ export class Extensions extends React.Component {
|
||||
return extensions.map(extension => {
|
||||
const { id, isEnabled, manifest } = extension;
|
||||
const { name, description, version } = manifest;
|
||||
const isUninstalling = this.extensionStateStore.extensionState.get(id)?.state === "uninstalling";
|
||||
const isUninstalling = Extensions.installStates.get(id)?.state === "uninstalling";
|
||||
|
||||
return (
|
||||
<div key={id} className="extension flex gaps align-center">
|
||||
@@ -483,16 +490,15 @@ export class Extensions extends React.Component {
|
||||
* True if at least one extension is in installing state
|
||||
*/
|
||||
@computed get isInstalling() {
|
||||
return [...this.extensionStateStore.extensionState.values()].some(extension => extension.state === "installing");
|
||||
return [...Extensions.installStates.values()].some(extension => extension.state === "installing");
|
||||
}
|
||||
|
||||
render() {
|
||||
const topHeader = <h2>Manage Lens Extensions</h2>;
|
||||
const { installPath } = this;
|
||||
|
||||
return (
|
||||
<DropFileInput onDropFiles={this.installOnDrop}>
|
||||
<PageLayout showOnTop className="Extensions" header={topHeader} contentGaps={false}>
|
||||
<PageLayout showOnTop className="Extensions" contentGaps={false}>
|
||||
<h2>Lens Extensions</h2>
|
||||
<div>
|
||||
Add new features and functionality via Lens Extensions.
|
||||
@@ -542,7 +548,11 @@ export class Extensions extends React.Component {
|
||||
value={this.search}
|
||||
onChange={(value) => this.search = value}
|
||||
/>
|
||||
{extensionDiscovery.isLoaded ? this.renderExtensions() : <div className="spinner-wrapper"><Spinner/></div>}
|
||||
{
|
||||
ExtensionDiscovery.getInstance().isLoaded
|
||||
? this.renderExtensions()
|
||||
: <div className="spinner-wrapper"><Spinner/></div>
|
||||
}
|
||||
</div>
|
||||
</PageLayout>
|
||||
</DropFileInput>
|
||||
|
||||
@@ -14,8 +14,8 @@ import { IngressCharts } from "./ingress-charts";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Ingress> {
|
||||
}
|
||||
@@ -101,6 +101,7 @@ export class IngressDetails extends React.Component<Props> {
|
||||
if (!ingress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { spec, status } = ingress;
|
||||
const ingressPoints = status?.loadBalancer?.ingress;
|
||||
const { metrics } = ingressStore;
|
||||
@@ -108,8 +109,7 @@ export class IngressDetails extends React.Component<Props> {
|
||||
"Network",
|
||||
"Duration",
|
||||
];
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Ingress);
|
||||
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Ingress);
|
||||
const { serviceName, servicePort } = ingress.getServiceNamePort();
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChartOptions, ChartPoint } from "chart.js";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { mapValues } from "lodash";
|
||||
|
||||
type IContext = IResourceMetricsValue<Node, { metrics: IClusterMetrics }>;
|
||||
@@ -14,7 +14,7 @@ type IContext = IResourceMetricsValue<Node, { metrics: IClusterMetrics }>;
|
||||
export const NodeCharts = observer(() => {
|
||||
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
|
||||
const id = object.getId();
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
|
||||
|
||||
if (!metrics) {
|
||||
return null;
|
||||
|
||||
@@ -17,8 +17,8 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Node> {
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export class NodeDetails extends React.Component<Props> {
|
||||
"Disk",
|
||||
"Pods",
|
||||
];
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Node);
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Node);
|
||||
|
||||
return (
|
||||
<div className="NodeDetails">
|
||||
|
||||
@@ -13,7 +13,7 @@ import { systemName, isUrl, isPath } from "../input/input_validators";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { Icon } from "../icon";
|
||||
import { Notifications } from "../notifications";
|
||||
import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager";
|
||||
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
|
||||
|
||||
interface Props extends Partial<DialogProps> {
|
||||
onAddRepo: Function
|
||||
@@ -84,7 +84,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
||||
|
||||
async addCustomRepo() {
|
||||
try {
|
||||
await repoManager.addСustomRepo(this.helmRepo);
|
||||
await HelmRepoManager.getInstance().addСustomRepo(this.helmRepo);
|
||||
Notifications.ok(<>Helm repository <b>{this.helmRepo.name}</b> has added</>);
|
||||
this.props.onAddRepo();
|
||||
this.close();
|
||||
|
||||
@@ -3,7 +3,7 @@ import "./helm-charts.scss";
|
||||
import React from "react";
|
||||
import { action, computed, observable, makeObservable } from "mobx";
|
||||
|
||||
import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager";
|
||||
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { Notifications } from "../notifications";
|
||||
@@ -39,9 +39,9 @@ export class HelmCharts extends React.Component {
|
||||
|
||||
try {
|
||||
if (!this.repos.length) {
|
||||
this.repos = await repoManager.loadAvailableRepos(); // via https://helm.sh
|
||||
this.repos = await HelmRepoManager.getInstance().loadAvailableRepos(); // via https://helm.sh
|
||||
}
|
||||
const repos = await repoManager.repositories(); // via helm-cli
|
||||
const repos = await HelmRepoManager.getInstance().repositories(); // via helm-cli
|
||||
|
||||
this.addedRepos.clear();
|
||||
repos.forEach(repo => this.addedRepos.set(repo.name, repo));
|
||||
@@ -54,7 +54,7 @@ export class HelmCharts extends React.Component {
|
||||
|
||||
async addRepo(repo: HelmRepo) {
|
||||
try {
|
||||
await repoManager.addRepo(repo);
|
||||
await HelmRepoManager.getInstance().addRepo(repo);
|
||||
this.addedRepos.set(repo.name, repo);
|
||||
} catch (err) {
|
||||
Notifications.error(<>Adding helm branch <b>{repo.name}</b> has failed: {String(err)}</>);
|
||||
@@ -63,7 +63,7 @@ export class HelmCharts extends React.Component {
|
||||
|
||||
async removeRepo(repo: HelmRepo) {
|
||||
try {
|
||||
await repoManager.removeRepo(repo);
|
||||
await HelmRepoManager.getInstance().removeRepo(repo);
|
||||
this.addedRepos.delete(repo.name);
|
||||
} catch (err) {
|
||||
Notifications.error(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { Input, InputValidators } from "../input";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { UserPreferences, userStore } from "../../../common/user-store";
|
||||
import { getDefaultKubectlPath, UserPreferences } from "../../../common/user-store";
|
||||
import { observer } from "mobx-react";
|
||||
import { bundledKubectlPath } from "../../../main/kubectl";
|
||||
import { SelectOption, Select } from "../select";
|
||||
@@ -59,7 +59,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={downloadPath}
|
||||
placeholder={userStore.getDefaultKubectlPath()}
|
||||
placeholder={getDefaultKubectlPath()}
|
||||
validators={pathValidator}
|
||||
onChange={setDownloadPath}
|
||||
onBlur={save}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import "./preferences.scss";
|
||||
|
||||
import React from "react";
|
||||
import moment from "moment-timezone";
|
||||
import { computed, observable, reaction, makeObservable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
|
||||
import { userStore } from "../../../common/user-store";
|
||||
import { isWindows } from "../../../common/vars";
|
||||
import { appPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { Input } from "../input";
|
||||
import { PageLayout } from "../layout/page-layout";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
@@ -29,8 +30,8 @@ enum Pages {
|
||||
|
||||
@observer
|
||||
export class Preferences extends React.Component {
|
||||
@observable httpProxy = userStore.preferences.httpsProxy || "";
|
||||
@observable shell = userStore.preferences.shell || "";
|
||||
@observable httpProxy = UserStore.getInstance().preferences.httpsProxy || "";
|
||||
@observable shell = UserStore.getInstance().preferences.shell || "";
|
||||
@observable activeTab = Pages.Application;
|
||||
|
||||
constructor(props: object) {
|
||||
@@ -39,12 +40,17 @@ export class Preferences extends React.Component {
|
||||
}
|
||||
|
||||
@computed get themeOptions(): SelectOption<string>[] {
|
||||
return themeStore.themes.map(theme => ({
|
||||
return ThemeStore.getInstance().themes.map(theme => ({
|
||||
label: theme.name,
|
||||
value: theme.id,
|
||||
}));
|
||||
}
|
||||
|
||||
timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
|
||||
label: zone,
|
||||
value: zone,
|
||||
}));
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => navigation.location.hash, hash => {
|
||||
@@ -97,18 +103,16 @@ export class Preferences extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { preferences } = userStore;
|
||||
const extensions = appPreferenceRegistry.getItems();
|
||||
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry);
|
||||
let defaultShell = process.env.SHELL || process.env.PTYSHELL;
|
||||
|
||||
if (!defaultShell) {
|
||||
if (isWindows) {
|
||||
defaultShell = "powershell.exe";
|
||||
} else {
|
||||
defaultShell = "System default shell";
|
||||
}
|
||||
}
|
||||
const { preferences } = UserStore.getInstance();
|
||||
const defaultShell = process.env.SHELL
|
||||
|| process.env.PTYSHELL
|
||||
|| (
|
||||
isWindows
|
||||
? "powershell.exe"
|
||||
: "System default shell"
|
||||
);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
@@ -130,9 +134,9 @@ export class Preferences extends React.Component {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<hr className="small"/>
|
||||
<hr/>
|
||||
|
||||
<section id="shell" className="small">
|
||||
<section id="shell">
|
||||
<SubTitle title="Terminal Shell Path"/>
|
||||
<Input
|
||||
theme="round-black"
|
||||
@@ -158,6 +162,18 @@ export class Preferences extends React.Component {
|
||||
label="Automatically start Lens on login"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section id="locale">
|
||||
<SubTitle title="Locale Timezone" />
|
||||
<Select
|
||||
options={this.timezoneOptions}
|
||||
value={preferences.localeTimezone}
|
||||
onChange={({ value }: SelectOption) => UserStore.getInstance().setLocaleTimezone(value)}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
{this.activeTab == Pages.Proxy && (
|
||||
|
||||
@@ -14,8 +14,8 @@ import { VolumeClaimDiskChart } from "./volume-claim-disk-chart";
|
||||
import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-object";
|
||||
import { PersistentVolumeClaim } from "../../api/endpoints";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
|
||||
const metricTabs = [
|
||||
"Disk"
|
||||
];
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.VolumeClaim);
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.VolumeClaim);
|
||||
|
||||
return (
|
||||
<div className="PersistentVolumeClaimDetails">
|
||||
|
||||
@@ -5,13 +5,13 @@ import { BarChart, ChartDataSets, memoryOptions } from "../chart";
|
||||
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
|
||||
type IContext = IResourceMetricsValue<PersistentVolumeClaim, { metrics: IPvcMetrics }>;
|
||||
|
||||
export const VolumeClaimDiskChart = observer(() => {
|
||||
const { params: { metrics }, object } = useContext<IContext>(ResourceMetricsContext);
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
|
||||
const id = object.getId();
|
||||
|
||||
if (!metrics) return null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { userStore } from "../../../common/user-store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { navigate } from "../../navigation";
|
||||
import { Button } from "../button";
|
||||
import marked from "marked";
|
||||
@@ -14,7 +14,7 @@ export class WhatsNew extends React.Component {
|
||||
|
||||
ok = () => {
|
||||
navigate("/");
|
||||
userStore.saveLastSeenAppVersion();
|
||||
UserStore.getInstance().saveLastSeenAppVersion();
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -18,8 +18,8 @@ import { reaction } from "mobx";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<DaemonSet> {
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class DaemonSetDetails extends React.Component<Props> {
|
||||
const nodeSelector = daemonSet.getNodeSelectors();
|
||||
const childPods = daemonSetStore.getChildPods(daemonSet);
|
||||
const metrics = daemonSetStore.metrics;
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.DaemonSet);
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.DaemonSet);
|
||||
|
||||
return (
|
||||
<div className="DaemonSetDetails">
|
||||
|
||||
@@ -19,8 +19,8 @@ import { reaction } from "mobx";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Deployment> {
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
const selectors = deployment.getSelectors();
|
||||
const childPods = deploymentStore.getChildPods(deployment);
|
||||
const metrics = deploymentStore.metrics;
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Deployment);
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Deployment);
|
||||
|
||||
return (
|
||||
<div className="DeploymentDetails">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { observer } from "mobx-react";
|
||||
import { PieChart } from "../chart";
|
||||
import { cssVar } from "../../utils";
|
||||
import { ChartData, ChartDataSets } from "chart.js";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
|
||||
interface SimpleChartDataSets extends ChartDataSets {
|
||||
backgroundColor?: string[];
|
||||
@@ -46,7 +46,7 @@ export class OverviewWorkloadStatus extends React.Component<Props> {
|
||||
labels: [] as string[],
|
||||
datasets: [{
|
||||
data: [1],
|
||||
backgroundColor: [themeStore.activeTheme.colors.pieChartDefaultColor],
|
||||
backgroundColor: [ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor],
|
||||
label: "Empty"
|
||||
}]
|
||||
};
|
||||
|
||||
@@ -5,13 +5,13 @@ import { BarChart, cpuOptions, memoryOptions } from "../chart";
|
||||
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
|
||||
type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>;
|
||||
|
||||
export const ContainerCharts = observer(() => {
|
||||
const { params: { metrics }, tabId } = useContext<IContext>(ResourceMetricsContext);
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
|
||||
|
||||
if (!metrics) return null;
|
||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.ap
|
||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
|
||||
import { WorkloadKubeObject } from "../../api/workload-kube-object";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
|
||||
export const podMetricTabs = [
|
||||
"CPU",
|
||||
@@ -19,7 +19,7 @@ type IContext = IResourceMetricsValue<WorkloadKubeObject, { metrics: IPodMetrics
|
||||
|
||||
export const PodCharts = observer(() => {
|
||||
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
|
||||
const { chartCapacityColor } = themeStore.activeTheme.colors;
|
||||
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
|
||||
const id = object.getId();
|
||||
|
||||
if (!metrics) return null;
|
||||
|
||||
@@ -11,8 +11,9 @@ import { PodContainerPort } from "./pod-container-port";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import { IMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { ContainerCharts } from "./container-charts";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props {
|
||||
pod: Pod;
|
||||
@@ -39,8 +40,8 @@ export class PodDetailsContainer extends React.Component<Props> {
|
||||
<span>
|
||||
{lastState}<br/>
|
||||
Reason: {status.lastState.terminated.reason} - exit code: {status.lastState.terminated.exitCode}<br/>
|
||||
Started at: {status.lastState.terminated.startedAt}<br/>
|
||||
Finished at: {status.lastState.terminated.finishedAt}<br/>
|
||||
Started at: {<LocaleDate date={status.lastState.terminated.startedAt} />}<br/>
|
||||
Finished at: {<LocaleDate date={status.lastState.terminated.finishedAt} />}<br/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -65,7 +66,7 @@ export class PodDetailsContainer extends React.Component<Props> {
|
||||
"Memory",
|
||||
"Filesystem",
|
||||
];
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Container);
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Container);
|
||||
|
||||
return (
|
||||
<div className="PodDetailsContainer">
|
||||
|
||||
@@ -22,8 +22,8 @@ import { getItemMetrics } from "../../api/endpoints/metrics.api";
|
||||
import { PodCharts, podMetricTabs } from "./pod-charts";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Pod> {
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export class PodDetails extends React.Component<Props> {
|
||||
const nodeSelector = pod.getNodeSelectors();
|
||||
const volumes = pod.getVolumes();
|
||||
const metrics = podsStore.metrics;
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Pod);
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Pod);
|
||||
|
||||
return (
|
||||
<div className="PodDetails">
|
||||
|
||||
@@ -17,8 +17,8 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ReplicaSet> {
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
|
||||
const nodeSelector = replicaSet.getNodeSelectors();
|
||||
const images = replicaSet.getImages();
|
||||
const childPods = replicaSetStore.getChildPods(replicaSet);
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.ReplicaSet);
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.ReplicaSet);
|
||||
|
||||
return (
|
||||
<div className="ReplicaSetDetails">
|
||||
|
||||
@@ -18,8 +18,8 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<StatefulSet> {
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export class StatefulSetDetails extends React.Component<Props> {
|
||||
const nodeSelector = statefulSet.getNodeSelectors();
|
||||
const childPods = statefulSetStore.getChildPods(statefulSet);
|
||||
const metrics = statefulSetStore.metrics;
|
||||
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.StatefulSet);
|
||||
const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.StatefulSet);
|
||||
|
||||
return (
|
||||
<div className="StatefulSetDetails">
|
||||
|
||||
@@ -34,7 +34,7 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store
|
||||
import logger from "../../main/logger";
|
||||
import { webFrame } from "electron";
|
||||
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry";
|
||||
import { extensionLoader } from "../../extensions/extension-loader";
|
||||
import { ExtensionLoader } from "../../extensions/extension-loader";
|
||||
import { appEventBus } from "../../common/event-bus";
|
||||
import { requestMain } from "../../common/ipc";
|
||||
import whatInput from "what-input";
|
||||
@@ -67,7 +67,7 @@ export class App extends React.Component {
|
||||
|
||||
await requestMain(clusterSetFrameIdHandler, clusterId);
|
||||
await getHostedCluster().whenReady; // cluster.activate() is done at this point
|
||||
extensionLoader.loadOnClusterRenderer();
|
||||
ExtensionLoader.getInstance().loadOnClusterRenderer();
|
||||
setTimeout(() => {
|
||||
appEventBus.emit({
|
||||
name: "cluster",
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
overflow: hidden; // required for transition effect on hover
|
||||
color: white;
|
||||
font-family: var(--font-main);
|
||||
font-weight: var(--font-weight-bold);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } fro
|
||||
import { Chart, ChartKind, ChartProps } from "./chart";
|
||||
import { bytesToUnits, cssNames } from "../../utils";
|
||||
import { ZebraStripes } from "./zebra-stripes.plugin";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||
|
||||
interface Props extends ChartProps {
|
||||
@@ -26,7 +26,7 @@ export class BarChart extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { name, data, className, timeLabelStep, plugins, options: customOptions, ...settings } = this.props;
|
||||
const { textColorPrimary, borderFaintColor, chartStripesColor } = themeStore.activeTheme.colors;
|
||||
const { textColorPrimary, borderFaintColor, chartStripesColor } = ThemeStore.getInstance().activeTheme.colors;
|
||||
|
||||
const getBarColor: Scriptable<string> = ({ dataset }) => {
|
||||
const color = dataset.borderColor;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||
import ChartJS, { ChartOptions } from "chart.js";
|
||||
import { Chart, ChartProps } from "./chart";
|
||||
import { cssNames } from "../../utils";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
|
||||
interface Props extends ChartProps {
|
||||
}
|
||||
@@ -13,7 +13,7 @@ interface Props extends ChartProps {
|
||||
export class PieChart extends React.Component<Props> {
|
||||
render() {
|
||||
const { data, className, options, ...chartProps } = this.props;
|
||||
const { contentColor } = themeStore.activeTheme.colors;
|
||||
const { contentColor } = ThemeStore.getInstance().activeTheme.colors;
|
||||
const cutouts = [88, 76, 63];
|
||||
const opts: ChartOptions = this.props.showChart === false ? {} : {
|
||||
maintainAspectRatio: false,
|
||||
|
||||
@@ -7,7 +7,6 @@ jest.mock("../../../extensions/registries");
|
||||
import { statusBarRegistry } from "../../../extensions/registries";
|
||||
|
||||
describe("<BottomBar />", () => {
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = render(<BottomBar />);
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from "react";
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
import { clusterSettingsURL } from "../+cluster-settings";
|
||||
import { catalogURL } from "../+catalog";
|
||||
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { broadcastMessage, requestMain } from "../../../common/ipc";
|
||||
import { clusterDisconnectHandler } from "../../../common/cluster-ipc";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { Tooltip } from "../../components//tooltip";
|
||||
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
||||
|
||||
const navigate = (route: string) =>
|
||||
broadcastMessage(IpcRendererNavigationEvents.NAVIGATE_IN_APP, route);
|
||||
|
||||
/**
|
||||
* Creates handlers for high-level actions
|
||||
* that could be performed on an individual cluster
|
||||
* @param cluster Cluster
|
||||
*/
|
||||
export const ClusterActions = (cluster: Cluster) => ({
|
||||
showSettings: () => navigate(clusterSettingsURL({
|
||||
params: { clusterId: cluster.id }
|
||||
})),
|
||||
disconnect: async () => {
|
||||
clusterStore.deactivate(cluster.id);
|
||||
navigate(catalogURL());
|
||||
await requestMain(clusterDisconnectHandler, cluster.id);
|
||||
},
|
||||
remove: () => {
|
||||
const tooltipId = uniqueId("tooltip_target_");
|
||||
|
||||
return ConfirmDialog.open({
|
||||
okButtonProps: {
|
||||
primary: false,
|
||||
accent: true,
|
||||
label: "Remove"
|
||||
},
|
||||
ok: () => {
|
||||
clusterStore.deactivate(cluster.id);
|
||||
clusterStore.removeById(cluster.id);
|
||||
navigate(catalogURL());
|
||||
},
|
||||
message: <p>
|
||||
Are you sure want to remove cluster <b id={tooltipId}>{cluster.name}</b>?
|
||||
<Tooltip targetId={tooltipId}>{cluster.id}</Tooltip>
|
||||
</p>
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -9,19 +9,19 @@ import { Catalog, catalogRoute, catalogURL } from "../+catalog";
|
||||
import { Preferences, preferencesRoute } from "../+preferences";
|
||||
import { AddCluster, addClusterRoute } from "../+add-cluster";
|
||||
import { ClusterView } from "./cluster-view";
|
||||
import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings";
|
||||
import { clusterViewRoute } from "./cluster-view.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
|
||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||
import { Extensions, extensionsRoute } from "../+extensions";
|
||||
import { getMatchedClusterId } from "../../navigation";
|
||||
import { HotbarMenu } from "../hotbar/hotbar-menu";
|
||||
import { EntitySettings, entitySettingsRoute } from "../+entity-settings";
|
||||
|
||||
@observer
|
||||
export class ClusterManager extends React.Component {
|
||||
componentDidMount() {
|
||||
const getMatchedCluster = () => clusterStore.getById(getMatchedClusterId());
|
||||
const getMatchedCluster = () => ClusterStore.getInstance().getById(getMatchedClusterId());
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
reaction(getMatchedClusterId, initView, {
|
||||
@@ -58,10 +58,13 @@ export class ClusterManager extends React.Component {
|
||||
<Route component={Extensions} {...extensionsRoute} />
|
||||
<Route component={AddCluster} {...addClusterRoute} />
|
||||
<Route component={ClusterView} {...clusterViewRoute} />
|
||||
<Route component={ClusterSettings} {...clusterSettingsRoute} />
|
||||
{globalPageRegistry.getItems().map(({ url, components: { Page } }) => {
|
||||
return <Route key={url} path={url} component={Page}/>;
|
||||
})}
|
||||
<Route component={EntitySettings} {...entitySettingsRoute} />
|
||||
{
|
||||
globalPageRegistry.getItems()
|
||||
.map(({ url, components: { Page } }) => (
|
||||
<Route key={url} path={url} component={Page} />
|
||||
))
|
||||
}
|
||||
<Redirect exact to={this.startUrl}/>
|
||||
</Switch>
|
||||
</main>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Icon } from "../icon";
|
||||
import { Button } from "../button";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterId, ClusterStore } from "../../../common/cluster-store";
|
||||
import { CubeSpinner } from "../spinner";
|
||||
import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ClusterStatus extends React.Component<Props> {
|
||||
}
|
||||
|
||||
get cluster(): Cluster {
|
||||
return clusterStore.getById(this.props.clusterId);
|
||||
return ClusterStore.getInstance().getById(this.props.clusterId);
|
||||
}
|
||||
|
||||
@computed get hasErrors(): boolean {
|
||||
|
||||
@@ -7,9 +7,9 @@ import { IClusterViewRouteParams } from "./cluster-view.route";
|
||||
import { ClusterStatus } from "./cluster-status";
|
||||
import { hasLoadedView } from "./lens-views";
|
||||
import { Cluster } from "../../../main/cluster";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { navigate } from "../../navigation";
|
||||
import { catalogURL } from "../+catalog";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
||||
}
|
||||
@@ -21,12 +21,12 @@ export class ClusterView extends React.Component<Props> {
|
||||
}
|
||||
|
||||
get cluster(): Cluster {
|
||||
return clusterStore.getById(this.clusterId);
|
||||
return ClusterStore.getInstance().getById(this.clusterId);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
|
||||
reaction(() => this.clusterId, clusterId => ClusterStore.getInstance().setActive(clusterId), {
|
||||
fireImmediately: true,
|
||||
}),
|
||||
reaction(() => this.cluster.online, (online) => {
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./cluster-manager";
|
||||
export * from "./cluster-actions";
|
||||
@@ -1,5 +1,5 @@
|
||||
import { observable, when } from "mobx";
|
||||
import { ClusterId, clusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
||||
import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store";
|
||||
import { getMatchedClusterId } from "../../navigation";
|
||||
import logger from "../../../main/logger";
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function initView(clusterId: ClusterId) {
|
||||
if (!clusterId || lensViews.has(clusterId)) {
|
||||
return;
|
||||
}
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (!cluster) {
|
||||
return;
|
||||
@@ -44,13 +44,9 @@ export async function initView(clusterId: ClusterId) {
|
||||
|
||||
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
|
||||
await when(() => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (!cluster) return true;
|
||||
|
||||
const view = lensViews.get(clusterId);
|
||||
|
||||
return cluster.disconnected && view?.isLoaded;
|
||||
return !cluster || (cluster.disconnected && lensViews.get(clusterId)?.isLoaded);
|
||||
});
|
||||
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
|
||||
lensViews.delete(clusterId);
|
||||
@@ -64,7 +60,7 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
|
||||
}
|
||||
|
||||
export function refreshViews() {
|
||||
const cluster = clusterStore.getById(getMatchedClusterId());
|
||||
const cluster = ClusterStore.getInstance().getById(getMatchedClusterId());
|
||||
|
||||
lensViews.forEach(({ clusterId, view, isLoaded }) => {
|
||||
const isCurrent = clusterId === cluster?.id;
|
||||
|
||||
+4
-4
@@ -1,15 +1,15 @@
|
||||
import { navigate } from "../../navigation";
|
||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||
import { clusterSettingsURL } from "./cluster-settings.route";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { entitySettingsURL } from "../+entity-settings";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
|
||||
commandRegistry.add({
|
||||
id: "cluster.viewCurrentClusterSettings",
|
||||
title: "Cluster: View Settings",
|
||||
scope: "global",
|
||||
action: () => navigate(clusterSettingsURL({
|
||||
action: () => navigate(entitySettingsURL({
|
||||
params: {
|
||||
clusterId: clusterStore.active.id
|
||||
entityId: ClusterStore.getInstance().active.id
|
||||
}
|
||||
})),
|
||||
isActive: (context) => !!context.entity
|
||||
@@ -0,0 +1,141 @@
|
||||
import React from "react";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
|
||||
import { ClusterNameSetting } from "./components/cluster-name-setting";
|
||||
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
|
||||
import { ClusterAccessibleNamespaces } from "./components/cluster-accessible-namespaces";
|
||||
import { ClusterMetricsSetting } from "./components/cluster-metrics-setting";
|
||||
import { ShowMetricsSetting } from "./components/show-metrics";
|
||||
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
|
||||
import { ClusterKubeconfig } from "./components/cluster-kubeconfig";
|
||||
import { entitySettingRegistry } from "../../../extensions/registries";
|
||||
import { CatalogEntity } from "../../api/catalog-entity";
|
||||
|
||||
|
||||
function getClusterForEntity(entity: CatalogEntity) {
|
||||
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid);
|
||||
|
||||
if (!cluster?.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
entitySettingRegistry.add([
|
||||
{
|
||||
apiVersions: ["entity.k8slens.dev/v1alpha1"],
|
||||
kind: "KubernetesCluster",
|
||||
source: "local",
|
||||
title: "General",
|
||||
components: {
|
||||
View: (props: { entity: CatalogEntity }) => {
|
||||
const cluster = getClusterForEntity(props.entity);
|
||||
|
||||
if (!cluster) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<section>
|
||||
<ClusterNameSetting cluster={cluster} />
|
||||
</section>
|
||||
<section>
|
||||
<ClusterKubeconfig cluster={cluster} />
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
apiVersions: ["entity.k8slens.dev/v1alpha1"],
|
||||
kind: "KubernetesCluster",
|
||||
title: "Proxy",
|
||||
components: {
|
||||
View: (props: { entity: CatalogEntity }) => {
|
||||
const cluster = getClusterForEntity(props.entity);
|
||||
|
||||
if (!cluster) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<ClusterProxySetting cluster={cluster} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
apiVersions: ["entity.k8slens.dev/v1alpha1"],
|
||||
kind: "KubernetesCluster",
|
||||
title: "Terminal",
|
||||
components: {
|
||||
View: (props: { entity: CatalogEntity }) => {
|
||||
const cluster = getClusterForEntity(props.entity);
|
||||
|
||||
if (!cluster) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<ClusterHomeDirSetting cluster={cluster} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
apiVersions: ["entity.k8slens.dev/v1alpha1"],
|
||||
kind: "KubernetesCluster",
|
||||
title: "Namespaces",
|
||||
components: {
|
||||
View: (props: { entity: CatalogEntity }) => {
|
||||
const cluster = getClusterForEntity(props.entity);
|
||||
|
||||
if (!cluster) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<ClusterAccessibleNamespaces cluster={cluster} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
apiVersions: ["entity.k8slens.dev/v1alpha1"],
|
||||
kind: "KubernetesCluster",
|
||||
source: "local",
|
||||
title: "Metrics",
|
||||
components: {
|
||||
View: (props: { entity: CatalogEntity }) => {
|
||||
const cluster = getClusterForEntity(props.entity);
|
||||
|
||||
if (!cluster) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<section>
|
||||
<ClusterPrometheusSetting cluster={cluster} />
|
||||
</section>
|
||||
<section>
|
||||
<ClusterMetricsSetting cluster={cluster}/>
|
||||
</section>
|
||||
<section>
|
||||
<ShowMetricsSetting cluster={cluster}/>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
+3
-1
@@ -22,7 +22,6 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Accessible Namespaces" id="accessible-namespaces" />
|
||||
<p>This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.</p>
|
||||
<EditableList
|
||||
placeholder="Add new namespace..."
|
||||
add={(newNamespace) => {
|
||||
@@ -35,6 +34,9 @@ export class ClusterAccessibleNamespaces extends React.Component<Props> {
|
||||
this.props.cluster.accessibleNamespaces = Array.from(this.namespaces);
|
||||
}}
|
||||
/>
|
||||
<small className="hint">
|
||||
This setting is useful for manually specifying which namespaces you have access to. This is useful when you do not have permissions to list namespaces.
|
||||
</small>
|
||||
</>
|
||||
);
|
||||
}
|
||||
-1
@@ -38,7 +38,6 @@ export class ClusterHomeDirSetting extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Working Directory"/>
|
||||
<p>Terminal working directory.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={this.directory}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { observer } from "mobx-react";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { autobind } from "../../../../common/utils";
|
||||
import { shell } from "electron";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ClusterKubeconfig extends React.Component<Props> {
|
||||
|
||||
@autobind()
|
||||
openKubeconfig() {
|
||||
const { cluster } = this.props;
|
||||
|
||||
shell.showItemInFolder(cluster.kubeConfigPath);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Kubeconfig" />
|
||||
|
||||
<span>
|
||||
<a className="link value" onClick={this.openKubeconfig}>{this.props.cluster.kubeConfigPath}</a>
|
||||
</span>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
-1
@@ -39,7 +39,6 @@ export class ClusterNameSetting extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Cluster Name" />
|
||||
<p>Define cluster name.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
validators={isRequired}
|
||||
+1
-2
@@ -86,13 +86,12 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="Prometheus"/>
|
||||
<SubTitle title="Prometheus installation method"/>
|
||||
<p>
|
||||
Use pre-installed Prometheus service for metrics. Please refer to the{" "}
|
||||
<a href="https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md" target="_blank" rel="noreferrer">guide</a>{" "}
|
||||
for possible configuration changes.
|
||||
</p>
|
||||
<p>Prometheus installation method.</p>
|
||||
<Select
|
||||
value={this.provider}
|
||||
onChange={({value}) => {
|
||||
+3
-1
@@ -38,7 +38,6 @@ export class ClusterProxySetting extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
<SubTitle title="HTTP Proxy" />
|
||||
<p>HTTP Proxy server. Used for communicating with Kubernetes API.</p>
|
||||
<Input
|
||||
theme="round-black"
|
||||
value={this.proxy}
|
||||
@@ -47,6 +46,9 @@ export class ClusterProxySetting extends React.Component<Props> {
|
||||
placeholder="http://<address>:<port>"
|
||||
validators={this.proxy ? InputValidators.isUrl : undefined}
|
||||
/>
|
||||
<small className="hint">
|
||||
HTTP Proxy server. Used for communicating with Kubernetes API.
|
||||
</small>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { clusterStore } from "../../../../common/cluster-store";
|
||||
import { ClusterStore } from "../../../../common/cluster-store";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { autobind } from "../../../utils";
|
||||
import { Button } from "../../button";
|
||||
@@ -27,7 +27,7 @@ export class RemoveClusterButton extends React.Component<Props> {
|
||||
labelOk: "Yes",
|
||||
labelCancel: "No",
|
||||
ok: async () => {
|
||||
await clusterStore.removeById(cluster.id);
|
||||
await ClusterStore.getInstance().removeById(cluster.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
File renamed without changes.
-1
@@ -1,3 +1,2 @@
|
||||
export * from "./cluster-settings.route";
|
||||
export * from "./cluster-settings";
|
||||
export * from "./cluster-settings.command";
|
||||
@@ -4,7 +4,7 @@ import { computed, observable, toJS, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { commandRegistry } from "../../../extensions/registries/command-registry";
|
||||
import { clusterStore } from "../../../common/cluster-store";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { CommandOverlay } from "./command-container";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import { navigate } from "../../navigation";
|
||||
@@ -25,7 +25,7 @@ export class CommandDialog extends React.Component {
|
||||
};
|
||||
|
||||
return commandRegistry.getItems().filter((command) => {
|
||||
if (command.scope === "entity" && !clusterStore.active) {
|
||||
if (command.scope === "entity" && !ClusterStore.getInstance().active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { namespaceStore } from "./+namespaces/namespace.store";
|
||||
|
||||
export interface ClusterContext {
|
||||
cluster?: Cluster;
|
||||
allNamespaces?: string[]; // available / allowed namespaces from cluster.ts
|
||||
contextNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
|
||||
allNamespaces: string[]; // available / allowed namespaces from cluster.ts
|
||||
contextNamespaces: string[]; // selected by user (see: namespace-select.tsx)
|
||||
}
|
||||
|
||||
export const clusterContext: ClusterContext = {
|
||||
|
||||
@@ -16,6 +16,20 @@ const getComponent = () => (
|
||||
/>
|
||||
);
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // Deprecated
|
||||
removeListener: jest.fn(), // Deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
const renderTabs = () => render(getComponent());
|
||||
|
||||
const getTabKinds = () => dockStore.tabs.map(tab => tab.kind);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Pod } from "../../../api/endpoints";
|
||||
import { LogResourceSelector } from "../log-resource-selector";
|
||||
import { LogTabData } from "../log-tab.store";
|
||||
import { dockerPod, deploymentPod1 } from "./pod.mock";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
|
||||
const getComponent = (tabData: LogTabData) => {
|
||||
return (
|
||||
@@ -41,6 +43,16 @@ const getFewPodsTabData = (): LogTabData => {
|
||||
};
|
||||
|
||||
describe("<LogResourceSelector />", () => {
|
||||
beforeEach(() => {
|
||||
UserStore.getInstanceOrCreate();
|
||||
ThemeStore.getInstanceOrCreate();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
UserStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const tabData = getOnePodTabData();
|
||||
const { container } = render(getComponent(tabData));
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
.CreateResource {
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,59 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import filehound from "filehound";
|
||||
import { watch } from "chokidar";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||
|
||||
export class CreateResourceStore extends DockTabStore<string> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
storageKey: "create_resource"
|
||||
});
|
||||
fs.ensureDirSync(this.userTemplatesFolder);
|
||||
}
|
||||
|
||||
get lensTemplatesFolder():string {
|
||||
return path.resolve(__static, "../templates/create-resource");
|
||||
}
|
||||
|
||||
get userTemplatesFolder():string {
|
||||
return path.join(os.homedir(), ".k8slens", "templates");
|
||||
}
|
||||
|
||||
async getTemplates(templatesPath: string, defaultGroup: string) {
|
||||
const templates = await filehound.create().path(templatesPath).ext(["yaml", "json"]).depth(1).find();
|
||||
|
||||
return templates ? this.groupTemplates(templates, templatesPath, defaultGroup) : {};
|
||||
}
|
||||
|
||||
groupTemplates(templates: string[], templatesPath: string, defaultGroup: string) {
|
||||
return groupBy(templates,(v:string) =>
|
||||
path.relative(templatesPath,v).split(path.sep).length>1
|
||||
? path.parse(path.relative(templatesPath,v)).dir
|
||||
: defaultGroup);
|
||||
}
|
||||
|
||||
async getMergedTemplates() {
|
||||
const userTemplates = await this.getTemplates(this.userTemplatesFolder, "ungrouped");
|
||||
const lensTemplates = await this.getTemplates(this.lensTemplatesFolder, "lens");
|
||||
|
||||
return {...userTemplates,...lensTemplates};
|
||||
}
|
||||
|
||||
async watchUserTemplates(callback: ()=> void){
|
||||
watch(this.userTemplatesFolder, {
|
||||
depth: 1,
|
||||
ignoreInitial: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 500
|
||||
}
|
||||
}).on("all", () => {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import "./create-resource.scss";
|
||||
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import {Select, GroupSelectOption, SelectOption} from "../select";
|
||||
import jsYaml from "js-yaml";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
@@ -20,7 +23,25 @@ interface Props {
|
||||
|
||||
@observer
|
||||
export class CreateResource extends React.Component<Props> {
|
||||
@observable currentTemplates:Map<string,SelectOption> = new Map();
|
||||
@observable error = "";
|
||||
@observable templates:GroupSelectOption<SelectOption>[] = [];
|
||||
|
||||
componentDidMount() {
|
||||
createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v));
|
||||
createResourceStore.watchUserTemplates(() => createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)));
|
||||
}
|
||||
|
||||
updateGroupSelectOptions(templates :Record<string, string[]>) {
|
||||
this.templates = Object.entries(templates)
|
||||
.map(([name, grouping]) => this.convertToGroup(name, grouping));
|
||||
}
|
||||
|
||||
convertToGroup(group:string, items:string[]):GroupSelectOption {
|
||||
const options = items.map(v => ({label: path.parse(v).name, value: v}));
|
||||
|
||||
return {label: group, options};
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -35,11 +56,20 @@ export class CreateResource extends React.Component<Props> {
|
||||
return createResourceStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get currentTemplate() {
|
||||
return this.currentTemplates.get(this.tabId) ?? null;
|
||||
}
|
||||
|
||||
onChange = (value: string, error?: string) => {
|
||||
createResourceStore.setData(this.tabId, value);
|
||||
this.error = error;
|
||||
};
|
||||
|
||||
onSelectTemplate = (item: SelectOption) => {
|
||||
this.currentTemplates.set(this.tabId, item);
|
||||
fs.readFile(item.value,"utf8").then(v => createResourceStore.setData(this.tabId,v));
|
||||
};
|
||||
|
||||
create = async () => {
|
||||
if (this.error) return;
|
||||
if (!this.data.trim()) return; // do not save when field is empty
|
||||
@@ -72,6 +102,24 @@ export class CreateResource extends React.Component<Props> {
|
||||
return successMessage;
|
||||
};
|
||||
|
||||
renderControls(){
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<Select
|
||||
autoConvertOptions = {false}
|
||||
className="TemplateSelect"
|
||||
placeholder="Select Template ..."
|
||||
options={this.templates}
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
onChange={v => this.onSelectTemplate(v)}
|
||||
value = {this.currentTemplate}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { tabId, data, error, create, onChange } = this;
|
||||
const { className } = this.props;
|
||||
@@ -81,6 +129,7 @@ export class CreateResource extends React.Component<Props> {
|
||||
<InfoPanel
|
||||
tabId={tabId}
|
||||
error={error}
|
||||
controls={this.renderControls()}
|
||||
submit={create}
|
||||
submitLabel="Create"
|
||||
showNotifications={false}
|
||||
|
||||
@@ -6,9 +6,11 @@ import DOMPurify from "dompurify";
|
||||
import debounce from "lodash/debounce";
|
||||
import { action, computed, observable, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import moment from "moment-timezone";
|
||||
import { Align, ListOnScrollProps } from "react-window";
|
||||
|
||||
import { SearchStore, searchStore } from "../../../common/search-store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { cssNames } from "../../utils";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
@@ -84,12 +86,15 @@ export class LogList extends React.Component<Props> {
|
||||
@computed
|
||||
get logs() {
|
||||
const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
|
||||
const { preferences } = UserStore.getInstance();
|
||||
|
||||
if (!showTimestamps) {
|
||||
return logStore.logsWithoutTimestamps;
|
||||
}
|
||||
|
||||
return this.props.logs;
|
||||
return this.props.logs
|
||||
.map(log => logStore.splitOutTimestamp(log))
|
||||
.map(([logTimestamp, log]) => (`${moment.tz(logTimestamp, preferences.localeTimezone).format()}${log}`));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -144,6 +144,16 @@ export class LogStore {
|
||||
return stamp.toISOString();
|
||||
}
|
||||
|
||||
splitOutTimestamp(logs: string): [string, string] {
|
||||
const extraction = /^(\d+\S+)(.*)/m.exec(logs);
|
||||
|
||||
if (!extraction || extraction.length < 3) {
|
||||
return ["", ""];
|
||||
}
|
||||
|
||||
return [extraction[1], extraction[2]];
|
||||
}
|
||||
|
||||
getTimestamps(logs: string) {
|
||||
return logs.match(/^\d+\S+/gm);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cssNames } from "../../utils";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { Terminal } from "./terminal";
|
||||
import { terminalStore } from "./terminal.store";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -38,7 +38,7 @@ export class TerminalWindow extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cssNames("TerminalWindow", className, themeStore.activeTheme.type)}
|
||||
className={cssNames("TerminalWindow", className, ThemeStore.getInstance().activeTheme.type)}
|
||||
ref={e => this.elem = e}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,10 @@ import { Terminal as XTerm } from "xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import { dockStore, TabId } from "./dock.store";
|
||||
import { TerminalApi } from "../../api/terminal-api";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { isMac } from "../../../common/vars";
|
||||
import { camelCase } from "lodash";
|
||||
|
||||
export class Terminal {
|
||||
static spawningPool: HTMLElement;
|
||||
@@ -40,16 +41,10 @@ export class Terminal {
|
||||
// Replacing keys stored in styles to format accepted by terminal
|
||||
// E.g. terminalBrightBlack -> brightBlack
|
||||
const colorPrefix = "terminal";
|
||||
const terminalColors = Object.entries(colors)
|
||||
const terminalColorEntries = Object.entries(colors)
|
||||
.filter(([name]) => name.startsWith(colorPrefix))
|
||||
.reduce<any>((colors, [name, color]) => {
|
||||
const colorName = name.split("").slice(colorPrefix.length);
|
||||
|
||||
colorName[0] = colorName[0].toLowerCase();
|
||||
colors[colorName.join("")] = color;
|
||||
|
||||
return colors;
|
||||
}, {});
|
||||
.map(([name, color]) => [camelCase(name.slice(colorPrefix.length)), color]);
|
||||
const terminalColors = Object.fromEntries(terminalColorEntries);
|
||||
|
||||
this.xterm.setOption("theme", terminalColors);
|
||||
}
|
||||
@@ -109,7 +104,7 @@ export class Terminal {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
|
||||
this.disposers.push(
|
||||
reaction(() => toJS(themeStore.activeTheme.colors), this.setTheme, {
|
||||
reaction(() => toJS(ThemeStore.getInstance().activeTheme.colors), this.setTheme, {
|
||||
fireImmediately: true
|
||||
}),
|
||||
dockStore.onResize(this.onResize),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { action, autorun, IReactionDisposer, reaction, makeObservable } from "mobx";
|
||||
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { HelmRelease, helmReleasesApi } from "../../api/endpoints/helm-releases.api";
|
||||
import { getReleaseValues, HelmRelease } from "../../api/endpoints/helm-releases.api";
|
||||
import { releaseStore } from "../+apps-releases/release.store";
|
||||
|
||||
export interface IChartUpgradeData {
|
||||
@@ -91,7 +91,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
async loadValues(tabId: TabId) {
|
||||
this.values.clearData(tabId); // reset
|
||||
const { releaseName, releaseNamespace } = this.getData(tabId);
|
||||
const values = await helmReleasesApi.getValues(releaseName, releaseNamespace);
|
||||
const values = await getReleaseValues(releaseName, releaseNamespace, true);
|
||||
|
||||
this.values.setData(tabId, values);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Menu, MenuItem } from "../menu";
|
||||
import { Icon } from "../icon";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { navigate } from "../../navigation";
|
||||
import { hotbarStore } from "../../../common/hotbar-store";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
|
||||
interface Props extends DOMAttributes<HTMLElement> {
|
||||
@@ -65,7 +65,7 @@ export class HotbarIcon extends React.Component<Props> {
|
||||
}
|
||||
|
||||
removeFromHotbar(item: CatalogEntity) {
|
||||
const hotbar = hotbarStore.getByName("default"); // FIXME
|
||||
const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME
|
||||
|
||||
if (!hotbar) {
|
||||
return;
|
||||
@@ -105,6 +105,7 @@ export class HotbarIcon extends React.Component<Props> {
|
||||
await entity.onContextMenuOpen(this.contextMenu);
|
||||
this.toggleMenu();
|
||||
};
|
||||
const menuItems = this.contextMenu?.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === entity.metadata.source);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@@ -122,7 +123,7 @@ export class HotbarIcon extends React.Component<Props> {
|
||||
<MenuItem key="remove-from-hotbar" onClick={() => this.removeFromHotbar(entity) }>
|
||||
<Icon material="clear" small interactive={true} title="Remove from hotbar"/> Remove from Hotbar
|
||||
</MenuItem>
|
||||
{ this.contextMenu && this.contextMenu.menuItems.map((menuItem) => {
|
||||
{ this.contextMenu && menuItems.map((menuItem) => {
|
||||
return (
|
||||
<MenuItem key={menuItem.title} onClick={() => this.onMenuItemClick(menuItem) }>
|
||||
<Icon material={menuItem.icon} small interactive={true} title={menuItem.title}/> {menuItem.title}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||
import { HotbarIcon } from "./hotbar-icon";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { hotbarStore } from "../../../common/hotbar-store";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { catalogEntityRunContext } from "../../api/catalog-entity";
|
||||
|
||||
interface Props {
|
||||
@@ -14,20 +14,24 @@ interface Props {
|
||||
|
||||
@observer
|
||||
export class HotbarMenu extends React.Component<Props> {
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const hotbar = hotbarStore.getByName("default"); // FIXME
|
||||
|
||||
get hotbarItems() {
|
||||
const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME
|
||||
|
||||
if (!hotbar) {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = hotbar.items.map((item) => catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid)).filter(Boolean);
|
||||
return hotbar.items.map((item) => catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid)).filter(Boolean);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
|
||||
return (
|
||||
<div className={cssNames("HotbarMenu flex column", className)}>
|
||||
<div className="items flex column gaps">
|
||||
{items.map((entity, index) => {
|
||||
{this.hotbarItems.map((entity, index) => {
|
||||
return (
|
||||
<HotbarIcon
|
||||
key={index}
|
||||
@@ -43,4 +47,3 @@ export class HotbarMenu extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +98,9 @@
|
||||
label {
|
||||
background: var(--inputControlBackground);
|
||||
border: 1px solid var(--inputControlBorder);
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
padding: $padding;
|
||||
color: var(--textColorTertiary);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--inputControlHoverBorder);
|
||||
|
||||
@@ -16,11 +16,11 @@ import { Filter, FilterType, pageFilters } from "./page-filters.store";
|
||||
import { PageFiltersList } from "./page-filters-list";
|
||||
import { PageFiltersSelect } from "./page-filters-select";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||
import { themeStore } from "../../theme.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { MenuActions } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { userStore } from "../../../common/user-store";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
|
||||
// todo: refactor, split to small re-usable components
|
||||
@@ -177,6 +177,10 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
return this.props.isReady ?? this.props.store.isLoaded;
|
||||
}
|
||||
|
||||
@computed get failedToLoad() {
|
||||
return this.props.store.failedLoading;
|
||||
}
|
||||
|
||||
@computed get filters() {
|
||||
let { activeFilters } = pageFilters;
|
||||
const { isSearchable, searchFilters } = this.props;
|
||||
@@ -286,6 +290,11 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
});
|
||||
}
|
||||
|
||||
@autobind()
|
||||
toggleFilters() {
|
||||
this.showFilters = !this.showFilters;
|
||||
}
|
||||
|
||||
renderFilters() {
|
||||
const { hideFilters } = this.props;
|
||||
const { isReady, filters } = this;
|
||||
@@ -298,6 +307,14 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
}
|
||||
|
||||
renderNoItems() {
|
||||
if (this.failedToLoad) {
|
||||
return <NoItems>Failed to load items.</NoItems>;
|
||||
}
|
||||
|
||||
if (!this.isReady) {
|
||||
return <Spinner center />;
|
||||
}
|
||||
|
||||
if (this.filters.length > 0) {
|
||||
return (
|
||||
<NoItems>
|
||||
@@ -314,6 +331,14 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
return <NoItems/>;
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
if (this.props.virtual) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.items.map(item => this.getRow(item.getId()));
|
||||
}
|
||||
|
||||
renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode {
|
||||
const { isSearchable, searchFilters } = this.props;
|
||||
const { title, filters, search, info } = placeholders;
|
||||
@@ -322,7 +347,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
<>
|
||||
{title}
|
||||
<div className="info-panel box grow">
|
||||
{this.isReady && info}
|
||||
{info}
|
||||
</div>
|
||||
{filters}
|
||||
{isSearchable && searchFilters && search}
|
||||
@@ -331,20 +356,17 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
}
|
||||
|
||||
renderInfo() {
|
||||
const { items, isReady, filters } = this;
|
||||
const { items, filters } = this;
|
||||
const allItemsCount = this.props.store.getTotalCount();
|
||||
const itemsCount = items.length;
|
||||
const isFiltered = isReady && filters.length > 0;
|
||||
|
||||
if (isFiltered) {
|
||||
const toggleFilters = () => this.showFilters = !this.showFilters;
|
||||
|
||||
if (filters.length > 0) {
|
||||
return (
|
||||
<><a onClick={toggleFilters}>Filtered</a>: {itemsCount} / {allItemsCount}</>
|
||||
<><a onClick={this.toggleFilters}>Filtered</a>: {itemsCount} / {allItemsCount}</>
|
||||
);
|
||||
}
|
||||
|
||||
return allItemsCount <= 1 ? `${allItemsCount} item` : `${allItemsCount} items`;
|
||||
return allItemsCount === 1 ? `${allItemsCount} item` : `${allItemsCount} items`;
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
@@ -417,40 +439,31 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
|
||||
renderList() {
|
||||
const {
|
||||
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem,
|
||||
tableProps = {}, tableId
|
||||
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
|
||||
detailsItem, className, tableProps = {}, tableId,
|
||||
} = this.props;
|
||||
const { isReady, removeItemsDialog, items } = this;
|
||||
const { removeItemsDialog, items } = this;
|
||||
const { selectedItems } = store;
|
||||
const selectedItemId = detailsItem && detailsItem.getId();
|
||||
const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type);
|
||||
|
||||
return (
|
||||
<div className="items box grow flex column">
|
||||
{!isReady && (
|
||||
<Spinner center/>
|
||||
)}
|
||||
{isReady && (
|
||||
<Table
|
||||
tableId={tableId}
|
||||
virtual={virtual}
|
||||
selectable={hasDetailsView}
|
||||
sortable={sortingCallbacks}
|
||||
getTableRow={this.getRow}
|
||||
items={items}
|
||||
selectedItemId={selectedItemId}
|
||||
noItems={this.renderNoItems()}
|
||||
{...({
|
||||
...tableProps,
|
||||
className: cssNames("box grow", tableProps.className, themeStore.activeTheme.type),
|
||||
})}
|
||||
>
|
||||
{this.renderTableHeader()}
|
||||
{
|
||||
!virtual && items.map(item => this.getRow(item.getId()))
|
||||
}
|
||||
</Table>
|
||||
|
||||
)}
|
||||
<Table
|
||||
tableId={tableId}
|
||||
virtual={virtual}
|
||||
selectable={hasDetailsView}
|
||||
sortable={sortingCallbacks}
|
||||
getTableRow={this.getRow}
|
||||
items={items}
|
||||
selectedItemId={selectedItemId}
|
||||
noItems={this.renderNoItems()}
|
||||
className={classNames}
|
||||
{...tableProps}
|
||||
>
|
||||
{this.renderTableHeader()}
|
||||
{this.renderItems()}
|
||||
</Table>
|
||||
<AddRemoveButtons
|
||||
onRemove={selectedItems.length ? removeItemsDialog : null}
|
||||
removeTooltip={`Remove selected items (${selectedItems.length})`}
|
||||
@@ -461,7 +474,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
}
|
||||
|
||||
@computed get hiddenColumns() {
|
||||
return userStore.getHiddenTableColumns(this.props.tableId);
|
||||
return UserStore.getInstance().getHiddenTableColumns(this.props.tableId);
|
||||
}
|
||||
|
||||
isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
|
||||
@@ -483,7 +496,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
||||
hiddenColumns.delete(columnId);
|
||||
}
|
||||
|
||||
userStore.setHiddenTableColumns(this.props.tableId, hiddenColumns);
|
||||
UserStore.getInstance().setHiddenTableColumns(this.props.tableId, hiddenColumns);
|
||||
}
|
||||
|
||||
renderColumnVisibilityMenu() {
|
||||
|
||||
@@ -15,8 +15,14 @@ export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
|
||||
dependentStores?: KubeObjectStore[];
|
||||
}
|
||||
|
||||
const defaultProps: Partial<KubeObjectListLayoutProps> = {
|
||||
onDetails: (item: KubeObject) => showDetails(item.selfLink),
|
||||
};
|
||||
|
||||
@observer
|
||||
export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutProps> {
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
constructor(props: KubeObjectListLayoutProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
@@ -38,14 +44,6 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
|
||||
]);
|
||||
}
|
||||
|
||||
onDetails = (item: KubeObject) => {
|
||||
if (this.props.onDetails) {
|
||||
this.props.onDetails(item);
|
||||
} else {
|
||||
showDetails(item.selfLink);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className, store, items = store.contextItems, ...layoutProps } = this.props;
|
||||
|
||||
@@ -57,10 +55,7 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
|
||||
items={items}
|
||||
preloadStores={false} // loading handled in kubeWatchApi.subscribeStores()
|
||||
detailsItem={this.selectedItem}
|
||||
onDetails={this.onDetails}
|
||||
renderItemMenu={(item) => {
|
||||
return <KubeObjectMenu object={item}/>;
|
||||
}}
|
||||
renderItemMenu={(item: KubeObject) => <KubeObjectMenu object={item} />} // safe because we are dealing with KubeObjects here
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@ import { hideDetails } from "./kube-object-details";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
|
||||
|
||||
export interface KubeObjectMenuProps<T extends KubeObject = any> extends MenuActionsProps {
|
||||
object: T;
|
||||
export interface KubeObjectMenuProps<T> extends MenuActionsProps {
|
||||
object: T | null | undefined;
|
||||
editable?: boolean;
|
||||
removable?: boolean;
|
||||
}
|
||||
|
||||
export class KubeObjectMenu extends React.Component<KubeObjectMenuProps> {
|
||||
export class KubeObjectMenu<T extends KubeObject> extends React.Component<KubeObjectMenuProps<T>> {
|
||||
get store() {
|
||||
const { object } = this.props;
|
||||
|
||||
@@ -52,23 +52,35 @@ export class KubeObjectMenu extends React.Component<KubeObjectMenuProps> {
|
||||
@autobind()
|
||||
renderRemoveMessage() {
|
||||
const { object } = this.props;
|
||||
const resourceKind = object.kind;
|
||||
const resourceName = object.getName();
|
||||
|
||||
if (!object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p>Remove {resourceKind} <b>{resourceName}</b>?</p>
|
||||
<p>Remove {object.kind} <b>{object.getName()}</b>?</p>
|
||||
);
|
||||
}
|
||||
|
||||
getMenuItems(object: T): React.ReactChild[] {
|
||||
if (!object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return kubeObjectMenuRegistry
|
||||
.getItemsForKind(object.kind, object.apiVersion)
|
||||
.map(({components: { MenuItem }}, index) => (
|
||||
<MenuItem
|
||||
object={object}
|
||||
key={`menu-item-${index}`}
|
||||
toolbar={toolbar}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { remove, update, renderRemoveMessage, isEditable, isRemovable } = this;
|
||||
const { className, object, editable, removable, toolbar, ...menuProps } = this.props;
|
||||
|
||||
if (!object) return null;
|
||||
|
||||
const menuItems = kubeObjectMenuRegistry.getItemsForKind(object.kind, object.apiVersion).map((item, index) => {
|
||||
return <item.components.MenuItem object={object} key={`menu-item-${index}`} toolbar={toolbar} />;
|
||||
});
|
||||
const { className, object, editable, removable, ...menuProps } = this.props;
|
||||
|
||||
return (
|
||||
<MenuActions
|
||||
@@ -76,10 +88,9 @@ export class KubeObjectMenu extends React.Component<KubeObjectMenuProps> {
|
||||
updateAction={isEditable ? update : undefined}
|
||||
removeAction={isRemovable ? remove : undefined}
|
||||
removeConfirmationMessage={renderRemoveMessage}
|
||||
toolbar={toolbar}
|
||||
{...menuProps}
|
||||
>
|
||||
{menuItems}
|
||||
{this.getMenuItems(object)}
|
||||
</MenuActions>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DrawerItem, DrawerItemLabels } from "../drawer";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { Link } from "react-router-dom";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
import { getDetailsUrl } from "./kube-object-details";
|
||||
|
||||
export interface KubeObjectMetaProps {
|
||||
@@ -35,7 +36,7 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
|
||||
return (
|
||||
<>
|
||||
<DrawerItem name="Created" hidden={this.isHidden("creationTimestamp")}>
|
||||
{getAge(true, false)} ago ({creationTimestamp})
|
||||
{getAge(true, false)} ago ({<LocaleDate date={creationTimestamp} />})
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Name" hidden={this.isHidden("name")}>
|
||||
{getName()} <KubeObjectStatusIcon key="icon" object={object} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import "@testing-library/jest-dom/extend-expect";
|
||||
|
||||
import { MainLayoutHeader } from "../main-layout-header";
|
||||
import { Cluster } from "../../../../main/cluster";
|
||||
import { ClusterStore } from "../../../../common/cluster-store";
|
||||
|
||||
const cluster: Cluster = new Cluster({
|
||||
id: "foo",
|
||||
@@ -14,6 +15,14 @@ const cluster: Cluster = new Cluster({
|
||||
});
|
||||
|
||||
describe("<MainLayoutHeader />", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.getInstanceOrCreate();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = render(<MainLayoutHeader cluster={cluster} />);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid !important;
|
||||
color: var(--settingsColor);
|
||||
|
||||
@include media("<1000px") {
|
||||
--width: 85%;
|
||||
@@ -40,7 +41,16 @@
|
||||
|
||||
.sidebar {
|
||||
width: 218px;
|
||||
padding: 60px 10px 60px 20px;
|
||||
padding: 60px 0 60px 20px;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
padding: 6px 10px;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--textColorAccent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.Tabs {
|
||||
.header {
|
||||
@@ -62,18 +72,18 @@
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
color: var(--textColorSecondary);
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--navHoverBackground);
|
||||
background-color: var(--navSelectedBackground);
|
||||
color: var(--navHoverColor);
|
||||
}
|
||||
|
||||
@@ -194,5 +204,9 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.Checkbox .label {
|
||||
color: var(--textColorTertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ import { Icon } from "../icon";
|
||||
|
||||
export interface PageLayoutProps extends React.DOMAttributes<any> {
|
||||
className?: IClassName;
|
||||
header?: React.ReactNode;
|
||||
headerClass?: IClassName;
|
||||
contentClass?: IClassName;
|
||||
provideBackButtonNavigation?: boolean;
|
||||
contentGaps?: boolean;
|
||||
@@ -55,7 +53,7 @@ export class PageLayout extends React.Component<PageLayoutProps> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
contentClass, headerClass, provideBackButtonNavigation,
|
||||
contentClass, provideBackButtonNavigation,
|
||||
contentGaps, showOnTop, navigation, children, ...elemProps
|
||||
} = this.props;
|
||||
const className = cssNames("PageLayout", { showOnTop, showNavigation: navigation }, this.props.className);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./locale-date";
|
||||
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user