mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' of github.com:lensapp/lens into feature/protocol-handler
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
commit
4ad4e47006
7
.github/release-drafter.yml
vendored
7
.github/release-drafter.yml
vendored
@ -12,6 +12,7 @@ categories:
|
|||||||
- 'chore'
|
- 'chore'
|
||||||
- 'area/ci'
|
- 'area/ci'
|
||||||
- 'area/tests'
|
- 'area/tests'
|
||||||
|
- 'dependencies'
|
||||||
|
|
||||||
template: |
|
template: |
|
||||||
## Changes since $PREVIOUS_TAG
|
## Changes since $PREVIOUS_TAG
|
||||||
@ -20,8 +21,10 @@ template: |
|
|||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
- [Lens v$RESOLVED_VERSION - Linux](https://snapcraft.io/kontena-lens)
|
- Lens v$RESOLVED_VERSION - Linux
|
||||||
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.AppImage)
|
- [AppImage](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.AppImage)
|
||||||
|
- [DEB](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.amd64.deb)
|
||||||
|
- [RPM](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.x86_64.rpm)
|
||||||
- [Snapcraft](https://snapcraft.io/kontena-lens)
|
- [Snapcraft](https://snapcraft.io/kontena-lens)
|
||||||
- [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg)
|
- [Lens v$RESOLVED_VERSION - MacOS](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-$RESOLVED_VERSION.dmg)
|
||||||
- [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe)
|
- [Lens v$RESOLVED_VERSION - Windows](https://github.com/lensapp/lens/releases/download/v$RESOLVED_VERSION/Lens-Setup-$RESOLVED_VERSION.exe)
|
||||||
|
|||||||
11
package.json
11
package.json
@ -2,7 +2,7 @@
|
|||||||
"name": "kontena-lens",
|
"name": "kontena-lens",
|
||||||
"productName": "Lens",
|
"productName": "Lens",
|
||||||
"description": "Lens - The Kubernetes IDE",
|
"description": "Lens - The Kubernetes IDE",
|
||||||
"version": "4.1.0-alpha.1",
|
"version": "4.1.0-alpha.2",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2020, Mirantis, Inc.",
|
"copyright": "© 2020, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -103,7 +103,6 @@
|
|||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
"executableName": "lens",
|
|
||||||
"artifactName": "${productName}-${version}.${arch}.${ext}",
|
"artifactName": "${productName}-${version}.${arch}.${ext}",
|
||||||
"target": [
|
"target": [
|
||||||
"deb",
|
"deb",
|
||||||
@ -162,6 +161,9 @@
|
|||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true
|
"allowToChangeInstallationDirectory": true
|
||||||
},
|
},
|
||||||
|
"snap": {
|
||||||
|
"confinement": "classic"
|
||||||
|
},
|
||||||
"publish": [
|
"publish": [
|
||||||
{
|
{
|
||||||
"provider": "github",
|
"provider": "github",
|
||||||
@ -169,9 +171,6 @@
|
|||||||
"owner": "lensapp"
|
"owner": "lensapp"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"snap": {
|
|
||||||
"confinement": "classic"
|
|
||||||
},
|
|
||||||
"protocols": {
|
"protocols": {
|
||||||
"name": "Lens Protocol Handler",
|
"name": "Lens Protocol Handler",
|
||||||
"schemes": [
|
"schemes": [
|
||||||
@ -302,7 +301,7 @@
|
|||||||
"@types/webpack-dev-server": "^3.11.1",
|
"@types/webpack-dev-server": "^3.11.1",
|
||||||
"@types/webpack-env": "^1.15.2",
|
"@types/webpack-env": "^1.15.2",
|
||||||
"@types/webpack-node-externals": "^1.7.1",
|
"@types/webpack-node-externals": "^1.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.12.0",
|
"@typescript-eslint/eslint-plugin": "^4.14.2",
|
||||||
"@typescript-eslint/parser": "^4.0.0",
|
"@typescript-eslint/parser": "^4.0.0",
|
||||||
"ace-builds": "^1.4.11",
|
"ace-builds": "^1.4.11",
|
||||||
"ansi_up": "^4.0.4",
|
"ansi_up": "^4.0.4",
|
||||||
|
|||||||
@ -126,6 +126,7 @@ describe("create clusters", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
||||||
|
jest.spyOn(Cluster.prototype, "canUseWatchApi").mockReturnValue(Promise.resolve(true));
|
||||||
jest.spyOn(Cluster.prototype, "canI")
|
jest.spyOn(Cluster.prototype, "canI")
|
||||||
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||||
expect(attr.namespace).toBe("default");
|
expect(attr.namespace).toBe("default");
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export interface ClusterState {
|
|||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
allowedNamespaces: string[]
|
allowedNamespaces: string[]
|
||||||
allowedResources: string[]
|
allowedResources: string[]
|
||||||
|
isGlobalWatchEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,7 +92,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@observable initializing = false;
|
@observable initializing = false;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is cluster object initialized
|
* Is cluster object initialized
|
||||||
*
|
*
|
||||||
@ -177,6 +177,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable isAdmin = false;
|
@observable isAdmin = false;
|
||||||
|
/**
|
||||||
|
* Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
@observable isGlobalWatchEnabled = false;
|
||||||
/**
|
/**
|
||||||
* Preferences
|
* Preferences
|
||||||
*
|
*
|
||||||
@ -353,9 +359,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
await this.refreshAllowedResources();
|
await this.refreshAccessibility();
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
|
||||||
this.ready = true;
|
|
||||||
this.ensureKubectl();
|
this.ensureKubectl();
|
||||||
}
|
}
|
||||||
this.activated = true;
|
this.activated = true;
|
||||||
@ -410,13 +414,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
await this.refreshAccessibility();
|
||||||
await this.refreshAllowedResources();
|
|
||||||
|
|
||||||
if (opts.refreshMetadata) {
|
if (opts.refreshMetadata) {
|
||||||
this.refreshMetadata();
|
this.refreshMetadata();
|
||||||
}
|
}
|
||||||
this.ready = true;
|
|
||||||
}
|
}
|
||||||
this.pushState();
|
this.pushState();
|
||||||
}
|
}
|
||||||
@ -433,6 +435,18 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.metadata = Object.assign(existingMetadata, metadata);
|
this.metadata = Object.assign(existingMetadata, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
private async refreshAccessibility(): Promise<void> {
|
||||||
|
this.isAdmin = await this.isClusterAdmin();
|
||||||
|
this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" });
|
||||||
|
|
||||||
|
await this.refreshAllowedResources();
|
||||||
|
|
||||||
|
this.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@ -571,6 +585,17 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise<boolean> {
|
||||||
|
return this.canI({
|
||||||
|
verb: "watch",
|
||||||
|
resource: "*",
|
||||||
|
...customizeResource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toJSON(): ClusterModel {
|
toJSON(): ClusterModel {
|
||||||
const model: ClusterModel = {
|
const model: ClusterModel = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
@ -604,6 +629,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
isAdmin: this.isAdmin,
|
isAdmin: this.isAdmin,
|
||||||
allowedNamespaces: this.allowedNamespaces,
|
allowedNamespaces: this.allowedNamespaces,
|
||||||
allowedResources: this.allowedResources,
|
allowedResources: this.allowedResources,
|
||||||
|
isGlobalWatchEnabled: this.isGlobalWatchEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(state, {
|
return toJS(state, {
|
||||||
|
|||||||
@ -62,16 +62,6 @@ function buildTray(icon: string | NativeImage, menu: Menu, windowManager: Window
|
|||||||
|
|
||||||
function createTrayMenu(windowManager: WindowManager): Menu {
|
function createTrayMenu(windowManager: WindowManager): Menu {
|
||||||
return Menu.buildFromTemplate([
|
return Menu.buildFromTemplate([
|
||||||
{
|
|
||||||
label: "About Lens",
|
|
||||||
async click() {
|
|
||||||
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
|
||||||
const browserWindow = await windowManager.ensureMainWindow();
|
|
||||||
|
|
||||||
showAbout(browserWindow);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
{
|
||||||
label: "Open Lens",
|
label: "Open Lens",
|
||||||
async click() {
|
async click() {
|
||||||
@ -124,6 +114,15 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "About Lens",
|
||||||
|
async click() {
|
||||||
|
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
||||||
|
const browserWindow = await windowManager.ensureMainWindow();
|
||||||
|
|
||||||
|
showAbout(browserWindow);
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: "Quit App",
|
label: "Quit App",
|
||||||
|
|||||||
@ -5,12 +5,11 @@ import type { Cluster } from "../../main/cluster";
|
|||||||
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
|
||||||
import type { KubeObject } from "./kube-object";
|
import type { KubeObject } from "./kube-object";
|
||||||
import type { KubeObjectStore } from "../kube-object.store";
|
import type { KubeObjectStore } from "../kube-object.store";
|
||||||
import type { NamespaceStore } from "../components/+namespaces/namespace.store";
|
|
||||||
|
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import { comparer, computed, observable, reaction } from "mobx";
|
import { autorun, comparer, computed, IReactionDisposer, observable, reaction } from "mobx";
|
||||||
import { autobind, EventEmitter } from "../utils";
|
import { autobind, EventEmitter, noop } from "../utils";
|
||||||
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
|
||||||
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
|
||||||
import { apiPrefix, isDebugging, isProduction } from "../../common/vars";
|
import { apiPrefix, isDebugging, isProduction } from "../../common/vars";
|
||||||
@ -19,6 +18,7 @@ import { apiManager } from "./api-manager";
|
|||||||
export { IKubeWatchEvent, IKubeWatchEventStreamEnd };
|
export { IKubeWatchEvent, IKubeWatchEventStreamEnd };
|
||||||
|
|
||||||
export interface IKubeWatchMessage<T extends KubeObject = any> {
|
export interface IKubeWatchMessage<T extends KubeObject = any> {
|
||||||
|
namespace?: string;
|
||||||
data?: IKubeWatchEvent<KubeJsonApiData>
|
data?: IKubeWatchEvent<KubeJsonApiData>
|
||||||
error?: IKubeWatchEvent<KubeJsonApiError>;
|
error?: IKubeWatchEvent<KubeJsonApiError>;
|
||||||
api?: KubeApi<T>;
|
api?: KubeApi<T>;
|
||||||
@ -28,7 +28,7 @@ export interface IKubeWatchMessage<T extends KubeObject = any> {
|
|||||||
export interface IKubeWatchSubscribeStoreOptions {
|
export interface IKubeWatchSubscribeStoreOptions {
|
||||||
preload?: boolean; // preload store items, default: true
|
preload?: boolean; // preload store items, default: true
|
||||||
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
|
waitUntilLoaded?: boolean; // subscribe only after loading all stores, default: true
|
||||||
cacheLoading?: boolean; // when enabled loading store will be skipped, default: false
|
loadOnce?: boolean; // check store.isLoaded to skip loading if done already, default: false
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IKubeWatchReconnectOptions {
|
export interface IKubeWatchReconnectOptions {
|
||||||
@ -43,50 +43,49 @@ export interface IKubeWatchLog {
|
|||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class KubeWatchApi {
|
export class KubeWatchApi {
|
||||||
private cluster: Cluster;
|
|
||||||
private namespaceStore: NamespaceStore;
|
|
||||||
|
|
||||||
private requestId = 0;
|
private requestId = 0;
|
||||||
private isConnected = false;
|
|
||||||
private reader: ReadableStreamReader<string>;
|
private reader: ReadableStreamReader<string>;
|
||||||
private subscribers = observable.map<KubeApi, number>();
|
|
||||||
|
|
||||||
// events
|
|
||||||
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
|
||||||
|
|
||||||
|
@observable.ref private cluster: Cluster;
|
||||||
|
@observable.ref private namespaces: string[] = [];
|
||||||
|
@observable subscribers = observable.map<KubeApi, number>();
|
||||||
|
@observable isConnected = false;
|
||||||
|
|
||||||
|
@computed get isReady(): boolean {
|
||||||
|
return Boolean(this.cluster && this.namespaces);
|
||||||
|
}
|
||||||
|
|
||||||
@computed get isActive(): boolean {
|
@computed get isActive(): boolean {
|
||||||
return this.apis.length > 0;
|
return this.apis.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get apis(): string[] {
|
@computed get apis(): string[] {
|
||||||
const { cluster, namespaceStore } = this;
|
if (!this.isReady) {
|
||||||
const activeApis = Array.from(this.subscribers.keys());
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return activeApis.map(api => {
|
return Array.from(this.subscribers.keys()).map(api => {
|
||||||
if (!cluster.isAllowedResource(api.kind)) {
|
if (!this.isAllowedApi(api)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (api.isNamespaced) {
|
if (api.isNamespaced && !this.cluster.isGlobalWatchEnabled) {
|
||||||
return namespaceStore.getContextNamespaces().map(namespace => api.getWatchUrl(namespace));
|
return this.namespaces.map(namespace => api.getWatchUrl(namespace));
|
||||||
} else {
|
|
||||||
return api.getWatchUrl();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return api.getWatchUrl();
|
||||||
}).flat();
|
}).flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
async init({ getCluster, getNamespaces }: {
|
||||||
this.init();
|
getCluster: () => Cluster,
|
||||||
}
|
getNamespaces: () => string[],
|
||||||
|
}): Promise<void> {
|
||||||
private async init() {
|
autorun(() => {
|
||||||
const { getHostedCluster } = await import("../../common/cluster-store");
|
this.cluster = getCluster();
|
||||||
const { namespaceStore } = await import("../components/+namespaces/namespace.store");
|
this.namespaces = getNamespaces();
|
||||||
|
});
|
||||||
await namespaceStore.whenReady;
|
|
||||||
|
|
||||||
this.cluster = getHostedCluster();
|
|
||||||
this.namespaceStore = namespaceStore;
|
|
||||||
this.bindAutoConnect();
|
this.bindAutoConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +107,7 @@ export class KubeWatchApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isAllowedApi(api: KubeApi): boolean {
|
isAllowedApi(api: KubeApi): boolean {
|
||||||
return !!this?.cluster.isAllowedResource(api.kind);
|
return Boolean(this?.cluster.isAllowedResource(api.kind));
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeApi(api: KubeApi | KubeApi[]): () => void {
|
subscribeApi(api: KubeApi | KubeApi[]): () => void {
|
||||||
@ -129,45 +128,66 @@ export class KubeWatchApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeStores(stores: KubeObjectStore[], options: IKubeWatchSubscribeStoreOptions = {}): () => void {
|
preloadStores(stores: KubeObjectStore[], { loadOnce = false } = {}) {
|
||||||
const { preload = true, waitUntilLoaded = true, cacheLoading = false } = options;
|
|
||||||
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
|
||||||
const preloading: Promise<any>[] = [];
|
const preloading: Promise<any>[] = [];
|
||||||
|
|
||||||
|
for (const store of stores) {
|
||||||
|
preloading.push(limitRequests(async () => {
|
||||||
|
if (store.isLoaded && loadOnce) return; // skip
|
||||||
|
|
||||||
|
return store.loadAll(this.namespaces);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading: Promise.allSettled(preloading),
|
||||||
|
cancelLoading: () => limitRequests.clearQueue(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeStores(stores: KubeObjectStore[], options: IKubeWatchSubscribeStoreOptions = {}): () => void {
|
||||||
|
const { preload = true, waitUntilLoaded = true, loadOnce = false } = options;
|
||||||
const apis = new Set(stores.map(store => store.getSubscribeApis()).flat());
|
const apis = new Set(stores.map(store => store.getSubscribeApis()).flat());
|
||||||
const unsubscribeList: (() => void)[] = [];
|
const unsubscribeList: (() => void)[] = [];
|
||||||
let isUnsubscribed = false;
|
let isUnsubscribed = false;
|
||||||
|
|
||||||
|
const load = () => this.preloadStores(stores, { loadOnce });
|
||||||
|
let preloading = preload && load();
|
||||||
|
let cancelReloading: IReactionDisposer = noop;
|
||||||
|
|
||||||
const subscribe = () => {
|
const subscribe = () => {
|
||||||
if (isUnsubscribed) return;
|
if (isUnsubscribed) return;
|
||||||
apis.forEach(api => unsubscribeList.push(this.subscribeApi(api)));
|
apis.forEach(api => unsubscribeList.push(this.subscribeApi(api)));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (preload) {
|
if (preloading) {
|
||||||
for (const store of stores) {
|
if (waitUntilLoaded) {
|
||||||
preloading.push(limitRequests(async () => {
|
preloading.loading.then(subscribe, error => {
|
||||||
if (cacheLoading && store.isLoaded) return; // skip
|
this.log({
|
||||||
|
message: new Error("Loading stores has failed"),
|
||||||
return store.loadAll();
|
meta: { stores, error, options },
|
||||||
}));
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (waitUntilLoaded) {
|
|
||||||
Promise.all(preloading).then(subscribe, error => {
|
|
||||||
this.log({
|
|
||||||
message: new Error("Loading stores has failed"),
|
|
||||||
meta: { stores, error, options },
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload when context namespaces changes
|
||||||
|
cancelReloading = reaction(() => this.namespaces, () => {
|
||||||
|
preloading?.cancelLoading();
|
||||||
|
preloading = load();
|
||||||
|
}, {
|
||||||
|
equals: comparer.shallow,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
subscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unsubscribe
|
// unsubscribe
|
||||||
return () => {
|
return () => {
|
||||||
if (isUnsubscribed) return;
|
if (isUnsubscribed) return;
|
||||||
isUnsubscribed = true;
|
isUnsubscribed = true;
|
||||||
limitRequests.clearQueue();
|
cancelReloading();
|
||||||
|
preloading?.cancelLoading();
|
||||||
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
unsubscribeList.forEach(unsubscribe => unsubscribe());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -254,6 +274,10 @@ export class KubeWatchApi {
|
|||||||
const kubeEvent: IKubeWatchEvent = JSON.parse(json);
|
const kubeEvent: IKubeWatchEvent = JSON.parse(json);
|
||||||
const message = this.getMessage(kubeEvent);
|
const message = this.getMessage(kubeEvent);
|
||||||
|
|
||||||
|
if (!this.namespaces.includes(message.namespace)) {
|
||||||
|
continue; // skip updates from non-watching resources context
|
||||||
|
}
|
||||||
|
|
||||||
this.onMessage.emit(message);
|
this.onMessage.emit(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return json;
|
return json;
|
||||||
@ -286,6 +310,7 @@ export class KubeWatchApi {
|
|||||||
|
|
||||||
message.api = api;
|
message.api = api;
|
||||||
message.store = apiManager.getStore(api);
|
message.store = apiManager.getStore(api);
|
||||||
|
message.namespace = namespace;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,11 +58,11 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll() {
|
async loadAll(namespaces = namespaceStore.allowedNamespaces) {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const items = await this.loadItems(namespaceStore.getContextNamespaces());
|
const items = await this.loadItems(namespaces);
|
||||||
|
|
||||||
this.items.replace(this.sortItems(items));
|
this.items.replace(this.sortItems(items));
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
@ -73,6 +73,10 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadSelectedNamespaces(): Promise<void> {
|
||||||
|
return this.loadAll(namespaceStore.getContextNamespaces());
|
||||||
|
}
|
||||||
|
|
||||||
async loadItems(namespaces: string[]) {
|
async loadItems(namespaces: string[]) {
|
||||||
return Promise
|
return Promise
|
||||||
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
|
||||||
@ -82,7 +86,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async create(payload: IReleaseCreatePayload) {
|
async create(payload: IReleaseCreatePayload) {
|
||||||
const response = await helmReleasesApi.create(payload);
|
const response = await helmReleasesApi.create(payload);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadSelectedNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -90,7 +94,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
||||||
const response = await helmReleasesApi.update(name, namespace, payload);
|
const response = await helmReleasesApi.update(name, namespace, payload);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadSelectedNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -98,7 +102,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
async rollback(name: string, namespace: string, revision: number) {
|
async rollback(name: string, namespace: string, revision: number) {
|
||||||
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
const response = await helmReleasesApi.rollback(name, namespace, revision);
|
||||||
|
|
||||||
if (this.isLoaded) this.loadAll();
|
if (this.isLoaded) this.loadSelectedNamespaces();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
const { store } = this;
|
const { store } = this;
|
||||||
|
|
||||||
if (store && !store.isLoading && !store.isLoaded) {
|
if (store && !store.isLoading && !store.isLoaded) {
|
||||||
store.loadAll();
|
store.loadSelectedNamespaces();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export interface KubeEventDetailsProps {
|
|||||||
@observer
|
@observer
|
||||||
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
eventStore.loadAll();
|
eventStore.loadSelectedNamespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -32,8 +32,8 @@ export class NamespaceDetails extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
resourceQuotaStore.loadAll();
|
resourceQuotaStore.loadSelectedNamespaces();
|
||||||
limitRangeStore.loadAll();
|
limitRangeStore.loadSelectedNamespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -13,17 +13,14 @@ import { kubeWatchApi } from "../../api/kube-watch-api";
|
|||||||
|
|
||||||
interface Props extends SelectProps {
|
interface Props extends SelectProps {
|
||||||
showIcons?: boolean;
|
showIcons?: boolean;
|
||||||
showClusterOption?: boolean; // show cluster option on the top (default: false)
|
showClusterOption?: boolean; // show "Cluster" option on the top (default: false)
|
||||||
clusterOptionLabel?: React.ReactNode; // label for cluster option (default: "Cluster")
|
showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
|
||||||
customizeOptions?(nsOptions: SelectOption[]): SelectOption[];
|
customizeOptions?(options: SelectOption[]): SelectOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultProps: Partial<Props> = {
|
const defaultProps: Partial<Props> = {
|
||||||
showIcons: true,
|
showIcons: true,
|
||||||
showClusterOption: false,
|
showClusterOption: false,
|
||||||
get clusterOptionLabel() {
|
|
||||||
return `Cluster`;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -39,13 +36,17 @@ export class NamespaceSelect extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get options(): SelectOption[] {
|
@computed get options(): SelectOption[] {
|
||||||
const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props;
|
const { customizeOptions, showClusterOption, showAllNamespacesOption } = this.props;
|
||||||
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
||||||
|
|
||||||
options = customizeOptions ? customizeOptions(options) : options;
|
if (showAllNamespacesOption) {
|
||||||
|
options.unshift({ label: "All Namespaces", value: "" });
|
||||||
|
} else if (showClusterOption) {
|
||||||
|
options.unshift({ label: "Cluster", value: "" });
|
||||||
|
}
|
||||||
|
|
||||||
if (showClusterOption) {
|
if (customizeOptions) {
|
||||||
options.unshift({ value: null, label: clusterOptionLabel });
|
options = customizeOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
@ -64,7 +65,7 @@ export class NamespaceSelect extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props;
|
const { className, showIcons, customizeOptions, ...selectProps } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
@ -80,32 +81,56 @@ export class NamespaceSelect extends React.Component<Props> {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class NamespaceSelectFilter extends React.Component {
|
export class NamespaceSelectFilter extends React.Component {
|
||||||
|
@computed get placeholder(): React.ReactNode {
|
||||||
|
const namespaces = namespaceStore.getContextNamespaces();
|
||||||
|
|
||||||
|
switch (namespaces.length) {
|
||||||
|
case namespaceStore.allowedNamespaces.length:
|
||||||
|
return <>All namespaces</>;
|
||||||
|
case 0:
|
||||||
|
return <>Select a namespace</>;
|
||||||
|
case 1:
|
||||||
|
return <>Namespace: {namespaces[0]}</>;
|
||||||
|
default:
|
||||||
|
return <>Namespaces: {namespaces.join(", ")}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatOptionLabel = ({ value: namespace, label }: SelectOption) => {
|
||||||
|
if (namespace) {
|
||||||
|
const isSelected = namespaceStore.hasContext(namespace);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gaps align-center">
|
||||||
|
<FilterIcon type={FilterType.NAMESPACE}/>
|
||||||
|
<span>{namespace}</span>
|
||||||
|
{isSelected && <Icon small material="check" className="box right"/>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = ([{ value: namespace }]: SelectOption[]) => {
|
||||||
|
if (namespace) {
|
||||||
|
namespaceStore.toggleContext(namespace);
|
||||||
|
} else {
|
||||||
|
namespaceStore.toggleAll(); // "All namespaces" option clicked
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { contextNs, hasContext, toggleContext } = namespaceStore;
|
|
||||||
let placeholder = <>All namespaces</>;
|
|
||||||
|
|
||||||
if (contextNs.length == 1) placeholder = <>Namespace: {contextNs[0]}</>;
|
|
||||||
if (contextNs.length >= 2) placeholder = <>Namespaces: {contextNs.join(", ")}</>;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NamespaceSelect
|
<NamespaceSelect
|
||||||
placeholder={placeholder}
|
isMulti={true}
|
||||||
|
showAllNamespacesOption={true}
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
isOptionSelected={() => false}
|
isOptionSelected={() => false}
|
||||||
controlShouldRenderValue={false}
|
controlShouldRenderValue={false}
|
||||||
isMulti
|
placeholder={this.placeholder}
|
||||||
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
|
onChange={this.onChange}
|
||||||
formatOptionLabel={({ value: namespace }: SelectOption) => {
|
formatOptionLabel={this.formatOptionLabel}
|
||||||
const isSelected = hasContext(namespace);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gaps align-center">
|
|
||||||
<FilterIcon type={FilterType.NAMESPACE}/>
|
|
||||||
<span>{namespace}</span>
|
|
||||||
{isSelected && <Icon small material="check" className="box right"/>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
|
import { action, comparer, computed, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
|
||||||
import { autobind, createStorage } from "../../utils";
|
import { autobind, createStorage } from "../../utils";
|
||||||
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
|
||||||
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
|
||||||
@ -6,7 +6,7 @@ import { createPageParam } from "../../navigation";
|
|||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
|
||||||
|
|
||||||
const storage = createStorage<string[]>("context_namespaces");
|
const storage = createStorage<string[]>("context_namespaces", []);
|
||||||
|
|
||||||
export const namespaceUrlParam = createPageParam<string[]>({
|
export const namespaceUrlParam = createPageParam<string[]>({
|
||||||
name: "namespaces",
|
name: "namespaces",
|
||||||
@ -34,7 +34,7 @@ export function getDummyNamespace(name: string) {
|
|||||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||||
api = namespacesApi;
|
api = namespacesApi;
|
||||||
|
|
||||||
@observable contextNs = observable.array<string>();
|
@observable private contextNs = observable.set<string>();
|
||||||
@observable isReady = false;
|
@observable isReady = false;
|
||||||
|
|
||||||
whenReady = when(() => this.isReady);
|
whenReady = when(() => this.isReady);
|
||||||
@ -57,7 +57,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
|
||||||
return reaction(() => this.contextNs.toJS(), callback, {
|
return reaction(() => Array.from(this.contextNs), callback, {
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
...opts,
|
...opts,
|
||||||
});
|
});
|
||||||
@ -73,48 +73,38 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
private autoLoadAllowedNamespaces(): IReactionDisposer {
|
||||||
return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
|
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll(namespaces), {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
equals: comparer.shallow,
|
equals: comparer.shallow,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get allowedNamespaces(): string[] {
|
@computed get allowedNamespaces(): string[] {
|
||||||
return toJS(getHostedCluster().allowedNamespaces);
|
return toJS(getHostedCluster().allowedNamespaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
private get initialNamespaces(): string[] {
|
private get initialNamespaces(): string[] {
|
||||||
const allowed = new Set(this.allowedNamespaces);
|
const namespaces = new Set(this.allowedNamespaces);
|
||||||
const prevSelected = storage.get();
|
const prevSelected = storage.get().filter(namespace => namespaces.has(namespace));
|
||||||
|
|
||||||
if (Array.isArray(prevSelected)) {
|
// return previously saved namespaces from local-storage
|
||||||
return prevSelected.filter(namespace => allowed.has(namespace));
|
if (prevSelected.length > 0) {
|
||||||
|
return prevSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise select "default" or first allowed namespace
|
// otherwise select "default" or first allowed namespace
|
||||||
if (allowed.has("default")) {
|
if (namespaces.has("default")) {
|
||||||
return ["default"];
|
return ["default"];
|
||||||
} else if (allowed.size) {
|
} else if (namespaces.size) {
|
||||||
return [Array.from(allowed)[0]];
|
return [Array.from(namespaces)[0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getContextNamespaces(): string[] {
|
public getContextNamespaces(): string[] {
|
||||||
const namespaces = this.contextNs.toJS();
|
return Array.from(this.contextNs);
|
||||||
|
|
||||||
// show all namespaces when nothing selected
|
|
||||||
if (!namespaces.length) {
|
|
||||||
if (this.isLoaded) {
|
|
||||||
// return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale
|
|
||||||
return this.items.map(namespace => namespace.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.allowedNamespaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
return namespaces;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubscribeApis() {
|
getSubscribeApis() {
|
||||||
@ -143,26 +133,46 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setContext(namespaces: string[]) {
|
setContext(namespace: string | string[]) {
|
||||||
|
const namespaces = [namespace].flat();
|
||||||
|
|
||||||
this.contextNs.replace(namespaces);
|
this.contextNs.replace(namespaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasContext(namespace: string | string[]) {
|
hasContext(namespaces: string | string[]) {
|
||||||
const context = Array.isArray(namespace) ? namespace : [namespace];
|
return [namespaces].flat().every(namespace => this.contextNs.has(namespace));
|
||||||
|
}
|
||||||
|
|
||||||
return context.every(namespace => this.contextNs.includes(namespace));
|
@computed get hasAllContexts(): boolean {
|
||||||
|
return this.contextNs.size === this.allowedNamespaces.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleContext(namespace: string) {
|
toggleContext(namespace: string) {
|
||||||
if (this.hasContext(namespace)) this.contextNs.remove(namespace);
|
if (this.hasContext(namespace)) {
|
||||||
else this.contextNs.push(namespace);
|
this.contextNs.delete(namespace);
|
||||||
|
} else {
|
||||||
|
this.contextNs.add(namespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleAll(showAll?: boolean) {
|
||||||
|
if (typeof showAll === "boolean") {
|
||||||
|
if (showAll) {
|
||||||
|
this.setContext(this.allowedNamespaces);
|
||||||
|
} else {
|
||||||
|
this.contextNs.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toggleAll(!this.hasAllContexts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async remove(item: Namespace) {
|
async remove(item: Namespace) {
|
||||||
await super.remove(item);
|
await super.remove(item);
|
||||||
this.contextNs.remove(item.getName());
|
this.contextNs.delete(item.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,9 +29,7 @@ export class NodeDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
await Promise.all(stores.map(store => store.loadAll()));
|
await Promise.all(stores.map(store => store.loadSelectedNamespaces()));
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,9 +20,7 @@ interface Props extends KubeObjectDetailsProps<CronJob> {
|
|||||||
@observer
|
@observer
|
||||||
export class CronJobDetails extends React.Component<Props> {
|
export class CronJobDetails extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!jobStore.isLoaded) {
|
jobStore.loadSelectedNamespaces();
|
||||||
jobStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -30,9 +30,7 @@ export class DaemonSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -31,9 +31,7 @@ export class DeploymentDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -25,9 +25,7 @@ interface Props extends KubeObjectDetailsProps<Job> {
|
|||||||
@observer
|
@observer
|
||||||
export class JobDetails extends React.Component<Props> {
|
export class JobDetails extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -29,9 +29,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -30,9 +30,7 @@ export class StatefulSetDetails extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!podsStore.isLoaded) {
|
podsStore.loadSelectedNamespaces();
|
||||||
podsStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { computed, observable, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Redirect, Route, Router, Switch } from "react-router";
|
import { Redirect, Route, Router, Switch } from "react-router";
|
||||||
import { history } from "../navigation";
|
import { history } from "../navigation";
|
||||||
@ -42,7 +43,7 @@ import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../exte
|
|||||||
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||||
import { eventStore } from "./+events/event.store";
|
import { eventStore } from "./+events/event.store";
|
||||||
import { computed, reaction, observable } from "mobx";
|
import { namespaceStore } from "./+namespaces/namespace.store";
|
||||||
import { nodesStore } from "./+nodes/nodes.store";
|
import { nodesStore } from "./+nodes/nodes.store";
|
||||||
import { podsStore } from "./+workloads-pods/pods.store";
|
import { podsStore } from "./+workloads-pods/pods.store";
|
||||||
import { kubeWatchApi } from "../api/kube-watch-api";
|
import { kubeWatchApi } from "../api/kube-watch-api";
|
||||||
@ -74,6 +75,12 @@ export class App extends React.Component {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
whatInput.ask(); // Start to monitor user input device
|
whatInput.ask(); // Start to monitor user input device
|
||||||
|
|
||||||
|
await namespaceStore.whenReady;
|
||||||
|
await kubeWatchApi.init({
|
||||||
|
getCluster: getHostedCluster,
|
||||||
|
getNamespaces: namespaceStore.getContextNamespaces,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|||||||
@ -80,7 +80,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
|||||||
const values = this.values.getData(tabId);
|
const values = this.values.getData(tabId);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
!releaseStore.isLoaded && releaseStore.loadAll(),
|
!releaseStore.isLoaded && releaseStore.loadSelectedNamespaces(),
|
||||||
!values && this.loadValues(tabId)
|
!values && this.loadValues(tabId)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,7 +138,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
const { store, dependentStores } = this.props;
|
const { store, dependentStores } = this.props;
|
||||||
const stores = Array.from(new Set([store, ...dependentStores]));
|
const stores = Array.from(new Set([store, ...dependentStores]));
|
||||||
|
|
||||||
stores.forEach(store => store.loadAll());
|
stores.forEach(store => store.loadAll(namespaceStore.getContextNamespaces()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
private filterCallbacks: { [type: string]: ItemsFilter } = {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export class PageFiltersStore {
|
|||||||
protected syncWithContextNamespace() {
|
protected syncWithContextNamespace() {
|
||||||
const disposers = [
|
const disposers = [
|
||||||
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
|
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
|
||||||
if (filteredNs.length !== namespaceStore.contextNs.length) {
|
if (filteredNs.length !== namespaceStore.getContextNamespaces().length) {
|
||||||
namespaceStore.setContext(filteredNs);
|
namespaceStore.setContext(filteredNs);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -40,9 +40,7 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class Sidebar extends React.Component<Props> {
|
export class Sidebar extends React.Component<Props> {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
if (!crdStore.isLoaded && isAllowedResource("customresourcedefinitions")) {
|
crdStore.loadSelectedNamespaces();
|
||||||
crdStore.loadAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCustomResources() {
|
renderCustomResources() {
|
||||||
|
|||||||
@ -106,17 +106,18 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) {
|
async loadAll(namespaces: string[] = []): Promise<void> {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!contextNamespaces) {
|
if (!namespaces.length) {
|
||||||
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
||||||
|
|
||||||
contextNamespaces = namespaceStore.getContextNamespaces();
|
// load all available namespaces by default
|
||||||
|
namespaces.push(...namespaceStore.allowedNamespaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api });
|
let items = await this.loadItems({ namespaces, api: this.api });
|
||||||
|
|
||||||
items = this.filterItemsOnLoad(items);
|
items = this.filterItemsOnLoad(items);
|
||||||
items = this.sortItems(items);
|
items = this.sortItems(items);
|
||||||
@ -131,6 +132,12 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadSelectedNamespaces(): Promise<void> {
|
||||||
|
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
|
||||||
|
|
||||||
|
return this.loadAll(namespaceStore.getContextNamespaces());
|
||||||
|
}
|
||||||
|
|
||||||
protected resetOnError(error: any) {
|
protected resetOnError(error: any) {
|
||||||
if (error) this.reset();
|
if (error) this.reset();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
||||||
|
|
||||||
## 4.1.0-alpha.1 (current version)
|
## 4.1.0-alpha.2 (current version)
|
||||||
|
|
||||||
- Change: list views default to a namespace (insted of listing resources from all namespaces)
|
- Change: list views default to a namespace (insted of listing resources from all namespaces)
|
||||||
|
- Command palette
|
||||||
- Generic logs view with Pod selector
|
- Generic logs view with Pod selector
|
||||||
- Possibility to add custom Helm repository through Lens
|
- Possibility to add custom Helm repository through Lens
|
||||||
- Possibility to change visibility of Pod list columns
|
- Possibility to change visibility of common resource list columns
|
||||||
- Suspend / resume buttons for CronJobs
|
- Suspend / resume buttons for CronJobs
|
||||||
|
- Allow namespace to specified on role creation
|
||||||
|
- Allow for changing installation directory on Windows
|
||||||
- Dock tabs context menu
|
- Dock tabs context menu
|
||||||
- Display node column in Pod list
|
- Display node column in Pod list
|
||||||
- Unify age column output with kubectl
|
- Unify age column output with kubectl
|
||||||
@ -16,6 +19,8 @@ Here you can find description of changes we've built into each release. While we
|
|||||||
- Improve Pod tolerations layout
|
- Improve Pod tolerations layout
|
||||||
- Lens metrics: scrape only lens-metrics namespace
|
- Lens metrics: scrape only lens-metrics namespace
|
||||||
- Lens metrics: Prometheus v2.19.3
|
- Lens metrics: Prometheus v2.19.3
|
||||||
|
- Update bundled kubectl to v1.18.15
|
||||||
|
- Improve how watch requests are handled
|
||||||
- Export PodDetailsList component to extension API
|
- Export PodDetailsList component to extension API
|
||||||
- Export Wizard components to extension API
|
- Export Wizard components to extension API
|
||||||
- Export NamespaceSelect component to extension API
|
- Export NamespaceSelect component to extension API
|
||||||
|
|||||||
76
yarn.lock
76
yarn.lock
@ -1843,28 +1843,29 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@^4.12.0", "@typescript-eslint/eslint-plugin@^4.5.0":
|
"@typescript-eslint/eslint-plugin@^4.14.2", "@typescript-eslint/eslint-plugin@^4.5.0":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.12.0.tgz#00d1b23b40b58031e6d7c04a5bc6c1a30a2e834a"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.2.tgz#47a15803cfab89580b96933d348c2721f3d2f6fe"
|
||||||
integrity sha512-wHKj6q8s70sO5i39H2g1gtpCXCvjVszzj6FFygneNFyIAxRvNSVz9GML7XpqrB9t7hNutXw+MHnLN/Ih6uyB8Q==
|
integrity sha512-uMGfG7GFYK/nYutK/iqYJv6K/Xuog/vrRRZX9aEP4Zv1jsYXuvFUMDFLhUnc8WFv3D2R5QhNQL3VYKmvLS5zsQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/experimental-utils" "4.12.0"
|
"@typescript-eslint/experimental-utils" "4.14.2"
|
||||||
"@typescript-eslint/scope-manager" "4.12.0"
|
"@typescript-eslint/scope-manager" "4.14.2"
|
||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
functional-red-black-tree "^1.0.1"
|
functional-red-black-tree "^1.0.1"
|
||||||
|
lodash "^4.17.15"
|
||||||
regexpp "^3.0.0"
|
regexpp "^3.0.0"
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
tsutils "^3.17.1"
|
tsutils "^3.17.1"
|
||||||
|
|
||||||
"@typescript-eslint/experimental-utils@4.12.0":
|
"@typescript-eslint/experimental-utils@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.12.0.tgz#372838e76db76c9a56959217b768a19f7129546b"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.2.tgz#9df35049d1d36b6cbaba534d703648b9e1f05cbb"
|
||||||
integrity sha512-MpXZXUAvHt99c9ScXijx7i061o5HEjXltO+sbYfZAAHxv3XankQkPaNi5myy0Yh0Tyea3Hdq1pi7Vsh0GJb0fA==
|
integrity sha512-mV9pmET4C2y2WlyHmD+Iun8SAEqkLahHGBkGqDVslHkmoj3VnxnGP4ANlwuxxfq1BsKdl/MPieDbohCEQgKrwA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/json-schema" "^7.0.3"
|
"@types/json-schema" "^7.0.3"
|
||||||
"@typescript-eslint/scope-manager" "4.12.0"
|
"@typescript-eslint/scope-manager" "4.14.2"
|
||||||
"@typescript-eslint/types" "4.12.0"
|
"@typescript-eslint/types" "4.14.2"
|
||||||
"@typescript-eslint/typescript-estree" "4.12.0"
|
"@typescript-eslint/typescript-estree" "4.14.2"
|
||||||
eslint-scope "^5.0.0"
|
eslint-scope "^5.0.0"
|
||||||
eslint-utils "^2.0.0"
|
eslint-utils "^2.0.0"
|
||||||
|
|
||||||
@ -1878,13 +1879,13 @@
|
|||||||
"@typescript-eslint/typescript-estree" "4.8.2"
|
"@typescript-eslint/typescript-estree" "4.8.2"
|
||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@4.12.0":
|
"@typescript-eslint/scope-manager@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.12.0.tgz#beeb8beca895a07b10c593185a5612f1085ef279"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.2.tgz#64cbc9ca64b60069aae0c060b2bf81163243b266"
|
||||||
integrity sha512-QVf9oCSVLte/8jvOsxmgBdOaoe2J0wtEmBr13Yz0rkBNkl5D8bfnf6G4Vhox9qqMIoG7QQoVwd2eG9DM/ge4Qg==
|
integrity sha512-cuV9wMrzKm6yIuV48aTPfIeqErt5xceTheAgk70N1V4/2Ecj+fhl34iro/vIssJlb7XtzcaD07hWk7Jk0nKghg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "4.12.0"
|
"@typescript-eslint/types" "4.14.2"
|
||||||
"@typescript-eslint/visitor-keys" "4.12.0"
|
"@typescript-eslint/visitor-keys" "4.14.2"
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager@4.8.2":
|
"@typescript-eslint/scope-manager@4.8.2":
|
||||||
version "4.8.2"
|
version "4.8.2"
|
||||||
@ -1894,23 +1895,23 @@
|
|||||||
"@typescript-eslint/types" "4.8.2"
|
"@typescript-eslint/types" "4.8.2"
|
||||||
"@typescript-eslint/visitor-keys" "4.8.2"
|
"@typescript-eslint/visitor-keys" "4.8.2"
|
||||||
|
|
||||||
"@typescript-eslint/types@4.12.0":
|
"@typescript-eslint/types@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.12.0.tgz#fb891fe7ccc9ea8b2bbd2780e36da45d0dc055e5"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.2.tgz#d96da62be22dc9dc6a06647f3633815350fb3174"
|
||||||
integrity sha512-N2RhGeheVLGtyy+CxRmxdsniB7sMSCfsnbh8K/+RUIXYYq3Ub5+sukRCjVE80QerrUBvuEvs4fDhz5AW/pcL6g==
|
integrity sha512-LltxawRW6wXy4Gck6ZKlBD05tCHQUj4KLn4iR69IyRiDHX3d3NCAhO+ix5OR2Q+q9bjCrHE/HKt+riZkd1At8Q==
|
||||||
|
|
||||||
"@typescript-eslint/types@4.8.2":
|
"@typescript-eslint/types@4.8.2":
|
||||||
version "4.8.2"
|
version "4.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.8.2.tgz#c862dd0e569d9478eb82d6aee662ea53f5661a36"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.8.2.tgz#c862dd0e569d9478eb82d6aee662ea53f5661a36"
|
||||||
integrity sha512-z1/AVcVF8ju5ObaHe2fOpZYEQrwHyZ7PTOlmjd3EoFeX9sv7UekQhfrCmgUO7PruLNfSHrJGQvrW3Q7xQ8EoAw==
|
integrity sha512-z1/AVcVF8ju5ObaHe2fOpZYEQrwHyZ7PTOlmjd3EoFeX9sv7UekQhfrCmgUO7PruLNfSHrJGQvrW3Q7xQ8EoAw==
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@4.12.0":
|
"@typescript-eslint/typescript-estree@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.12.0.tgz#3963418c850f564bdab3882ae23795d115d6d32e"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.2.tgz#9c5ebd8cae4d7b014f890acd81e8e17f309c9df9"
|
||||||
integrity sha512-gZkFcmmp/CnzqD2RKMich2/FjBTsYopjiwJCroxqHZIY11IIoN0l5lKqcgoAPKHt33H2mAkSfvzj8i44Jm7F4w==
|
integrity sha512-ESiFl8afXxt1dNj8ENEZT12p+jl9PqRur+Y19m0Z/SPikGL6rqq4e7Me60SU9a2M28uz48/8yct97VQYaGl0Vg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "4.12.0"
|
"@typescript-eslint/types" "4.14.2"
|
||||||
"@typescript-eslint/visitor-keys" "4.12.0"
|
"@typescript-eslint/visitor-keys" "4.14.2"
|
||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
globby "^11.0.1"
|
globby "^11.0.1"
|
||||||
is-glob "^4.0.1"
|
is-glob "^4.0.1"
|
||||||
@ -1932,12 +1933,12 @@
|
|||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
tsutils "^3.17.1"
|
tsutils "^3.17.1"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@4.12.0":
|
"@typescript-eslint/visitor-keys@4.14.2":
|
||||||
version "4.12.0"
|
version "4.14.2"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.12.0.tgz#a470a79be6958075fa91c725371a83baf428a67a"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.2.tgz#997cbe2cb0690e1f384a833f64794e98727c70c6"
|
||||||
integrity sha512-hVpsLARbDh4B9TKYz5cLbcdMIOAoBYgFPCSP9FFS/liSF+b33gVNq8JHY3QGhHNVz85hObvL7BEYLlgx553WCw==
|
integrity sha512-KBB+xLBxnBdTENs/rUgeUKO0UkPBRs2vD09oMRRIkj5BEN8PX1ToXV532desXfpQnZsYTyLLviS7JrPhdL154w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/types" "4.12.0"
|
"@typescript-eslint/types" "4.14.2"
|
||||||
eslint-visitor-keys "^2.0.0"
|
eslint-visitor-keys "^2.0.0"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@4.8.2":
|
"@typescript-eslint/visitor-keys@4.8.2":
|
||||||
@ -8671,12 +8672,7 @@ lodash.without@~4.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
|
resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
|
||||||
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
|
integrity sha1-PNRXSgC2e643OpS3SHcmQFB7eqw=
|
||||||
|
|
||||||
lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10:
|
lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.3.0, lodash@^4.8.0, lodash@~4.17.10:
|
||||||
version "4.17.15"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
|
||||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
|
||||||
|
|
||||||
lodash@^4.17.19:
|
|
||||||
version "4.17.20"
|
version "4.17.20"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user