mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into catalog-entity-pin-icons
This commit is contained in:
commit
18896ac9f6
11
package.json
11
package.json
@ -205,7 +205,7 @@
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"handlebars": "^4.7.7",
|
||||
"http-proxy": "^1.18.1",
|
||||
"immer": "^8.0.4",
|
||||
"immer": "^9.0.6",
|
||||
"joi": "^17.4.2",
|
||||
"js-yaml": "^3.14.0",
|
||||
"jsdom": "^16.7.0",
|
||||
@ -310,7 +310,7 @@
|
||||
"@types/tcp-port-used": "^1.0.0",
|
||||
"@types/tempy": "^0.3.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"@types/url-parse": "^1.4.3",
|
||||
"@types/url-parse": "^1.4.4",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@types/webdriverio": "^4.13.0",
|
||||
"@types/webpack": "^4.41.31",
|
||||
@ -323,7 +323,7 @@
|
||||
"chart.js": "^2.9.4",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"color": "^3.1.2",
|
||||
"concurrently": "^5.2.0",
|
||||
"concurrently": "^5.3.0",
|
||||
"css-loader": "^5.2.7",
|
||||
"deepdash": "^5.3.9",
|
||||
"dompurify": "^2.3.3",
|
||||
@ -353,11 +353,10 @@
|
||||
"node-gyp": "7.1.2",
|
||||
"node-loader": "^1.0.3",
|
||||
"nodemon": "^2.0.13",
|
||||
"playwright": "^1.14.0",
|
||||
"playwright": "^1.15.2",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-loader": "4.3.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.4.1",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"randomcolor": "^0.6.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
@ -368,7 +367,7 @@
|
||||
"react-select-event": "^5.1.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-window": "^1.8.5",
|
||||
"sass": "^1.41.1",
|
||||
"sass": "^1.43.2",
|
||||
"sass-loader": "^8.0.2",
|
||||
"sharp": "^0.29.1",
|
||||
"style-loader": "^2.0.0",
|
||||
|
||||
@ -38,6 +38,6 @@ fetchMock.enableMocks();
|
||||
// Mock __non_webpack_require__ for tests
|
||||
globalThis.__non_webpack_require__ = jest.fn();
|
||||
|
||||
process.on("unhandledRejection", (err) => {
|
||||
process.on("unhandledRejection", (err: any) => {
|
||||
fail(err);
|
||||
});
|
||||
|
||||
@ -26,7 +26,7 @@ import { FSWatcher, watch } from "chokidar";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type stream from "stream";
|
||||
import { Disposer, ExtendedObservableMap, iter, Singleton, storedKubeConfigFolder } from "../../common/utils";
|
||||
import { bytesToUnits, Disposer, ExtendedObservableMap, iter, noop, Singleton, storedKubeConfigFolder } from "../../common/utils";
|
||||
import logger from "../logger";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { loadConfigFromString, splitConfig } from "../../common/kube-helpers";
|
||||
@ -54,6 +54,15 @@ const ignoreGlobs = [
|
||||
matcher: globToRegExp(rawGlob),
|
||||
}));
|
||||
|
||||
/**
|
||||
* This should be much larger than any kubeconfig text file
|
||||
*
|
||||
* Even if you have a cert-file, key-file, and client-cert files that is only
|
||||
* 12kb of extra data (at 4096 bytes each) which allows for around 150 entries.
|
||||
*/
|
||||
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
|
||||
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
|
||||
|
||||
export class KubeconfigSyncManager extends Singleton {
|
||||
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
||||
protected syncing = false;
|
||||
@ -228,15 +237,30 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
|
||||
});
|
||||
}
|
||||
|
||||
function diffChangedConfig(filePath: string, source: RootSource): Disposer {
|
||||
interface DiffChangedConfigArgs {
|
||||
filePath: string;
|
||||
source: RootSource;
|
||||
stats: fs.Stats;
|
||||
maxAllowedFileReadSize: number;
|
||||
}
|
||||
|
||||
function diffChangedConfig({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer {
|
||||
logger.debug(`${logPrefix} file changed`, { filePath });
|
||||
|
||||
if (stats.size >= maxAllowedFileReadSize) {
|
||||
logger.warn(`${logPrefix} skipping ${filePath}: size=${bytesToUnits(stats.size)} is larger than maxSize=${bytesToUnits(maxAllowedFileReadSize)}`);
|
||||
source.clear();
|
||||
|
||||
return noop;
|
||||
}
|
||||
|
||||
// TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out)
|
||||
const fileReader = fs.createReadStream(filePath, {
|
||||
mode: fs.constants.O_RDONLY,
|
||||
});
|
||||
const readStream: stream.Readable = fileReader;
|
||||
const bufs: Buffer[] = [];
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true });
|
||||
let fileString = "";
|
||||
let closed = false;
|
||||
|
||||
const cleanup = () => {
|
||||
@ -253,7 +277,15 @@ function diffChangedConfig(filePath: string, source: RootSource): Disposer {
|
||||
};
|
||||
|
||||
readStream
|
||||
.on("data", chunk => bufs.push(chunk))
|
||||
.on("data", (chunk: Buffer) => {
|
||||
try {
|
||||
fileString += decoder.decode(chunk, { stream: true });
|
||||
} catch (error) {
|
||||
logger.warn(`${logPrefix} skipping ${filePath}: ${error}`);
|
||||
source.clear();
|
||||
cleanup();
|
||||
}
|
||||
})
|
||||
.on("close", () => cleanup())
|
||||
.on("error", error => {
|
||||
cleanup();
|
||||
@ -261,7 +293,7 @@ function diffChangedConfig(filePath: string, source: RootSource): Disposer {
|
||||
})
|
||||
.on("end", () => {
|
||||
if (!closed) {
|
||||
computeDiff(Buffer.concat(bufs).toString("utf-8"), source, filePath);
|
||||
computeDiff(fileString, source, filePath);
|
||||
}
|
||||
});
|
||||
|
||||
@ -279,6 +311,9 @@ function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, D
|
||||
const stat = await fs.promises.stat(filePath);
|
||||
const isFolderSync = stat.isDirectory();
|
||||
const cleanupFns = new Map<string, Disposer>();
|
||||
const maxAllowedFileReadSize = isFolderSync
|
||||
? folderSyncMaxAllowedFileReadSize
|
||||
: fileSyncMaxAllowedFileReadSize;
|
||||
|
||||
watcher = watch(filePath, {
|
||||
followSymlinks: true,
|
||||
@ -294,7 +329,7 @@ function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, D
|
||||
});
|
||||
|
||||
watcher
|
||||
.on("change", (childFilePath) => {
|
||||
.on("change", (childFilePath, stats) => {
|
||||
const cleanup = cleanupFns.get(childFilePath);
|
||||
|
||||
if (!cleanup) {
|
||||
@ -303,9 +338,14 @@ function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, D
|
||||
}
|
||||
|
||||
cleanup();
|
||||
cleanupFns.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map)));
|
||||
cleanupFns.set(childFilePath, diffChangedConfig({
|
||||
filePath: childFilePath,
|
||||
source: rootSource.getOrInsert(childFilePath, observable.map),
|
||||
stats,
|
||||
maxAllowedFileReadSize,
|
||||
}));
|
||||
})
|
||||
.on("add", (childFilePath) => {
|
||||
.on("add", (childFilePath, stats) => {
|
||||
if (isFolderSync) {
|
||||
const fileName = path.basename(childFilePath);
|
||||
|
||||
@ -316,7 +356,12 @@ function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, D
|
||||
}
|
||||
}
|
||||
|
||||
cleanupFns.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map)));
|
||||
cleanupFns.set(childFilePath, diffChangedConfig({
|
||||
filePath: childFilePath,
|
||||
source: rootSource.getOrInsert(childFilePath, observable.map),
|
||||
stats,
|
||||
maxAllowedFileReadSize,
|
||||
}));
|
||||
})
|
||||
.on("unlink", (childFilePath) => {
|
||||
cleanupFns.get(childFilePath)?.();
|
||||
|
||||
@ -35,6 +35,7 @@ import plimit from "p-limit";
|
||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../common/cluster-types";
|
||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types";
|
||||
import { storedKubeConfigFolder, toJS } from "../common/utils";
|
||||
import type { Response } from "request";
|
||||
|
||||
/**
|
||||
* Cluster
|
||||
@ -642,8 +643,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
};
|
||||
}
|
||||
|
||||
protected getAllowedNamespacesErrorCount = 0;
|
||||
|
||||
protected async getAllowedNamespaces() {
|
||||
if (this.accessibleNamespaces.length) {
|
||||
return this.accessibleNamespaces;
|
||||
@ -655,24 +654,16 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
const { body: { items } } = await api.listNamespace();
|
||||
const namespaces = items.map(ns => ns.metadata.name);
|
||||
|
||||
this.getAllowedNamespacesErrorCount = 0; // reset on success
|
||||
|
||||
return namespaces;
|
||||
} catch (error) {
|
||||
const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName);
|
||||
const namespaceList = [ctx.namespace].filter(Boolean);
|
||||
|
||||
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||
this.getAllowedNamespacesErrorCount += 1;
|
||||
const { response } = error as HttpError & { response: Response };
|
||||
|
||||
if (this.getAllowedNamespacesErrorCount > 3) {
|
||||
// reset on send
|
||||
this.getAllowedNamespacesErrorCount = 0;
|
||||
|
||||
// then broadcast, make sure it is 3 successive attempts
|
||||
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error });
|
||||
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
|
||||
}
|
||||
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
|
||||
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
|
||||
}
|
||||
|
||||
return namespaceList;
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
import styles from "./catalog.module.css";
|
||||
import React from "react";
|
||||
import { action, computed } from "mobx";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { CatalogEntity } from "../../api/catalog-entity";
|
||||
import type { ItemObject } from "../../../common/item.store";
|
||||
import { Badge } from "../badge";
|
||||
import { navigation } from "../../navigation";
|
||||
@ -33,7 +33,11 @@ import type { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
const css = makeCss(styles);
|
||||
|
||||
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
|
||||
constructor(public entity: T, private registry: CatalogEntityRegistry) {}
|
||||
constructor(public entity: T, private registry: CatalogEntityRegistry) {
|
||||
if (!(entity instanceof CatalogEntity)) {
|
||||
throw Object.assign(new TypeError("CatalogEntityItem cannot wrap a non-CatalogEntity type"), { typeof: typeof entity, prototype: Object.getPrototypeOf(entity) });
|
||||
}
|
||||
}
|
||||
|
||||
get kind() {
|
||||
return this.entity.kind;
|
||||
|
||||
@ -26,7 +26,7 @@ import { Catalog } from "./catalog";
|
||||
import { createMemoryHistory } from "history";
|
||||
import { mockWindow } from "../../../../__mocks__/windowMock";
|
||||
import { kubernetesClusterCategory } from "../../../common/catalog-entities/kubernetes-cluster";
|
||||
import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../../common/catalog";
|
||||
import { catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog";
|
||||
import { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
|
||||
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||
import { CatalogEntityItem } from "./catalog-entity-item";
|
||||
@ -52,6 +52,18 @@ jest.mock("./hotbar-toggle-menu-item", () => {
|
||||
};
|
||||
});
|
||||
|
||||
class MockCatalogEntity extends CatalogEntity {
|
||||
public apiVersion = "api";
|
||||
public kind = "kind";
|
||||
|
||||
constructor(data: CatalogEntityData, public onRun: (context: CatalogEntityActionContext) => void | Promise<void>) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public onContextMenuOpen(): void | Promise<void> {}
|
||||
public onSettingsOpen(): void | Promise<void> {}
|
||||
}
|
||||
|
||||
describe("<Catalog />", () => {
|
||||
const history = createMemoryHistory();
|
||||
const mockLocation = {
|
||||
@ -72,30 +84,21 @@ describe("<Catalog />", () => {
|
||||
url: "",
|
||||
};
|
||||
|
||||
const catalogEntityUid = "a_catalogEntity_uid";
|
||||
const catalogEntity = {
|
||||
enabled: true,
|
||||
apiVersion: "api",
|
||||
kind: "kind",
|
||||
metadata: {
|
||||
uid: catalogEntityUid,
|
||||
name: "a catalog entity",
|
||||
labels: {
|
||||
test: "label",
|
||||
function createMockCatalogEntity(onRun: (context: CatalogEntityActionContext) => void | Promise<void>) {
|
||||
return new MockCatalogEntity({
|
||||
metadata: {
|
||||
uid: "a_catalogEntity_uid",
|
||||
name: "a catalog entity",
|
||||
labels: {
|
||||
test: "label",
|
||||
},
|
||||
},
|
||||
},
|
||||
status: {
|
||||
phase: "",
|
||||
},
|
||||
spec: {},
|
||||
};
|
||||
const catalogEntityItemMethods = {
|
||||
getId: () => catalogEntity.metadata.uid,
|
||||
getName: () => catalogEntity.metadata.name,
|
||||
onContextMenuOpen: () => {},
|
||||
onSettingsOpen: () => {},
|
||||
onRun: () => {},
|
||||
};
|
||||
status: {
|
||||
phase: "",
|
||||
},
|
||||
spec: {},
|
||||
}, onRun);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
CatalogEntityDetailRegistry.createInstance();
|
||||
@ -123,11 +126,7 @@ describe("<Catalog />", () => {
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = new CatalogEntityItem({
|
||||
...catalogEntity,
|
||||
...catalogEntityItemMethods,
|
||||
onRun,
|
||||
}, catalogEntityRegistry);
|
||||
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
@ -136,29 +135,8 @@ describe("<Catalog />", () => {
|
||||
|
||||
catalogEntityRegistry.addOnBeforeRun(
|
||||
(event) => {
|
||||
expect(event.target).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"apiVersion": "api",
|
||||
"enabled": true,
|
||||
"getId": [Function],
|
||||
"getName": [Function],
|
||||
"kind": "kind",
|
||||
"metadata": Object {
|
||||
"labels": Object {
|
||||
"test": "label",
|
||||
},
|
||||
"name": "a catalog entity",
|
||||
"uid": "a_catalogEntity_uid",
|
||||
},
|
||||
"onContextMenuOpen": [Function],
|
||||
"onRun": [MockFunction],
|
||||
"onSettingsOpen": [Function],
|
||||
"spec": Object {},
|
||||
"status": Object {
|
||||
"phase": "",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(event.target.getId()).toBe("a_catalogEntity_uid");
|
||||
expect(event.target.getName()).toBe("a catalog entity");
|
||||
|
||||
setTimeout(() => {
|
||||
expect(onRun).toHaveBeenCalled();
|
||||
@ -184,11 +162,7 @@ describe("<Catalog />", () => {
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = new CatalogEntityItem({
|
||||
...catalogEntity,
|
||||
...catalogEntityItemMethods,
|
||||
onRun,
|
||||
}, catalogEntityRegistry);
|
||||
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
@ -222,11 +196,7 @@ describe("<Catalog />", () => {
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = new CatalogEntityItem({
|
||||
...catalogEntity,
|
||||
...catalogEntityItemMethods,
|
||||
onRun,
|
||||
}, catalogEntityRegistry);
|
||||
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
@ -261,11 +231,7 @@ describe("<Catalog />", () => {
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn(() => done());
|
||||
const catalogEntityItem = new CatalogEntityItem({
|
||||
...catalogEntity,
|
||||
...catalogEntityItemMethods,
|
||||
onRun,
|
||||
}, catalogEntityRegistry);
|
||||
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
@ -295,11 +261,7 @@ describe("<Catalog />", () => {
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = new CatalogEntityItem({
|
||||
...catalogEntity,
|
||||
...catalogEntityItemMethods,
|
||||
onRun,
|
||||
}, catalogEntityRegistry);
|
||||
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
@ -336,11 +298,7 @@ describe("<Catalog />", () => {
|
||||
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||
const onRun = jest.fn();
|
||||
const catalogEntityItem = new CatalogEntityItem({
|
||||
...catalogEntity,
|
||||
...catalogEntityItemMethods,
|
||||
onRun,
|
||||
}, catalogEntityRegistry);
|
||||
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry);
|
||||
|
||||
// mock as if there is a selected item > the detail panel opens
|
||||
jest
|
||||
|
||||
@ -33,6 +33,7 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
export interface HpaDetailsProps extends KubeObjectDetailsProps<HorizontalPodAutoscaler> {
|
||||
}
|
||||
@ -80,17 +81,13 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
<TableCell className="metrics">Current / Target</TableCell>
|
||||
</TableHead>
|
||||
{
|
||||
hpa.getMetrics().map((metric, index) => {
|
||||
const name = renderName(metric);
|
||||
const values = hpa.getMetricValues(metric);
|
||||
|
||||
return (
|
||||
hpa.getMetrics()
|
||||
.map((metric, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="name">{name}</TableCell>
|
||||
<TableCell className="metrics">{values}</TableCell>
|
||||
<TableCell className="name">{renderName(metric)}</TableCell>
|
||||
<TableCell className="metrics">{hpa.getMetricValues(metric)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
))
|
||||
}
|
||||
</Table>
|
||||
);
|
||||
@ -99,7 +96,16 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
||||
render() {
|
||||
const { object: hpa } = this.props;
|
||||
|
||||
if (!hpa) return null;
|
||||
if (!hpa) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(hpa instanceof HorizontalPodAutoscaler)) {
|
||||
logger.error("[HpaDetails]: passed object that is not an instanceof HorizontalPodAutoscaler", hpa);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { scaleTargetRef } = hpa.spec;
|
||||
|
||||
return (
|
||||
|
||||
@ -28,6 +28,7 @@ import { LimitPart, LimitRange, LimitRangeItem, Resource } from "../../../common
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { DrawerItem } from "../drawer/drawer-item";
|
||||
import { Badge } from "../badge";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<LimitRange> {
|
||||
}
|
||||
@ -57,15 +58,13 @@ function renderResourceLimits(limit: LimitRangeItem, resource: Resource) {
|
||||
|
||||
function renderLimitDetails(limits: LimitRangeItem[], resources: Resource[]) {
|
||||
|
||||
return resources.map(resource =>
|
||||
return resources.map(resource => (
|
||||
<DrawerItem key={resource} name={resource}>
|
||||
{
|
||||
limits.map(limit =>
|
||||
renderResourceLimits(limit, resource)
|
||||
)
|
||||
limits.map(limit => renderResourceLimits(limit, resource))
|
||||
}
|
||||
</DrawerItem>
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -73,7 +72,16 @@ export class LimitRangeDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: limitRange } = this.props;
|
||||
|
||||
if (!limitRange) return null;
|
||||
if (!limitRange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(limitRange instanceof LimitRange)) {
|
||||
logger.error("[LimitRangeDetails]: passed object that is not an instanceof LimitRange", limitRange);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerLimits = limitRange.getContainerLimits();
|
||||
const podLimits = limitRange.getPodLimits();
|
||||
const pvcLimits = limitRange.getPVCLimits();
|
||||
|
||||
@ -30,8 +30,9 @@ import { Input } from "../input";
|
||||
import { Button } from "../button";
|
||||
import { configMapsStore } from "./config-maps.store";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { ConfigMap } from "../../../common/k8s-api/endpoints";
|
||||
import { ConfigMap } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ConfigMap> {
|
||||
}
|
||||
@ -82,7 +83,16 @@ export class ConfigMapDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: configMap } = this.props;
|
||||
|
||||
if (!configMap) return null;
|
||||
if (!configMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(configMap instanceof ConfigMap)) {
|
||||
logger.error("[ConfigMapDetails]: passed object that is not an instanceof ConfigMap", configMap);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = Array.from(this.data.entries());
|
||||
|
||||
return (
|
||||
@ -93,22 +103,20 @@ export class ConfigMapDetails extends React.Component<Props> {
|
||||
<>
|
||||
<DrawerTitle title="Data"/>
|
||||
{
|
||||
data.map(([name, value]) => {
|
||||
return (
|
||||
<div key={name} className="data">
|
||||
<div className="name">{name}</div>
|
||||
<div className="flex gaps align-flex-start">
|
||||
<Input
|
||||
multiLine
|
||||
theme="round-black"
|
||||
className="box grow"
|
||||
value={value}
|
||||
onChange={v => this.data.set(name, v)}
|
||||
/>
|
||||
</div>
|
||||
data.map(([name, value]) => (
|
||||
<div key={name} className="data">
|
||||
<div className="name">{name}</div>
|
||||
<div className="flex gaps align-flex-start">
|
||||
<Input
|
||||
multiLine
|
||||
theme="round-black"
|
||||
className="box grow"
|
||||
value={value}
|
||||
onChange={v => this.data.set(name, v)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<Button
|
||||
primary
|
||||
|
||||
@ -26,8 +26,9 @@ import { observer } from "mobx-react";
|
||||
import { DrawerItem } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { PodDisruptionBudget } from "../../../common/k8s-api/endpoints";
|
||||
import { PodDisruptionBudget } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<PodDisruptionBudget> {
|
||||
}
|
||||
@ -38,7 +39,16 @@ export class PodDisruptionBudgetDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: pdb } = this.props;
|
||||
|
||||
if (!pdb) return null;
|
||||
if (!pdb) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(pdb instanceof PodDisruptionBudget)) {
|
||||
logger.error("[PodDisruptionBudgetDetails]: passed object that is not an instanceof PodDisruptionBudget", pdb);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectors = pdb.getSelectors();
|
||||
|
||||
return (
|
||||
|
||||
@ -26,10 +26,11 @@ import { observer } from "mobx-react";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { cpuUnitsToNumber, cssNames, unitsToBytes, metricUnitsToNumber } from "../../utils";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { ResourceQuota } from "../../../common/k8s-api/endpoints/resource-quota.api";
|
||||
import { ResourceQuota } from "../../../common/k8s-api/endpoints/resource-quota.api";
|
||||
import { LineProgress } from "../line-progress";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ResourceQuota> {
|
||||
}
|
||||
@ -77,7 +78,15 @@ export class ResourceQuotaDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: quota } = this.props;
|
||||
|
||||
if (!quota) return null;
|
||||
if (!quota) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(quota instanceof ResourceQuota)) {
|
||||
logger.error("[ResourceQuotaDetails]: passed object that is not an instanceof ResourceQuota", quota);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ResourceQuotaDetails">
|
||||
|
||||
@ -33,8 +33,9 @@ import { base64 } from "../../utils";
|
||||
import { Icon } from "../icon";
|
||||
import { secretsStore } from "./secrets.store";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { Secret } from "../../../common/k8s-api/endpoints";
|
||||
import { Secret } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Secret> {
|
||||
}
|
||||
@ -84,7 +85,15 @@ export class SecretDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: secret } = this.props;
|
||||
|
||||
if (!secret) return null;
|
||||
if (!secret) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(secret instanceof Secret)) {
|
||||
logger.error("[SecretDetails]: passed object that is not an instanceof Secret", secret);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SecretDetails">
|
||||
|
||||
@ -24,7 +24,7 @@ import "./crd-details.scss";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import { cssNames } from "../../utils";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { Badge } from "../badge";
|
||||
@ -35,6 +35,7 @@ import { Input } from "../input";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import MonacoEditor from "react-monaco-editor";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
|
||||
}
|
||||
@ -44,7 +45,16 @@ export class CRDDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: crd } = this.props;
|
||||
|
||||
if (!crd) return null;
|
||||
if (!crd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(crd instanceof CustomResourceDefinition)) {
|
||||
logger.error("[CRDDetails]: passed object that is not an instanceof CustomResourceDefinition", crd);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { plural, singular, kind, listKind } = crd.getNames();
|
||||
const printerColumns = crd.getPrinterColumns();
|
||||
const validation = crd.getValidation();
|
||||
|
||||
@ -30,9 +30,10 @@ import { DrawerItem } from "../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { Input } from "../input";
|
||||
import type { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
|
||||
import { parseJsonPath } from "../../utils/jsonPath";
|
||||
import type { KubeObject, KubeObjectMetadata, KubeObjectStatus } from "../../../common/k8s-api/kube-object";
|
||||
import { KubeObject, KubeObjectMetadata, KubeObjectStatus } from "../../../common/k8s-api/kube-object";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<KubeObject> {
|
||||
crd: CustomResourceDefinition;
|
||||
@ -101,6 +102,18 @@ export class CrdResourceDetails extends React.Component<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(object instanceof KubeObject)) {
|
||||
logger.error("[CrdResourceDetails]: passed object that is not an instanceof KubeObject", object);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(crd instanceof CustomResourceDefinition)) {
|
||||
logger.error("[CrdResourceDetails]: passed crd that is not an instanceof CustomResourceDefinition", crd);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const extraColumns = crd.getPrinterColumns();
|
||||
|
||||
return (
|
||||
|
||||
@ -34,6 +34,7 @@ import type { EntitySettingsRouteParams } from "../../../common/routes";
|
||||
import { groupBy } from "lodash";
|
||||
import { SettingLayout } from "../layout/setting-layout";
|
||||
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends RouteComponentProps<EntitySettingsRouteParams> {
|
||||
}
|
||||
@ -45,6 +46,17 @@ export class EntitySettings extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
|
||||
const { hash } = navigation.location;
|
||||
|
||||
if (hash) {
|
||||
const menuId = hash.slice(1);
|
||||
const item = this.menuItems.find((item) => item.id === menuId);
|
||||
|
||||
if (item) {
|
||||
this.activeTab = item.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get entityId() {
|
||||
@ -61,18 +73,10 @@ export class EntitySettings extends React.Component<Props> {
|
||||
return EntitySettingRegistry.getInstance().getItemsForKind(this.entity.kind, this.entity.apiVersion, this.entity.metadata.source);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const { hash } = navigation.location;
|
||||
get activeSetting() {
|
||||
this.activeTab ||= this.menuItems[0]?.id;
|
||||
|
||||
if (hash) {
|
||||
const item = this.menuItems.find((item) => item.title === hash.slice(1));
|
||||
|
||||
if (item) {
|
||||
this.activeTab = item.id;
|
||||
}
|
||||
}
|
||||
|
||||
this.ensureActiveTab();
|
||||
return this.menuItems.find((setting) => setting.id === this.activeTab);
|
||||
}
|
||||
|
||||
onTabChange = (tabId: string) => {
|
||||
@ -122,33 +126,31 @@ export class EntitySettings extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
ensureActiveTab() {
|
||||
if (!this.activeTab) {
|
||||
this.activeTab = this.menuItems[0]?.id;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.entity) {
|
||||
console.error("entity not found", this.entityId);
|
||||
logger.error("[ENTITY-SETTINGS]: entity not found", this.entityId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
this.ensureActiveTab();
|
||||
const activeSetting = this.menuItems.find((setting) => setting.id === this.activeTab);
|
||||
const { activeSetting } = this;
|
||||
|
||||
|
||||
return (
|
||||
<SettingLayout
|
||||
navigation={this.renderNavigation()}
|
||||
contentGaps={false}
|
||||
>
|
||||
<section>
|
||||
<h2 data-testid={`${activeSetting.id}-header`}>{activeSetting.title}</h2>
|
||||
<section>
|
||||
<activeSetting.components.View entity={this.entity} key={activeSetting.title} />
|
||||
</section>
|
||||
</section>
|
||||
{
|
||||
activeSetting && (
|
||||
<section>
|
||||
<h2 data-testid={`${activeSetting.id}-header`}>{activeSetting.title}</h2>
|
||||
<section>
|
||||
<activeSetting.components.View entity={this.entity} key={activeSetting.title} />
|
||||
</section>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</SettingLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@ -27,12 +27,13 @@ import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Link } from "react-router-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { KubeEvent } from "../../../common/k8s-api/endpoints/events.api";
|
||||
import { KubeEvent } from "../../../common/k8s-api/endpoints/events.api";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<KubeEvent> {
|
||||
}
|
||||
@ -42,7 +43,16 @@ export class EventDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: event } = this.props;
|
||||
|
||||
if (!event) return null;
|
||||
if (!event) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(event instanceof KubeEvent)) {
|
||||
logger.error("[EventDetails]: passed object that is not an instanceof KubeEvent", event);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { message, reason, count, type, involvedObject } = event;
|
||||
const { kind, name, namespace, fieldPath } = involvedObject;
|
||||
|
||||
|
||||
@ -23,11 +23,12 @@ import "./kube-event-details.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { cssNames } from "../../utils";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
import { eventStore } from "./event.store";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
export interface KubeEventDetailsProps {
|
||||
object: KubeObject;
|
||||
@ -41,6 +42,17 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||
|
||||
render() {
|
||||
const { object } = this.props;
|
||||
|
||||
if (!object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(object instanceof KubeObject)) {
|
||||
logger.error("[KubeEventDetails]: passed object that is not an instanceof KubeObject", object);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const events = eventStore.getEventsByObject(object);
|
||||
|
||||
if (!events.length) {
|
||||
|
||||
@ -38,6 +38,7 @@ import { PodCharts, podMetricTabs } from "../+workloads-pods/pod-charts";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Namespace> {
|
||||
}
|
||||
@ -81,7 +82,16 @@ export class NamespaceDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: namespace } = this.props;
|
||||
|
||||
if (!namespace) return null;
|
||||
if (!namespace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(namespace instanceof Namespace)) {
|
||||
logger.error("[NamespaceDetails]: passed object that is not an instanceof Namespace", namespace);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = namespace.getStatus();
|
||||
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Namespace);
|
||||
|
||||
|
||||
@ -25,9 +25,10 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { Endpoint } from "../../../common/k8s-api/endpoints";
|
||||
import { Endpoint } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { EndpointSubsetList } from "./endpoint-subset-list";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Endpoint> {
|
||||
}
|
||||
@ -37,7 +38,15 @@ export class EndpointDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: endpoint } = this.props;
|
||||
|
||||
if (!endpoint) return null;
|
||||
if (!endpoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(endpoint instanceof Endpoint)) {
|
||||
logger.error("[EndpointDetails]: passed object that is not an instanceof Endpoint", endpoint);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="EndpointDetails">
|
||||
|
||||
@ -25,7 +25,7 @@ import React from "react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { makeObservable, observable, reaction } from "mobx";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import type { ILoadBalancerIngress, Ingress } from "../../../common/k8s-api/endpoints";
|
||||
import { ILoadBalancerIngress, Ingress } from "../../../common/k8s-api/endpoints";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { ResourceMetrics } from "../resource-metrics";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
@ -35,6 +35,7 @@ import { getBackendServiceNamePort, getMetricsForIngress, IIngressMetrics } from
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Ingress> {
|
||||
}
|
||||
@ -132,6 +133,12 @@ export class IngressDetails extends React.Component<Props> {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(ingress instanceof Ingress)) {
|
||||
logger.error("[IngressDetails]: passed object that is not an instanceof Ingress", ingress);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { spec, status } = ingress;
|
||||
const ingressPoints = status?.loadBalancer?.ingress;
|
||||
const { metrics } = this;
|
||||
|
||||
@ -24,12 +24,13 @@ import "./network-policy-details.scss";
|
||||
import get from "lodash/get";
|
||||
import React, { Fragment } from "react";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import type { IPolicyEgress, IPolicyIngress, IPolicyIpBlock, IPolicySelector, NetworkPolicy } from "../../../common/k8s-api/endpoints/network-policy.api";
|
||||
import { IPolicyEgress, IPolicyIngress, IPolicyIpBlock, IPolicySelector, NetworkPolicy } from "../../../common/k8s-api/endpoints/network-policy.api";
|
||||
import { Badge } from "../badge";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { observer } from "mobx-react";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<NetworkPolicy> {
|
||||
}
|
||||
@ -117,6 +118,13 @@ export class NetworkPolicyDetails extends React.Component<Props> {
|
||||
if (!policy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(policy instanceof NetworkPolicy)) {
|
||||
logger.error("[NetworkPolicyDetails]: passed object that is not an instanceof NetworkPolicy", policy);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ingress, egress } = policy.spec;
|
||||
const selector = policy.getMatchLabels();
|
||||
|
||||
|
||||
@ -26,13 +26,14 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { Service } from "../../../common/k8s-api/endpoints";
|
||||
import { Service } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { ServicePortComponent } from "./service-port-component";
|
||||
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
||||
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
|
||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||
import { portForwardStore } from "../../port-forward";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Service> {
|
||||
}
|
||||
@ -54,7 +55,16 @@ export class ServiceDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: service } = this.props;
|
||||
|
||||
if (!service) return null;
|
||||
if (!service) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(service instanceof Service)) {
|
||||
logger.error("[ServiceDetails]: passed object that is not an instanceof Service", service);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { spec } = service;
|
||||
const endpoint = endpointStore.getByName(service.getName(), service.getNs());
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { NodeDetailsResources } from "./node-details-resources";
|
||||
import { DrawerTitle } from "../drawer/drawer-title";
|
||||
import { boundMethod } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Node> {
|
||||
}
|
||||
@ -72,7 +73,16 @@ export class NodeDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: node } = this.props;
|
||||
|
||||
if (!node) return null;
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(node instanceof Node)) {
|
||||
logger.error("[NodeDetails]: passed object that is not an instanceof Node", node);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { status } = node;
|
||||
const { nodeInfo, addresses } = status;
|
||||
const conditions = node.getActiveConditions();
|
||||
|
||||
@ -25,22 +25,26 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { PodSecurityPolicy } from "../../../common/k8s-api/endpoints";
|
||||
import { PodSecurityPolicy } from "../../../common/k8s-api/endpoints";
|
||||
import { Badge } from "../badge";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<PodSecurityPolicy> {
|
||||
}
|
||||
|
||||
interface RuleGroup {
|
||||
rule: string;
|
||||
ranges?: {
|
||||
max: number;
|
||||
min: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
@observer
|
||||
export class PodSecurityPolicyDetails extends React.Component<Props> {
|
||||
renderRuleGroup(
|
||||
title: React.ReactNode,
|
||||
group: {
|
||||
rule: string;
|
||||
ranges?: { max: number; min: number }[];
|
||||
}) {
|
||||
renderRuleGroup( title: React.ReactNode, group: RuleGroup) {
|
||||
if (!group) return null;
|
||||
const { rule, ranges } = group;
|
||||
|
||||
@ -67,6 +71,13 @@ export class PodSecurityPolicyDetails extends React.Component<Props> {
|
||||
if (!psp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(psp instanceof PodSecurityPolicy)) {
|
||||
logger.error("[PodSecurityPolicyDetails]: passed object that is not an instanceof PodSecurityPolicy", psp);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
allowedHostPaths, allowedCapabilities, allowedCSIDrivers, allowedFlexVolumes, allowedProcMountTypes,
|
||||
allowedUnsafeSysctls, allowPrivilegeEscalation, defaultAddCapabilities, forbiddenSysctls, fsGroup,
|
||||
@ -172,14 +183,14 @@ export class PodSecurityPolicyDetails extends React.Component<Props> {
|
||||
<TableCell>Path Prefix</TableCell>
|
||||
<TableCell>Read-only</TableCell>
|
||||
</TableHead>
|
||||
{allowedHostPaths.map(({ pathPrefix, readOnly }, index) => {
|
||||
return (
|
||||
{
|
||||
allowedHostPaths.map(({ pathPrefix, readOnly }, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{pathPrefix}</TableCell>
|
||||
<TableCell>{readOnly ? "Yes" : "No"}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
))
|
||||
}
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
@ -193,7 +204,7 @@ export class PodSecurityPolicyDetails extends React.Component<Props> {
|
||||
<>
|
||||
<DrawerTitle title="Runtime Class"/>
|
||||
<DrawerItem name="Allowed Runtime Class Names">
|
||||
{(runtimeClass.allowedRuntimeClassNames || []).join(", ") || "-"}
|
||||
{runtimeClass.allowedRuntimeClassNames?.join(", ") || "-"}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Default Runtime Class Name">
|
||||
{runtimeClass.defaultRuntimeClassName || "-"}
|
||||
|
||||
@ -27,11 +27,12 @@ import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
import { Badge } from "../badge";
|
||||
import { observer } from "mobx-react";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import type { StorageClass } from "../../../common/k8s-api/endpoints";
|
||||
import { StorageClass } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { storageClassStore } from "./storage-class.store";
|
||||
import { VolumeDetailsList } from "../+storage-volumes/volume-details-list";
|
||||
import { volumesStore } from "../+storage-volumes/volumes.store";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<StorageClass> {
|
||||
}
|
||||
@ -44,9 +45,18 @@ export class StorageClassDetails extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const { object: storageClass } = this.props;
|
||||
const persistentVolumes = storageClassStore.getPersistentVolumes(storageClass);
|
||||
|
||||
if (!storageClass) return null;
|
||||
if (!storageClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(storageClass instanceof StorageClass)) {
|
||||
logger.error("[StorageClassDetails]: passed object that is not an instanceof StorageClass", storageClass);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const persistentVolumes = storageClassStore.getPersistentVolumes(storageClass);
|
||||
const { provisioner, parameters, mountOptions } = storageClass;
|
||||
|
||||
return (
|
||||
|
||||
@ -37,6 +37,7 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import { boundMethod } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
||||
}
|
||||
@ -68,6 +69,13 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
|
||||
if (!volumeClaim) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(volumeClaim instanceof PersistentVolumeClaim)) {
|
||||
logger.error("[PersistentVolumeClaimDetails]: passed object that is not an instanceof PersistentVolumeClaim", volumeClaim);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { storageClassName, accessModes } = volumeClaim.spec;
|
||||
const { metrics } = this;
|
||||
const pods = volumeClaim.getPods(podsStore.items);
|
||||
|
||||
@ -31,6 +31,7 @@ import { PersistentVolume, pvcApi } from "../../../common/k8s-api/endpoints";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<PersistentVolume> {
|
||||
}
|
||||
@ -43,6 +44,13 @@ export class PersistentVolumeDetails extends React.Component<Props> {
|
||||
if (!volume) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(volume instanceof PersistentVolume)) {
|
||||
logger.error("[PersistentVolumeDetails]: passed object that is not an instanceof PersistentVolume", volume);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { accessModes, capacity, persistentVolumeReclaimPolicy, storageClassName, claimRef, flexVolume, mountOptions, nfs } = volume.spec;
|
||||
|
||||
return (
|
||||
|
||||
@ -31,8 +31,9 @@ import { Link } from "react-router-dom";
|
||||
import { cronJobStore } from "./cronjob.store";
|
||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import type { CronJob, Job } from "../../../common/k8s-api/endpoints";
|
||||
import { CronJob, Job } from "../../../common/k8s-api/endpoints";
|
||||
import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<CronJob> {
|
||||
}
|
||||
@ -46,18 +47,27 @@ export class CronJobDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: cronJob } = this.props;
|
||||
|
||||
if (!cronJob) return null;
|
||||
if (!cronJob) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(cronJob instanceof CronJob)) {
|
||||
logger.error("[CronJobDetails]: passed object that is not an instanceof CronJob", cronJob);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const childJobs = jobStore.getJobsByOwner(cronJob);
|
||||
|
||||
return (
|
||||
<div className="CronJobDetails">
|
||||
<KubeObjectMeta object={cronJob}/>
|
||||
<DrawerItem name="Schedule">
|
||||
{cronJob.isNeverRun() ? (
|
||||
<>
|
||||
never ({cronJob.getSchedule()})
|
||||
</>
|
||||
) : cronJob.getSchedule()}
|
||||
{
|
||||
cronJob.isNeverRun()
|
||||
? `never (${cronJob.getSchedule()})`
|
||||
: cronJob.getSchedule()
|
||||
}
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Active">
|
||||
{cronJobStore.getActiveJobsNum(cronJob)}
|
||||
|
||||
@ -40,6 +40,7 @@ import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<DaemonSet> {
|
||||
}
|
||||
@ -72,7 +73,16 @@ export class DaemonSetDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: daemonSet } = this.props;
|
||||
|
||||
if (!daemonSet) return null;
|
||||
if (!daemonSet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(daemonSet instanceof DaemonSet)) {
|
||||
logger.error("[DaemonSetDetails]: passed object that is not an instanceof DaemonSet", daemonSet);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { spec } = daemonSet;
|
||||
const selectors = daemonSet.getSelectors();
|
||||
const images = daemonSet.getImages();
|
||||
|
||||
@ -42,6 +42,7 @@ import { DeploymentReplicaSets } from "./deployment-replicasets";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Deployment> {
|
||||
}
|
||||
@ -75,7 +76,16 @@ export class DeploymentDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: deployment } = this.props;
|
||||
|
||||
if (!deployment) return null;
|
||||
if (!deployment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(deployment instanceof Deployment)) {
|
||||
logger.error("[DeploymentDetails]: passed object that is not an instanceof Deployment", deployment);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { status, spec } = deployment;
|
||||
const nodeSelector = deployment.getNodeSelectors();
|
||||
const selectors = deployment.getSelectors();
|
||||
|
||||
@ -44,6 +44,7 @@ import { ResourceMetrics } from "../resource-metrics";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Job> {
|
||||
}
|
||||
@ -71,7 +72,16 @@ export class JobDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: job } = this.props;
|
||||
|
||||
if (!job) return null;
|
||||
if (!job) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(job instanceof Job)) {
|
||||
logger.error("[JobDetails]: passed object that is not an instanceof Job", job);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectors = job.getSelectors();
|
||||
const nodeSelector = job.getNodeSelectors();
|
||||
const images = job.getImages();
|
||||
|
||||
@ -43,6 +43,7 @@ import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Pod> {
|
||||
}
|
||||
@ -77,7 +78,16 @@ export class PodDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: pod } = this.props;
|
||||
|
||||
if (!pod) return null;
|
||||
if (!pod) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(pod instanceof Pod)) {
|
||||
logger.error("[PodDetails]: passed object that is not an instanceof Pod", pod);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { status, spec } = pod;
|
||||
const { conditions, podIP } = status;
|
||||
const podIPs = pod.getIPs();
|
||||
|
||||
@ -39,6 +39,7 @@ import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<ReplicaSet> {
|
||||
}
|
||||
@ -71,7 +72,16 @@ export class ReplicaSetDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: replicaSet } = this.props;
|
||||
|
||||
if (!replicaSet) return null;
|
||||
if (!replicaSet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(replicaSet instanceof ReplicaSet)) {
|
||||
logger.error("[ReplicaSetDetails]: passed object that is not an instanceof ReplicaSet", replicaSet);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const { metrics } = this;
|
||||
const { status } = replicaSet;
|
||||
const { availableReplicas, replicas } = status;
|
||||
|
||||
@ -40,6 +40,7 @@ import { KubeObjectMeta } from "../kube-object-meta";
|
||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||
import { boundMethod } from "../../utils";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<StatefulSet> {
|
||||
}
|
||||
@ -72,7 +73,16 @@ export class StatefulSetDetails extends React.Component<Props> {
|
||||
render() {
|
||||
const { object: statefulSet } = this.props;
|
||||
|
||||
if (!statefulSet) return null;
|
||||
if (!statefulSet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(statefulSet instanceof StatefulSet)) {
|
||||
logger.error("[StatefulSetDetails]: passed object that is not an instanceof StatefulSet", statefulSet);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const images = statefulSet.getImages();
|
||||
const selectors = statefulSet.getSelectors();
|
||||
const nodeSelector = statefulSet.getNodeSelectors();
|
||||
|
||||
@ -90,7 +90,7 @@ export class ClusterStatus extends React.Component<Props> {
|
||||
params: {
|
||||
entityId: this.props.clusterId,
|
||||
},
|
||||
fragment: "Proxy",
|
||||
fragment: "proxy",
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@ -20,13 +20,14 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import type { KubeMetaField, KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { KubeMetaField, KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
||||
import { Link } from "react-router-dom";
|
||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||
import { LocaleDate } from "../locale-date";
|
||||
import { getDetailsUrl } from "../kube-detail-params";
|
||||
import logger from "../../../common/logger";
|
||||
|
||||
export interface KubeObjectMetaProps {
|
||||
object: KubeObject;
|
||||
@ -46,6 +47,17 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
|
||||
|
||||
render() {
|
||||
const { object } = this.props;
|
||||
|
||||
if (!object) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(object instanceof KubeObject)) {
|
||||
logger.error("[KubeObjectMeta]: passed object that is not an instanceof KubeObject", object);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
getNs, getLabels, getResourceVersion, selfLink, getAnnotations,
|
||||
getFinalizers, getId, getAge, getName, metadata: { creationTimestamp },
|
||||
|
||||
@ -23,7 +23,7 @@ import "./setting-layout.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { boundMethod, cssNames, IClassName } from "../../utils";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { navigation } from "../../navigation";
|
||||
import { Icon } from "../icon";
|
||||
|
||||
@ -36,17 +36,10 @@ export interface SettingLayoutProps extends React.DOMAttributes<any> {
|
||||
back?: (evt: React.MouseEvent | KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
function scrollToAnchor() {
|
||||
const { hash } = window.location;
|
||||
|
||||
if (hash) {
|
||||
document.querySelector(`${hash}`).scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
const defaultProps: Partial<SettingLayoutProps> = {
|
||||
provideBackButtonNavigation: true,
|
||||
contentGaps: true,
|
||||
back: () => navigation.goBack(),
|
||||
};
|
||||
|
||||
/**
|
||||
@ -56,19 +49,14 @@ const defaultProps: Partial<SettingLayoutProps> = {
|
||||
export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
static defaultProps = defaultProps as object;
|
||||
|
||||
@boundMethod
|
||||
back(evt?: React.MouseEvent | KeyboardEvent) {
|
||||
if (this.props.back) {
|
||||
this.props.back(evt);
|
||||
} else {
|
||||
navigation.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
window.addEventListener("keydown", this.onEscapeKey);
|
||||
const { hash } = window.location;
|
||||
|
||||
scrollToAnchor();
|
||||
if (hash) {
|
||||
document.querySelector(hash)?.scrollIntoView();
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", this.onEscapeKey);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -82,7 +70,7 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
|
||||
if (evt.code === "Escape") {
|
||||
evt.stopPropagation();
|
||||
this.back(evt);
|
||||
this.props.back(evt);
|
||||
}
|
||||
};
|
||||
|
||||
@ -107,17 +95,18 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
|
||||
{children}
|
||||
</div>
|
||||
<div className="toolsRegion">
|
||||
{ this.props.provideBackButtonNavigation && (
|
||||
<div className="fixedTools">
|
||||
<div className="closeBtn" role="button" aria-label="Close" onClick={this.back}>
|
||||
<Icon material="close"/>
|
||||
{
|
||||
this.props.provideBackButtonNavigation && (
|
||||
<div className="fixedTools">
|
||||
<div className="closeBtn" role="button" aria-label="Close" onClick={back}>
|
||||
<Icon material="close" />
|
||||
</div>
|
||||
<div className="esc" aria-hidden="true">
|
||||
ESC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="esc" aria-hidden="true">
|
||||
ESC
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -77,16 +77,15 @@ function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, update
|
||||
);
|
||||
}
|
||||
|
||||
const listNamespacesForbiddenHandlerDisplayedAt = new Map<string, number>();
|
||||
const notificationLastDisplayedAt = new Map<string, number>();
|
||||
const intervalBetweenNotifications = 1000 * 60; // 60s
|
||||
|
||||
function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: ListNamespaceForbiddenArgs): void {
|
||||
const lastDisplayedAt = listNamespacesForbiddenHandlerDisplayedAt.get(clusterId);
|
||||
const wasDisplayed = Boolean(lastDisplayedAt);
|
||||
const lastDisplayedAt = notificationLastDisplayedAt.get(clusterId);
|
||||
const now = Date.now();
|
||||
|
||||
if (!wasDisplayed || (now - lastDisplayedAt) > intervalBetweenNotifications) {
|
||||
listNamespacesForbiddenHandlerDisplayedAt.set(clusterId, now);
|
||||
if (!notificationLastDisplayedAt.has(clusterId) || (now - lastDisplayedAt) > intervalBetweenNotifications) {
|
||||
notificationLastDisplayedAt.set(clusterId, now);
|
||||
} else {
|
||||
// don't bother the user too often
|
||||
return;
|
||||
@ -94,21 +93,39 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]:
|
||||
|
||||
const notificationId = `list-namespaces-forbidden:${clusterId}`;
|
||||
|
||||
if (notificationsStore.getById(notificationId)) {
|
||||
// notification is still visible
|
||||
return;
|
||||
}
|
||||
|
||||
Notifications.info(
|
||||
(
|
||||
<div className="flex column gaps">
|
||||
<b>Add Accessible Namespaces</b>
|
||||
<p>Cluster <b>{ClusterStore.getInstance().getById(clusterId).name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
|
||||
<p>
|
||||
Cluster <b>{ClusterStore.getInstance().getById(clusterId).name}</b> does not have permissions to list namespaces.{" "}
|
||||
Please add the namespaces you have access to.
|
||||
</p>
|
||||
<div className="flex gaps row align-left box grow">
|
||||
<Button active outlined label="Go to Accessible Namespaces Settings" onClick={() => {
|
||||
navigate(entitySettingsURL({ params: { entityId: clusterId }, fragment: "accessible-namespaces" }));
|
||||
notificationsStore.remove(notificationId);
|
||||
}} />
|
||||
<Button
|
||||
active
|
||||
outlined
|
||||
label="Go to Accessible Namespaces Settings"
|
||||
onClick={() => {
|
||||
navigate(entitySettingsURL({ params: { entityId: clusterId }, fragment: "namespaces" }));
|
||||
notificationsStore.remove(notificationId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
id: notificationId,
|
||||
/**
|
||||
* Set the time when the notification is closed as well so that there is at
|
||||
* least a minute between closing the notification as seeing it again
|
||||
*/
|
||||
onClose: () => notificationLastDisplayedAt.set(clusterId, Date.now()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -19,149 +19,109 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { reaction } from "mobx";
|
||||
import { StorageAdapter, StorageHelper } from "../storageHelper";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { StorageHelper } from "../storageHelper";
|
||||
import { delay } from "../../../common/utils/delay";
|
||||
|
||||
describe("renderer/utils/StorageHelper", () => {
|
||||
describe("window.localStorage might be used as StorageAdapter", () => {
|
||||
type StorageModel = string;
|
||||
type StorageModel = {
|
||||
[prop: string]: any /*json-serializable*/;
|
||||
message?: string;
|
||||
description?: any;
|
||||
};
|
||||
|
||||
describe("renderer/utils/StorageHelper", () => {
|
||||
describe("Using custom StorageAdapter", () => {
|
||||
const storageKey = "ui-settings";
|
||||
const remoteStorageMock = observable.map<string, StorageModel>();
|
||||
let storageHelper: StorageHelper<StorageModel>;
|
||||
let storageHelperAsync: StorageHelper<StorageModel>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
remoteStorageMock.set(storageKey, {
|
||||
message: "saved-before", // pretending as previously saved data
|
||||
});
|
||||
|
||||
storageHelper = new StorageHelper<StorageModel>(storageKey, {
|
||||
autoInit: false,
|
||||
storage: localStorage,
|
||||
defaultValue: "test",
|
||||
});
|
||||
});
|
||||
|
||||
it("initialized with default value", async () => {
|
||||
localStorage.setItem(storageKey, "saved"); // pretending it was saved previously
|
||||
|
||||
expect(storageHelper.key).toBe(storageKey);
|
||||
expect(storageHelper.defaultValue).toBe("test");
|
||||
expect(storageHelper.get()).toBe("test");
|
||||
|
||||
storageHelper.init();
|
||||
|
||||
expect(storageHelper.key).toBe(storageKey);
|
||||
expect(storageHelper.defaultValue).toBe("test");
|
||||
expect(storageHelper.get()).toBe("saved");
|
||||
});
|
||||
|
||||
it("updates storage", async () => {
|
||||
storageHelper.init();
|
||||
|
||||
storageHelper.set("test2");
|
||||
expect(localStorage.getItem(storageKey)).toBe("test2");
|
||||
|
||||
localStorage.setItem(storageKey, "test3");
|
||||
storageHelper.init({ force: true }); // reload from underlying storage and merge
|
||||
expect(storageHelper.get()).toBe("test3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Using custom StorageAdapter", () => {
|
||||
type SettingsStorageModel = {
|
||||
[key: string]: any;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const storageKey = "mySettings";
|
||||
const storageMock: Record<string, any> = {};
|
||||
let storageHelper: StorageHelper<SettingsStorageModel>;
|
||||
let storageHelperAsync: StorageHelper<SettingsStorageModel>;
|
||||
let storageAdapter: StorageAdapter<SettingsStorageModel>;
|
||||
|
||||
const storageHelperDefaultValue: SettingsStorageModel = {
|
||||
message: "hello-world",
|
||||
anyOtherStorableData: 123,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
storageMock[storageKey] = {
|
||||
message: "saved-before",
|
||||
} as SettingsStorageModel;
|
||||
|
||||
storageAdapter = {
|
||||
onChange: jest.fn(),
|
||||
getItem: jest.fn((key: string) => {
|
||||
return storageMock[key];
|
||||
}),
|
||||
setItem: jest.fn((key: string, value: any) => {
|
||||
storageMock[key] = value;
|
||||
}),
|
||||
removeItem: jest.fn((key: string) => {
|
||||
delete storageMock[key];
|
||||
}),
|
||||
};
|
||||
|
||||
storageHelper = new StorageHelper(storageKey, {
|
||||
autoInit: false,
|
||||
defaultValue: storageHelperDefaultValue,
|
||||
storage: storageAdapter,
|
||||
defaultValue: {
|
||||
message: "blabla",
|
||||
description: "default"
|
||||
},
|
||||
storage: {
|
||||
getItem(key: string): StorageModel {
|
||||
return Object.assign(
|
||||
storageHelper.defaultValue,
|
||||
remoteStorageMock.get(key),
|
||||
);
|
||||
},
|
||||
setItem(key: string, value: StorageModel) {
|
||||
remoteStorageMock.set(key, value);
|
||||
},
|
||||
removeItem(key: string) {
|
||||
remoteStorageMock.delete(key);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
storageHelperAsync = new StorageHelper(storageKey, {
|
||||
autoInit: false,
|
||||
defaultValue: storageHelperDefaultValue,
|
||||
defaultValue: storageHelper.defaultValue,
|
||||
storage: {
|
||||
...storageAdapter,
|
||||
async getItem(key: string): Promise<SettingsStorageModel> {
|
||||
...storageHelper.storage,
|
||||
async getItem(key: string): Promise<StorageModel> {
|
||||
await delay(500); // fake loading timeout
|
||||
|
||||
return storageAdapter.getItem(key);
|
||||
return storageHelper.storage.getItem(key);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("loads data from storage with fallback to default-value", () => {
|
||||
expect(storageHelper.get()).toEqual(storageHelperDefaultValue);
|
||||
it("initialized with default value", async () => {
|
||||
storageHelper.init();
|
||||
|
||||
expect(storageHelper.get().message).toBe("saved-before");
|
||||
expect(storageAdapter.getItem).toHaveBeenCalledWith(storageHelper.key);
|
||||
expect(storageHelper.key).toBe(storageKey);
|
||||
expect(storageHelper.get()).toEqual(storageHelper.defaultValue);
|
||||
});
|
||||
|
||||
it("async loading from storage supported too", async () => {
|
||||
expect(storageHelperAsync.initialized).toBeFalsy();
|
||||
storageHelperAsync.init();
|
||||
await delay(300);
|
||||
expect(storageHelperAsync.get()).toEqual(storageHelperDefaultValue);
|
||||
expect(storageHelperAsync.get()).toEqual(storageHelper.defaultValue);
|
||||
await delay(200);
|
||||
expect(storageHelperAsync.get().message).toBe("saved-before");
|
||||
});
|
||||
|
||||
it("set() fully replaces data in storage", () => {
|
||||
storageHelper.init();
|
||||
storageHelper.set({ message: "test2" });
|
||||
expect(storageHelper.get().message).toBe("test2");
|
||||
expect(storageMock[storageKey]).toEqual({ message: "test2" });
|
||||
expect(storageAdapter.setItem).toHaveBeenCalledWith(storageHelper.key, { message: "test2" });
|
||||
storageHelper.set({ message: "msg" });
|
||||
storageHelper.get().description = "desc";
|
||||
expect(storageHelper.get().message).toBe("msg");
|
||||
expect(storageHelper.get().description).toBe("desc");
|
||||
expect(remoteStorageMock.get(storageKey)).toEqual({
|
||||
message: "msg",
|
||||
description: "desc",
|
||||
} as StorageModel);
|
||||
});
|
||||
|
||||
it("merge() does partial data tree updates", () => {
|
||||
storageHelper.init();
|
||||
storageHelper.merge({ message: "updated" });
|
||||
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelperDefaultValue, message: "updated" });
|
||||
expect(storageAdapter.setItem).toHaveBeenCalledWith(storageHelper.key, { ...storageHelperDefaultValue, message: "updated" });
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated" });
|
||||
expect(remoteStorageMock.get(storageKey)).toEqual({ ...storageHelper.defaultValue, message: "updated" });
|
||||
|
||||
// `draft` modified inside, returning `void` is expected
|
||||
storageHelper.merge(draft => {
|
||||
draft.message = "updated2";
|
||||
});
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelperDefaultValue, message: "updated2" });
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated2" });
|
||||
|
||||
// returning object modifies `draft` as well
|
||||
storageHelper.merge(draft => ({
|
||||
message: draft.message.replace("2", "3")
|
||||
}));
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelperDefaultValue, message: "updated3" });
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated3" });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -20,10 +20,9 @@
|
||||
*/
|
||||
|
||||
// Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.)
|
||||
|
||||
import { action, comparer, makeObservable, observable, toJS, when, } from "mobx";
|
||||
import produce, { Draft } from "immer";
|
||||
import { isEqual, isFunction, isPlainObject } from "lodash";
|
||||
import produce, { Draft, isDraft } from "immer";
|
||||
import { isEqual } from "lodash";
|
||||
import logger from "../../main/logger";
|
||||
|
||||
export interface StorageAdapter<T> {
|
||||
@ -150,15 +149,22 @@ export class StorageHelper<T> {
|
||||
|
||||
@action
|
||||
merge(value: Partial<T> | ((draft: Draft<T>) => Partial<T> | void)) {
|
||||
const nextValue = produce(this.toJSON(), (state: Draft<T>) => {
|
||||
const newValue = isFunction(value) ? value(state) : value;
|
||||
const nextValue = produce<T>(this.toJSON(), (draft: Draft<T>) => {
|
||||
|
||||
return isPlainObject(newValue)
|
||||
? Object.assign(state, newValue) // partial updates for returned plain objects
|
||||
: newValue;
|
||||
if (typeof value == "function") {
|
||||
const newValue = value(draft);
|
||||
|
||||
// merge returned plain objects from `value-as-callback` usage
|
||||
// otherwise `draft` can be just modified inside a callback without returning any value (void)
|
||||
if (newValue && !isDraft(newValue)) {
|
||||
Object.assign(draft, newValue);
|
||||
}
|
||||
} else {
|
||||
Object.assign(draft, value);
|
||||
}
|
||||
});
|
||||
|
||||
this.set(nextValue as T);
|
||||
this.set(nextValue);
|
||||
}
|
||||
|
||||
toJSON(): T {
|
||||
|
||||
14
templates/create-resource/Pod.yaml
Normal file
14
templates/create-resource/Pod.yaml
Normal file
@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: static-web
|
||||
labels:
|
||||
role: myrole
|
||||
spec:
|
||||
containers:
|
||||
- name: web
|
||||
image: nginx
|
||||
ports:
|
||||
- name: web
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
61
yarn.lock
61
yarn.lock
@ -2078,10 +2078,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.0.tgz#5cb0dff2a5f616fc8e0c61b482bf01fa20a03cec"
|
||||
integrity sha512-ZAbqul7QAKpM2h1PFGa5ETN27ulmqtj0QviYHasw9LffvXZvVHuraOx/FOsIPPDNGZN0Qo1nASxxSfMYOtSoCw==
|
||||
|
||||
"@types/url-parse@^1.4.3":
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.3.tgz#fba49d90f834951cb000a674efee3d6f20968329"
|
||||
integrity sha512-4kHAkbV/OfW2kb5BLVUuUMoumB3CP8rHqlw48aHvFy5tf9ER0AfOonBlX29l/DD68G70DmyhRlSYfQPSYpC5Vw==
|
||||
"@types/url-parse@^1.4.4":
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.4.tgz#ebeb0ec8b581318739cf73e9f9b186f610764255"
|
||||
integrity sha512-KtQLad12+4T/NfSxpoDhmr22+fig3T7/08QCgmutYA6QSznSRmEtuL95GrhVV40/0otTEdFc+etRcCTqhh1q5Q==
|
||||
|
||||
"@types/uuid@^8.3.1":
|
||||
version "8.3.1"
|
||||
@ -4136,10 +4136,10 @@ concat-stream@^1.5.0, concat-stream@^1.6.2:
|
||||
readable-stream "^2.2.2"
|
||||
typedarray "^0.0.6"
|
||||
|
||||
concurrently@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.2.0.tgz#ead55121d08a0fc817085584c123cedec2e08975"
|
||||
integrity sha512-XxcDbQ4/43d6CxR7+iV8IZXhur4KbmEJk1CetVMUqCy34z9l0DkszbY+/9wvmSnToTej0SYomc2WSRH+L0zVJw==
|
||||
concurrently@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-5.3.0.tgz#7500de6410d043c912b2da27de3202cb489b1e7b"
|
||||
integrity sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==
|
||||
dependencies:
|
||||
chalk "^2.4.2"
|
||||
date-fns "^2.0.1"
|
||||
@ -7278,10 +7278,10 @@ immediate@~3.0.5:
|
||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
|
||||
|
||||
immer@^8.0.4:
|
||||
version "8.0.4"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.4.tgz#3a21605a4e2dded852fb2afd208ad50969737b7a"
|
||||
integrity sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ==
|
||||
immer@^9.0.6:
|
||||
version "9.0.6"
|
||||
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73"
|
||||
integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ==
|
||||
|
||||
import-cwd@^3.0.0:
|
||||
version "3.0.0"
|
||||
@ -9504,12 +9504,7 @@ mime@1.6.0, mime@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
mime@^2.4.4:
|
||||
version "2.4.6"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
|
||||
integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==
|
||||
|
||||
mime@^2.4.6, mime@^2.5.2:
|
||||
mime@^2.4.4, mime@^2.4.6, mime@^2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
|
||||
integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
|
||||
@ -11002,10 +10997,10 @@ pkg-up@^3.1.0:
|
||||
dependencies:
|
||||
find-up "^3.0.0"
|
||||
|
||||
playwright@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.14.0.tgz#18301b11f5278a446d36b5cf96f67db36ce2cd20"
|
||||
integrity sha512-aR5oZ1iVsjQkGfYCjgYAmyMAVu0MQ0i8MgdnfdqDu9EVLfbnpuuFmTv/Rb7/Yjno1kOrDUP9+RyNC+zfG3wozA==
|
||||
playwright@^1.15.2:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.15.2.tgz#b350056d1fffbe5de5b1bdaca6ab73e35758baad"
|
||||
integrity sha512-+Z+7ckihyxR6rK5q8DWC6eUbKARfXpyxpjNcoJfgwSr64lAOzjhyFQiPC/JkdIqhsLgZjxpWfl1S7fLb+wPkgA==
|
||||
dependencies:
|
||||
commander "^6.1.0"
|
||||
debug "^4.1.1"
|
||||
@ -11225,11 +11220,6 @@ prepend-http@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
prettier@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
|
||||
integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
|
||||
|
||||
pretty-error@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
|
||||
@ -12339,10 +12329,10 @@ sass-loader@^8.0.2:
|
||||
schema-utils "^2.6.1"
|
||||
semver "^6.3.0"
|
||||
|
||||
sass@^1.32.13, sass@^1.41.1:
|
||||
version "1.41.1"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.41.1.tgz#bca5bed2154192779c29f48fca9c644c60c38d98"
|
||||
integrity sha512-vIjX7izRxw3Wsiez7SX7D+j76v7tenfO18P59nonjr/nzCkZuoHuF7I/Fo0ZRZPKr88v29ivIdE9BqGDgQD/Nw==
|
||||
sass@^1.32.13, sass@^1.43.2:
|
||||
version "1.43.2"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.2.tgz#c02501520c624ad6622529a8b3724eb08da82d65"
|
||||
integrity sha512-DncYhjl3wBaPMMJR0kIUaH3sF536rVrOcqqVGmTZHQRRzj7LQlyGV7Mb8aCKFyILMr5VsPHwRYtyKpnKYlmQSQ==
|
||||
dependencies:
|
||||
chokidar ">=3.0.0 <4.0.0"
|
||||
|
||||
@ -12956,14 +12946,7 @@ stack-trace@0.0.x:
|
||||
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
|
||||
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
|
||||
|
||||
stack-utils@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.2.tgz#5cf48b4557becb4638d0bc4f21d23f5d19586593"
|
||||
integrity sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg==
|
||||
dependencies:
|
||||
escape-string-regexp "^2.0.0"
|
||||
|
||||
stack-utils@^2.0.3:
|
||||
stack-utils@^2.0.2, stack-utils@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277"
|
||||
integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user