mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into feature/port-forward-custom-port-input
This commit is contained in:
commit
394ca5432d
2
Makefile
2
Makefile
@ -60,6 +60,8 @@ build: node_modules binaries/client
|
|||||||
$(MAKE) build-extensions -B
|
$(MAKE) build-extensions -B
|
||||||
yarn run compile
|
yarn run compile
|
||||||
ifeq "$(DETECTED_OS)" "Windows"
|
ifeq "$(DETECTED_OS)" "Windows"
|
||||||
|
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
||||||
|
rm -rf node_modules/win-ca/pem
|
||||||
yarn run electron-builder --publish onTag --x64 --ia32
|
yarn run electron-builder --publish onTag --x64 --ia32
|
||||||
else
|
else
|
||||||
yarn run electron-builder --publish onTag
|
yarn run electron-builder --publish onTag
|
||||||
|
|||||||
@ -6,7 +6,7 @@ In this section you will learn how this extension works under the hood.
|
|||||||
The Hello World sample extension does three things:
|
The Hello World sample extension does three things:
|
||||||
|
|
||||||
- Implements `onActivate()` and outputs a message to the console.
|
- Implements `onActivate()` and outputs a message to the console.
|
||||||
- Implements `onDectivate()` and outputs a message to the console.
|
- Implements `onDeactivate()` and outputs a message to the console.
|
||||||
- Registers `ClusterPage` so that the page is visible in the left-side menu of the cluster dashboard.
|
- Registers `ClusterPage` so that the page is visible in the left-side menu of the cluster dashboard.
|
||||||
|
|
||||||
Let's take a closer look at our Hello World sample's source code and see how these three things are achieved.
|
Let's take a closer look at our Hello World sample's source code and see how these three things are achieved.
|
||||||
|
|||||||
@ -72,4 +72,4 @@ To dive deeper, consider looking at [Common Capabilities](../capabilities/common
|
|||||||
If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues).
|
If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues).
|
||||||
You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing).
|
You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing).
|
||||||
|
|
||||||
The Generator source code is hosted at [Github](https://github.com/lensapp/generator-lens-ext).
|
The Generator source code is hosted at [GitHub](https://github.com/lensapp/generator-lens-ext).
|
||||||
|
|||||||
@ -213,7 +213,7 @@ export class CertificatePage extends React.Component<{ extension: LensRendererEx
|
|||||||
return (
|
return (
|
||||||
<TabLayout>
|
<TabLayout>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="Certicates" store={certificatesStore}
|
className="Certificates" store={certificatesStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
||||||
[sortBy.namespace]: (certificate: Certificate) => certificate.metadata.namespace,
|
[sortBy.namespace]: (certificate: Certificate) => certificate.metadata.namespace,
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
|||||||
```
|
```
|
||||||
|
|
||||||
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
||||||
This page would be defined in your extension's `Renderer.LensExtension` implmentation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
||||||
|
|
||||||
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ This is the cluster that the resource stack will be applied to, and the construc
|
|||||||
Similarly, `ExampleClusterFeature` implements an `uninstall()` method which simply invokes the `kubectlDeleteFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
Similarly, `ExampleClusterFeature` implements an `uninstall()` method which simply invokes the `kubectlDeleteFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
||||||
`kubectlDeleteFolder()` tries to delete from the cluster all kubernetes resources found in the folder passed to it, again in this case `../resources`.
|
`kubectlDeleteFolder()` tries to delete from the cluster all kubernetes resources found in the folder passed to it, again in this case `../resources`.
|
||||||
|
|
||||||
`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utiliize the kubernetes api to inspect the resource stack status.
|
`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utilize the kubernetes api to inspect the resource stack status.
|
||||||
`isInstalled()` simply tries to find a pod named `example-pod`, as a way to determine if the pod is already installed.
|
`isInstalled()` simply tries to find a pod named `example-pod`, as a way to determine if the pod is already installed.
|
||||||
This method can be useful in creating a context-sensitive UI for installing/uninstalling the feature, as demonstrated in the next sample code.
|
This method can be useful in creating a context-sensitive UI for installing/uninstalling the feature, as demonstrated in the next sample code.
|
||||||
|
|
||||||
|
|||||||
@ -148,8 +148,8 @@ export class ExamplePreferenceInput extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="I understand appPreferences"
|
label="I understand appPreferences"
|
||||||
value={ExamplePreferencesStore.getInstace().enabled}
|
value={ExamplePreferencesStore.getInstance().enabled}
|
||||||
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
onChange={v => { ExamplePreferencesStore.getInstance().enabled = v; }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,4 +43,4 @@ Say you have your project folder at `~/my-extension/` and you want to create an
|
|||||||
npm pack
|
npm pack
|
||||||
```
|
```
|
||||||
|
|
||||||
This will create a NPM tarball that can be hosted on Github Releases or any other publicly available file hosting service.
|
This will create a NPM tarball that can be hosted on GitHub Releases or any other publicly available file hosting service.
|
||||||
|
|||||||
18
package.json
18
package.json
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.0.3-beta.1",
|
"version": "5.1.2",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -63,8 +63,7 @@
|
|||||||
},
|
},
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
|
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
|
||||||
"^@lingui/macro$": "<rootDir>/__mocks__/@linguiMacro.ts"
|
|
||||||
},
|
},
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
"<rootDir>/dist",
|
"<rootDir>/dist",
|
||||||
@ -201,7 +200,6 @@
|
|||||||
"electron-updater": "^4.3.1",
|
"electron-updater": "^4.3.1",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"filehound": "^1.17.4",
|
"filehound": "^1.17.4",
|
||||||
"filenamify": "^4.1.0",
|
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
@ -218,7 +216,7 @@
|
|||||||
"mobx": "^6.3.0",
|
"mobx": "^6.3.0",
|
||||||
"mobx-observable-history": "^2.0.1",
|
"mobx-observable-history": "^2.0.1",
|
||||||
"mobx-react": "^7.1.0",
|
"mobx-react": "^7.1.0",
|
||||||
"mock-fs": "^4.12.0",
|
"mock-fs": "^4.14.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"moment-timezone": "^0.5.33",
|
"moment-timezone": "^0.5.33",
|
||||||
"node-pty": "^0.9.0",
|
"node-pty": "^0.9.0",
|
||||||
@ -279,7 +277,7 @@
|
|||||||
"@types/marked": "^2.0.3",
|
"@types/marked": "^2.0.3",
|
||||||
"@types/md5-file": "^4.0.2",
|
"@types/md5-file": "^4.0.2",
|
||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||||
"@types/mock-fs": "^4.10.0",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/module-alias": "^2.0.0",
|
"@types/module-alias": "^2.0.0",
|
||||||
"@types/node": "12.20",
|
"@types/node": "12.20",
|
||||||
"@types/npm": "^2.0.31",
|
"@types/npm": "^2.0.31",
|
||||||
@ -339,7 +337,7 @@
|
|||||||
"html-webpack-plugin": "^4.5.2",
|
"html-webpack-plugin": "^4.5.2",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"include-media": "^1.4.9",
|
"include-media": "^1.4.9",
|
||||||
"jest": "^26.0.1",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "^2.3.0",
|
"jest-canvas-mock": "^2.3.0",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.16",
|
"jest-mock-extended": "^1.0.16",
|
||||||
@ -355,7 +353,7 @@
|
|||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"progress-bar-webpack-plugin": "^2.1.0",
|
"progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"raw-loader": "^4.0.1",
|
"raw-loader": "^4.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
@ -377,9 +375,9 @@
|
|||||||
"typedoc-plugin-markdown": "^3.9.0",
|
"typedoc-plugin-markdown": "^3.9.0",
|
||||||
"typeface-roboto": "^1.1.13",
|
"typeface-roboto": "^1.1.13",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.3.2",
|
||||||
"typescript-plugin-css-modules": "^3.2.0",
|
"typescript-plugin-css-modules": "^3.4.0",
|
||||||
"url-loader": "^4.1.0",
|
"url-loader": "^4.1.0",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.46.0",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-server": "^3.11.0",
|
"webpack-dev-server": "^3.11.0",
|
||||||
"webpack-node-externals": "^1.7.2",
|
"webpack-node-externals": "^1.7.2",
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class TestCatalogCategory2 extends CatalogCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("CatalogCategoryRegistry", () => {
|
describe("CatalogCategoryRegistry", () => {
|
||||||
it("should remove only the category registered when running the disopser", () => {
|
it("should remove only the category registered when running the disposer", () => {
|
||||||
const registry = new TestCatalogCategoryRegistry();
|
const registry = new TestCatalogCategoryRegistry();
|
||||||
|
|
||||||
expect(registry.items.length).toBe(0);
|
expect(registry.items.length).toBe(0);
|
||||||
|
|||||||
@ -19,10 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Console } from "console";
|
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr);
|
|
||||||
|
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
jest.mock("electron", () => {
|
jest.mock("electron", () => {
|
||||||
@ -37,27 +33,27 @@ jest.mock("electron", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { UserStore } from "../user-store";
|
import { UserStore } from "../user-store";
|
||||||
|
import { Console } from "console";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import electron from "electron";
|
import electron from "electron";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
||||||
import { ThemeStore } from "../../renderer/theme.store";
|
import { ThemeStore } from "../../renderer/theme.store";
|
||||||
|
import type { ClusterStoreModel } from "../cluster-store";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
describe("user store tests", () => {
|
describe("user store tests", () => {
|
||||||
describe("for an empty config", () => {
|
describe("for an empty config", () => {
|
||||||
beforeEachWrapped(() => {
|
beforeEachWrapped(() => {
|
||||||
UserStore.resetInstance();
|
|
||||||
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
||||||
|
|
||||||
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
||||||
|
|
||||||
UserStore.getInstance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
|
UserStore.resetInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows setting and retrieving lastSeenAppVersion", () => {
|
it("allows setting and retrieving lastSeenAppVersion", () => {
|
||||||
@ -99,14 +95,31 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
describe("migrations", () => {
|
describe("migrations", () => {
|
||||||
beforeEachWrapped(() => {
|
beforeEachWrapped(() => {
|
||||||
UserStore.resetInstance();
|
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"config.json": JSON.stringify({
|
"config.json": JSON.stringify({
|
||||||
user: { username: "foobar" },
|
user: { username: "foobar" },
|
||||||
preferences: { colorTheme: "light" },
|
preferences: { colorTheme: "light" },
|
||||||
lastSeenAppVersion: "1.2.3"
|
lastSeenAppVersion: "1.2.3"
|
||||||
})
|
}),
|
||||||
|
"lens-cluster-store.json": JSON.stringify({
|
||||||
|
clusters: [
|
||||||
|
{
|
||||||
|
id: "foobar",
|
||||||
|
kubeConfigPath: "tmp/extension_data/foo/bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "barfoo",
|
||||||
|
kubeConfigPath: "some/other/path",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} as ClusterStoreModel),
|
||||||
|
"extension_data": {},
|
||||||
|
},
|
||||||
|
"some": {
|
||||||
|
"other": {
|
||||||
|
"path": "is file",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,6 +127,7 @@ describe("user store tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
UserStore.resetInstance();
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -122,5 +136,12 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.only("skips clusters for adding to kube-sync with files under extension_data/", () => {
|
||||||
|
const us = UserStore.getInstance();
|
||||||
|
|
||||||
|
expect(us.syncKubeconfigEntries.has("tmp/extension_data/foo/bar")).toBe(false);
|
||||||
|
expect(us.syncKubeconfigEntries.has("some/other/path")).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -55,13 +55,18 @@ export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
||||||
|
distro?: string;
|
||||||
|
kubeVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
|
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
|
||||||
|
|
||||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||||
phase: KubernetesClusterStatusPhase;
|
phase: KubernetesClusterStatusPhase;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||||
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||||
public static readonly kind = "KubernetesCluster";
|
public static readonly kind = "KubernetesCluster";
|
||||||
|
|
||||||
|
|||||||
@ -101,6 +101,8 @@ export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
httpsProxy?: string;
|
httpsProxy?: string;
|
||||||
hiddenMetrics?: string[];
|
hiddenMetrics?: string[];
|
||||||
|
nodeShellImage?: string;
|
||||||
|
imagePullSecret?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterPrometheusPreferences {
|
export interface ClusterPrometheusPreferences {
|
||||||
@ -117,6 +119,8 @@ export interface ClusterPrometheusPreferences {
|
|||||||
|
|
||||||
const initialStates = "cluster:states";
|
const initialStates = "cluster:states";
|
||||||
|
|
||||||
|
export const initialNodeShellImage = "docker.io/alpine:3.13";
|
||||||
|
|
||||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||||
private static StateChannel = "cluster:state";
|
private static StateChannel = "cluster:state";
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export interface HotbarStoreModel {
|
|||||||
activeHotbarId: string;
|
activeHotbarId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultHotbarCells = 12; // Number is choosen to easy hit any item with keyboard
|
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
|
||||||
|
|
||||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||||
@observable hotbars: Hotbar[] = [];
|
@observable hotbars: Hotbar[] = [];
|
||||||
@ -203,7 +203,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remvove all hotbar items that reference the `uid`.
|
* Remove all hotbar items that reference the `uid`.
|
||||||
* @param uid The `EntityId` that each hotbar item refers to
|
* @param uid The `EntityId` that each hotbar item refers to
|
||||||
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -40,14 +40,14 @@ export interface KubeApiResourceData {
|
|||||||
export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
|
export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
|
||||||
"clusterroles": { kind: "ClusterRole", group: "rbac.authorization.k8s.io" },
|
"clusterroles": { kind: "ClusterRole", group: "rbac.authorization.k8s.io" },
|
||||||
"clusterrolebindings": { kind: "ClusterRoleBinding", group: "rbac.authorization.k8s.io" },
|
"clusterrolebindings": { kind: "ClusterRoleBinding", group: "rbac.authorization.k8s.io" },
|
||||||
"configmaps": { kind: "ConfigMap" },
|
"configmaps": { kind: "ConfigMap" }, //empty group means "core"
|
||||||
"cronjobs": { kind: "CronJob", group: "batch" },
|
"cronjobs": { kind: "CronJob", group: "batch" },
|
||||||
"customresourcedefinitions": { kind: "CustomResourceDefinition", group: "apiextensions.k8s.io" },
|
"customresourcedefinitions": { kind: "CustomResourceDefinition", group: "apiextensions.k8s.io" },
|
||||||
"daemonsets": { kind: "DaemonSet", group: "apps" },
|
"daemonsets": { kind: "DaemonSet", group: "apps" },
|
||||||
"deployments": { kind: "Deployment", group: "apps" },
|
"deployments": { kind: "Deployment", group: "apps" },
|
||||||
"endpoints": { kind: "Endpoint" },
|
"endpoints": { kind: "Endpoint" },
|
||||||
"events": { kind: "Event" },
|
"events": { kind: "Event" },
|
||||||
"horizontalpodautoscalers": { kind: "HorizontalPodAutoscaler" },
|
"horizontalpodautoscalers": { kind: "HorizontalPodAutoscaler", group: "autoscaling" },
|
||||||
"ingresses": { kind: "Ingress", group: "networking.k8s.io" },
|
"ingresses": { kind: "Ingress", group: "networking.k8s.io" },
|
||||||
"jobs": { kind: "Job", group: "batch" },
|
"jobs": { kind: "Job", group: "batch" },
|
||||||
"namespaces": { kind: "Namespace" },
|
"namespaces": { kind: "Namespace" },
|
||||||
@ -58,13 +58,13 @@ export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
|
|||||||
"persistentvolumeclaims": { kind: "PersistentVolumeClaim" },
|
"persistentvolumeclaims": { kind: "PersistentVolumeClaim" },
|
||||||
"pods": { kind: "Pod" },
|
"pods": { kind: "Pod" },
|
||||||
"poddisruptionbudgets": { kind: "PodDisruptionBudget", group: "policy" },
|
"poddisruptionbudgets": { kind: "PodDisruptionBudget", group: "policy" },
|
||||||
"podsecuritypolicies": { kind: "PodSecurityPolicy" },
|
"podsecuritypolicies": { kind: "PodSecurityPolicy", group: "policy" },
|
||||||
"resourcequotas": { kind: "ResourceQuota" },
|
"resourcequotas": { kind: "ResourceQuota" },
|
||||||
"replicasets": { kind: "ReplicaSet", group: "apps" },
|
"replicasets": { kind: "ReplicaSet", group: "apps" },
|
||||||
"roles": { kind: "Role", group: "rbac.authorization.k8s.io" },
|
"roles": { kind: "Role", group: "rbac.authorization.k8s.io" },
|
||||||
"rolebindings": { kind: "RoleBinding", group: "rbac.authorization.k8s.io" },
|
"rolebindings": { kind: "RoleBinding", group: "rbac.authorization.k8s.io" },
|
||||||
"secrets": { kind: "Secret" },
|
"secrets": { kind: "Secret" },
|
||||||
"serviceaccounts": { kind: "ServiceAccount", group: "core" },
|
"serviceaccounts": { kind: "ServiceAccount" },
|
||||||
"services": { kind: "Service" },
|
"services": { kind: "Service" },
|
||||||
"statefulsets": { kind: "StatefulSet", group: "apps" },
|
"statefulsets": { kind: "StatefulSet", group: "apps" },
|
||||||
"storageclasses": { kind: "StorageClass", group: "storage.k8s.io" },
|
"storageclasses": { kind: "StorageClass", group: "storage.k8s.io" },
|
||||||
|
|||||||
@ -20,73 +20,58 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CaptureConsole, Dedupe, Offline } from "@sentry/integrations";
|
import { CaptureConsole, Dedupe, Offline } from "@sentry/integrations";
|
||||||
|
import * as Sentry from "@sentry/electron";
|
||||||
import { sentryDsn, isProduction } from "./vars";
|
import { sentryDsn, isProduction } from "./vars";
|
||||||
import { UserStore } from "./user-store";
|
import { UserStore } from "./user-store";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
|
||||||
export let sentryIsInitialized = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function bypasses webpack issues.
|
* "Translate" 'browser' to 'main' as Lens developer more familiar with the term 'main'
|
||||||
*
|
|
||||||
* See: https://docs.sentry.io/platforms/javascript/guides/electron/#webpack-configuration
|
|
||||||
*/
|
*/
|
||||||
async function requireSentry() {
|
function mapProcessName(processType: string) {
|
||||||
switch (process.type) {
|
if (processType === "browser") {
|
||||||
case "browser":
|
return "main";
|
||||||
return import("@sentry/electron/dist/main");
|
|
||||||
case "renderer":
|
|
||||||
return import("@sentry/electron/dist/renderer");
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported process type ${process.type}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return processType;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Sentry for the current process so to send errors for debugging.
|
* Initialize Sentry for the current process so to send errors for debugging.
|
||||||
*/
|
*/
|
||||||
export async function SentryInit(): Promise<void> {
|
export function SentryInit() {
|
||||||
try {
|
const processName = mapProcessName(process.type);
|
||||||
const Sentry = await requireSentry();
|
|
||||||
|
|
||||||
try {
|
Sentry.init({
|
||||||
Sentry.init({
|
beforeSend: (event) => {
|
||||||
beforeSend: event => {
|
// default to false, in case instance of UserStore is not created (yet)
|
||||||
if (UserStore.getInstance().allowErrorReporting) {
|
const allowErrorReporting = UserStore.getInstance(false)?.allowErrorReporting ?? false;
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`🔒 [SENTRY-BEFORE-SEND-HOOK]: allowErrorReporting: false. Sentry event is caught but not sent to server.`);
|
if (allowErrorReporting) {
|
||||||
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === START OF SENTRY EVENT ===");
|
return event;
|
||||||
logger.info(event);
|
}
|
||||||
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === END OF SENTRY EVENT ===");
|
|
||||||
|
|
||||||
// if return null, the event won't be sent
|
logger.info(`🔒 [SENTRY-BEFORE-SEND-HOOK]: allowErrorReporting: ${allowErrorReporting}. Sentry event is caught but not sent to server.`);
|
||||||
// ref https://github.com/getsentry/sentry-javascript/issues/2039
|
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === START OF SENTRY EVENT ===");
|
||||||
return null;
|
logger.info(event);
|
||||||
},
|
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === END OF SENTRY EVENT ===");
|
||||||
dsn: sentryDsn,
|
|
||||||
integrations: [
|
|
||||||
new CaptureConsole({ levels: ["error"] }),
|
|
||||||
new Dedupe(),
|
|
||||||
new Offline()
|
|
||||||
],
|
|
||||||
initialScope: {
|
|
||||||
tags: {
|
|
||||||
// "translate" browser to 'main' as Lens developer more familiar with the term 'main'
|
|
||||||
"process": process.type === "browser" ? "main" : "renderer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
environment: isProduction ? "production" : "development",
|
|
||||||
});
|
|
||||||
|
|
||||||
sentryIsInitialized = true;
|
// if return null, the event won't be sent
|
||||||
|
// ref https://github.com/getsentry/sentry-javascript/issues/2039
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
dsn: sentryDsn,
|
||||||
|
integrations: [
|
||||||
|
new CaptureConsole({ levels: ["error"] }),
|
||||||
|
new Dedupe(),
|
||||||
|
new Offline()
|
||||||
|
],
|
||||||
|
initialScope: {
|
||||||
|
tags: {
|
||||||
|
|
||||||
logger.info(`✔️ [SENTRY-INIT]: Sentry for ${process.type} is initialized.`);
|
"process": processName
|
||||||
} catch (error) {
|
}
|
||||||
logger.warn(`⚠️ [SENTRY-INIT]: Sentry.init() error: ${error?.message ?? error}.`);
|
},
|
||||||
}
|
environment: isProduction ? "production" : "development",
|
||||||
} catch (error) {
|
});
|
||||||
logger.warn(`⚠️ [SENTRY-INIT]: Error loading Sentry module ${error?.message ?? error}.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,5 +33,9 @@ if (isMac) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
try {
|
||||||
|
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[CA]: failed to force load: ${error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -193,9 +193,9 @@ const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string
|
|||||||
toStore(val) {
|
toStore(val) {
|
||||||
const res: [string, string[]][] = [];
|
const res: [string, string[]][] = [];
|
||||||
|
|
||||||
for (const [table, columnes] of val) {
|
for (const [table, columns] of val) {
|
||||||
if (columnes.size) {
|
if (columns.size) {
|
||||||
res.push([table, Array.from(columnes)]);
|
res.push([table, Array.from(columns)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
130
src/common/utils/__tests__/paths.test.ts
Normal file
130
src/common/utils/__tests__/paths.test.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describeIf } from "../../../../integration/helpers/utils";
|
||||||
|
import { isWindows } from "../../vars";
|
||||||
|
import { isLogicalChildPath } from "../paths";
|
||||||
|
|
||||||
|
describe("isLogicalChildPath", () => {
|
||||||
|
describeIf(isWindows)("windows tests", () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:\\Foo\\Bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:\\Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:/Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:/Foo/Bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "D:\\Foo\\Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("test %#", (testData) => {
|
||||||
|
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describeIf(!isWindows)("posix tests", () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foobar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/../bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/./bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/.bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/..bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/...bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/..\\.bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/bar/../foo",
|
||||||
|
testPath: "/foo/bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/\\bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "./bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("test %#", (testData) => {
|
||||||
|
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -27,7 +27,7 @@ export class ExtendedMap<K, V> extends Map<K, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the value behind `key`. If it was not pressent, first insert the value returned by `getVal`
|
* Get the value behind `key`. If it was not present, first insert the value returned by `getVal`
|
||||||
* @param key The key to insert into the map with
|
* @param key The key to insert into the map with
|
||||||
* @param getVal A function that returns a new instance of `V`.
|
* @param getVal A function that returns a new instance of `V`.
|
||||||
* @returns The value in the map
|
* @returns The value in the map
|
||||||
|
|||||||
@ -33,3 +33,35 @@ function resolveTilde(filePath: string) {
|
|||||||
export function resolvePath(filePath: string): string {
|
export function resolvePath(filePath: string): string {
|
||||||
return path.resolve(resolveTilde(filePath));
|
return path.resolve(resolveTilde(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if `testPath` represents a potential filesystem entry that would be
|
||||||
|
* logically "within" the `parentPath` directory.
|
||||||
|
*
|
||||||
|
* This function will return `true` in the above case, and `false` otherwise.
|
||||||
|
* It will return `false` if the two paths are the same (after resolving them).
|
||||||
|
*
|
||||||
|
* The function makes no FS calls and is platform dependant. Meaning that the
|
||||||
|
* results are only guaranteed to be correct for the platform you are running
|
||||||
|
* on.
|
||||||
|
* @param parentPath The known path of a directory
|
||||||
|
* @param testPath The path that is to be tested
|
||||||
|
*/
|
||||||
|
export function isLogicalChildPath(parentPath: string, testPath: string): boolean {
|
||||||
|
parentPath = path.resolve(parentPath);
|
||||||
|
testPath = path.resolve(testPath);
|
||||||
|
|
||||||
|
if (parentPath === testPath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (testPath.length >= parentPath.length) {
|
||||||
|
if (testPath === parentPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
testPath = path.dirname(testPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -71,4 +71,4 @@ export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
|||||||
export const appSemVer = new SemVer(packageInfo.version);
|
export const appSemVer = new SemVer(packageInfo.version);
|
||||||
export const docsUrl = "https://docs.k8slens.dev/main/" as string;
|
export const docsUrl = "https://docs.k8slens.dev/main/" as string;
|
||||||
|
|
||||||
export const sentryDsn = packageInfo.config?.sentryDsn;
|
export const sentryDsn = packageInfo.config?.sentryDsn ?? "";
|
||||||
|
|||||||
138
src/extensions/__tests__/extension-compatibility.test.ts
Normal file
138
src/extensions/__tests__/extension-compatibility.test.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isCompatibleExtension } from "../extension-compatibility";
|
||||||
|
import { Console } from "console";
|
||||||
|
import { stdout, stderr } from "process";
|
||||||
|
import type { LensExtensionManifest } from "../lens-extension";
|
||||||
|
import { appSemVer } from "../../common/vars";
|
||||||
|
|
||||||
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
|
describe("extension compatibility", () => {
|
||||||
|
describe("appSemVer with no prerelease tag", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
appSemVer.major = 5;
|
||||||
|
appSemVer.minor = 0;
|
||||||
|
appSemVer.patch = 3;
|
||||||
|
appSemVer.prerelease = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no extension comparator", () => {
|
||||||
|
const manifest = { name: "extensionName", version: "0.0.1"};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
comparator: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "bad comparator",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0-alpha.1",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||||
|
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator}};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("appSemVer with prerelease tag", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
appSemVer.major = 5;
|
||||||
|
appSemVer.minor = 0;
|
||||||
|
appSemVer.patch = 3;
|
||||||
|
appSemVer.prerelease = ["beta", 3];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no extension comparator", () => {
|
||||||
|
const manifest = { name: "extensionName", version: "0.0.1"};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
comparator: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "bad comparator",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0-alpha.1",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||||
|
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator}};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -84,7 +84,7 @@ jest.mock(
|
|||||||
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||||
if (channel === "extensions:main") {
|
if (channel === "extensions:main") {
|
||||||
// First initialize with extensions 1 and 2
|
// First initialize with extensions 1 and 2
|
||||||
// and then broadcast event to remove extensioin 2 and add extension number 3
|
// and then broadcast event to remove extension 2 and add extension number 3
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
listener({}, [
|
listener({}, [
|
||||||
[
|
[
|
||||||
|
|||||||
@ -19,33 +19,19 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./cube-spinner.scss";
|
import semver from "semver";
|
||||||
import React from "react";
|
import { appSemVer } from "../common/vars";
|
||||||
import { cssNames } from "../../utils";
|
import type { LensExtensionManifest } from "./lens-extension";
|
||||||
|
|
||||||
export interface CubeSpinnerProps {
|
export function isCompatibleExtension(manifest: LensExtensionManifest): boolean {
|
||||||
className?: string;
|
if (manifest.engines?.lens) {
|
||||||
center?: boolean;
|
/* include Lens's prerelease tag in the matching so the extension's compatibility is not limited by it */
|
||||||
}
|
return semver.satisfies(appSemVer, manifest.engines.lens, { includePrerelease: true });
|
||||||
|
|
||||||
export class CubeSpinner extends React.Component<CubeSpinnerProps> {
|
|
||||||
render() {
|
|
||||||
const { className, center } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cssNames("CubeSpinner ", className, { center })}>
|
|
||||||
<div className="sk-cube-grid">
|
|
||||||
<div className="sk-cube sk-cube1"></div>
|
|
||||||
<div className="sk-cube sk-cube2"></div>
|
|
||||||
<div className="sk-cube sk-cube3"></div>
|
|
||||||
<div className="sk-cube sk-cube4"></div>
|
|
||||||
<div className="sk-cube sk-cube5"></div>
|
|
||||||
<div className="sk-cube sk-cube6"></div>
|
|
||||||
<div className="sk-cube sk-cube7"></div>
|
|
||||||
<div className="sk-cube sk-cube8"></div>
|
|
||||||
<div className="sk-cube sk-cube9"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCompatibleBundledExtension(manifest: LensExtensionManifest): boolean {
|
||||||
|
return manifest.version === appSemVer.raw;
|
||||||
}
|
}
|
||||||
@ -34,9 +34,8 @@ import { extensionInstaller } from "./extension-installer";
|
|||||||
import { ExtensionsStore } from "./extensions-store";
|
import { ExtensionsStore } from "./extensions-store";
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||||
import semver from "semver";
|
|
||||||
import { appSemVer } from "../common/vars";
|
|
||||||
import { isProduction } from "../common/vars";
|
import { isProduction } from "../common/vars";
|
||||||
|
import { isCompatibleBundledExtension, isCompatibleExtension } from "./extension-compatibility";
|
||||||
|
|
||||||
export interface InstalledExtension {
|
export interface InstalledExtension {
|
||||||
id: LensExtensionId;
|
id: LensExtensionId;
|
||||||
@ -362,17 +361,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
const extensionDir = path.dirname(manifestPath);
|
const extensionDir = path.dirname(manifestPath);
|
||||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||||
let isCompatible = isBundled;
|
const isCompatible = (isBundled && isCompatibleBundledExtension(manifest)) || isCompatibleExtension(manifest);
|
||||||
|
|
||||||
if (manifest.engines?.lens) {
|
|
||||||
const appSemVerLatestImplied = appSemVer;
|
|
||||||
|
|
||||||
if (appSemVerLatestImplied.prerelease?.[0] === "latest") {
|
|
||||||
/* remove the "latest" prerelease tag so as not to require the extension to specify it */
|
|
||||||
appSemVerLatestImplied.prerelease = [];
|
|
||||||
}
|
|
||||||
isCompatible = semver.satisfies(appSemVerLatestImplied, manifest.engines.lens);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export abstract class IpcMain extends IpcRegistrar {
|
|||||||
* Listen for broadcasts within your extension
|
* Listen for broadcasts within your extension
|
||||||
* @param channel The channel to listen for broadcasts on
|
* @param channel The channel to listen for broadcasts on
|
||||||
* @param listener The function that will be called with the arguments of the broadcast
|
* @param listener The function that will be called with the arguments of the broadcast
|
||||||
* @returns An optional disopser, Lens will cleanup when the extension is disabled or uninstalled even if this is not called
|
* @returns An optional disposer, Lens will cleanup when the extension is disabled or uninstalled even if this is not called
|
||||||
*/
|
*/
|
||||||
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
||||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export class LensExtension {
|
|||||||
* getExtensionFileFolder returns the path to an already created folder. This
|
* getExtensionFileFolder returns the path to an already created folder. This
|
||||||
* folder is for the sole use of this extension.
|
* folder is for the sole use of this extension.
|
||||||
*
|
*
|
||||||
* Note: there is no security done on this folder, only obfiscation of the
|
* Note: there is no security done on this folder, only obfuscation of the
|
||||||
* folder name.
|
* folder name.
|
||||||
*/
|
*/
|
||||||
async getExtensionFileFolder(): Promise<string> {
|
async getExtensionFileFolder(): Promise<string> {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import type * as registries from "./registries";
|
|||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -37,7 +38,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: registries.CommandRegistration[] = [];
|
commands: registries.CommandRegistration[] = [];
|
||||||
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
topBarItems: registries.TopBarRegistration[] = [];
|
topBarItems: registries.TopBarRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
|
|||||||
@ -22,14 +22,24 @@
|
|||||||
import { ClusterPageRegistry, getExtensionPageUrl, GlobalPageRegistry, PageParams } from "../page-registry";
|
import { ClusterPageRegistry, getExtensionPageUrl, GlobalPageRegistry, PageParams } from "../page-registry";
|
||||||
import { LensExtension } from "../../lens-extension";
|
import { LensExtension } from "../../lens-extension";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import fse from "fs-extra";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
|
import { ThemeStore } from "../../../renderer/theme.store";
|
||||||
|
import { TerminalStore } from "../../renderer-api/components";
|
||||||
|
import { UserStore } from "../../../common/user-store";
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
let ext: LensExtension = null;
|
let ext: LensExtension = null;
|
||||||
|
|
||||||
describe("getPageUrl", () => {
|
describe("page registry tests", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
ext = new LensExtension({
|
ext = new LensExtension({
|
||||||
manifest: {
|
manifest: {
|
||||||
@ -43,6 +53,9 @@ describe("getPageUrl", () => {
|
|||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
isCompatible: true
|
isCompatible: true
|
||||||
});
|
});
|
||||||
|
UserStore.createInstance();
|
||||||
|
ThemeStore.createInstance();
|
||||||
|
TerminalStore.createInstance();
|
||||||
ClusterPageRegistry.createInstance();
|
ClusterPageRegistry.createInstance();
|
||||||
GlobalPageRegistry.createInstance().add({
|
GlobalPageRegistry.createInstance().add({
|
||||||
id: "page-with-params",
|
id: "page-with-params",
|
||||||
@ -54,70 +67,6 @@ describe("getPageUrl", () => {
|
|||||||
test2: "" // no default value, just declaration
|
test2: "" // no default value, just declaration
|
||||||
},
|
},
|
||||||
}, ext);
|
}, ext);
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
GlobalPageRegistry.resetInstance();
|
|
||||||
ClusterPageRegistry.resetInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a page url for extension", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows to pass base url as parameter", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes @ and replace `/` to `--`", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds / prefix", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("normalize possible multi-slashes in page.id", () => {
|
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gets page url with custom params", () => {
|
|
||||||
const params: PageParams = { test1: "one", test2: "2" };
|
|
||||||
const searchParams = new URLSearchParams(params);
|
|
||||||
const pageUrl = getExtensionPageUrl({
|
|
||||||
extensionId: ext.name,
|
|
||||||
pageId: "page-with-params",
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gets page url with default custom params", () => {
|
|
||||||
const defaultPageUrl = getExtensionPageUrl({
|
|
||||||
extensionId: ext.name,
|
|
||||||
pageId: "page-with-params",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("globalPageRegistry", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
ext = new LensExtension({
|
|
||||||
manifest: {
|
|
||||||
name: "@acme/foo-bar",
|
|
||||||
version: "0.1.1"
|
|
||||||
},
|
|
||||||
id: "/this/is/fake/package.json",
|
|
||||||
absolutePath: "/absolute/fake/",
|
|
||||||
manifestPath: "/this/is/fake/package.json",
|
|
||||||
isBundled: false,
|
|
||||||
isEnabled: true,
|
|
||||||
isCompatible: true
|
|
||||||
});
|
|
||||||
ClusterPageRegistry.createInstance();
|
|
||||||
GlobalPageRegistry.createInstance().add([
|
GlobalPageRegistry.createInstance().add([
|
||||||
{
|
{
|
||||||
id: "test-page",
|
id: "test-page",
|
||||||
@ -142,33 +91,82 @@ describe("globalPageRegistry", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
GlobalPageRegistry.resetInstance();
|
GlobalPageRegistry.resetInstance();
|
||||||
ClusterPageRegistry.resetInstance();
|
ClusterPageRegistry.resetInstance();
|
||||||
|
TerminalStore.resetInstance();
|
||||||
|
ThemeStore.resetInstance();
|
||||||
|
UserStore.resetInstance();
|
||||||
|
fse.remove("tmp");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getByPageTarget", () => {
|
describe("getPageUrl", () => {
|
||||||
it("matching to first registered page without id", () => {
|
it("returns a page url for extension", () => {
|
||||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name });
|
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
||||||
|
|
||||||
expect(page.id).toEqual(undefined);
|
|
||||||
expect(page.extensionId).toEqual(ext.name);
|
|
||||||
expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns matching page", () => {
|
it("allows to pass base url as parameter", () => {
|
||||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
|
||||||
pageId: "test-page",
|
|
||||||
extensionId: ext.name
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(page.id).toEqual("test-page");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null if target not found", () => {
|
it("removes @ and replace `/` to `--`", () => {
|
||||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar");
|
||||||
pageId: "wrong-page",
|
});
|
||||||
extensionId: ext.name
|
|
||||||
|
it("adds / prefix", () => {
|
||||||
|
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalize possible multi-slashes in page.id", () => {
|
||||||
|
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets page url with custom params", () => {
|
||||||
|
const params: PageParams = { test1: "one", test2: "2" };
|
||||||
|
const searchParams = new URLSearchParams(params);
|
||||||
|
const pageUrl = getExtensionPageUrl({
|
||||||
|
extensionId: ext.name,
|
||||||
|
pageId: "page-with-params",
|
||||||
|
params,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(page).toBeNull();
|
expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets page url with default custom params", () => {
|
||||||
|
const defaultPageUrl = getExtensionPageUrl({
|
||||||
|
extensionId: ext.name,
|
||||||
|
pageId: "page-with-params",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("globalPageRegistry", () => {
|
||||||
|
describe("getByPageTarget", () => {
|
||||||
|
it("matching to first registered page without id", () => {
|
||||||
|
const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name });
|
||||||
|
|
||||||
|
expect(page.id).toEqual(undefined);
|
||||||
|
expect(page.extensionId).toEqual(ext.name);
|
||||||
|
expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns matching page", () => {
|
||||||
|
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
||||||
|
pageId: "test-page",
|
||||||
|
extensionId: ext.name
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(page.id).toEqual("test-page");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null if target not found", () => {
|
||||||
|
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
||||||
|
pageId: "wrong-page",
|
||||||
|
extensionId: ext.name
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(page).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -20,20 +20,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
import type { CatalogEntity } from "../common-api/catalog";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface CatalogEntityDetailComponents {
|
export interface CatalogEntityDetailsProps<T extends CatalogEntity> {
|
||||||
Details: React.ComponentType<any>;
|
entity: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogEntityDetailRegistration {
|
export interface CatalogEntityDetailComponents<T extends CatalogEntity> {
|
||||||
|
Details: React.ComponentType<CatalogEntityDetailsProps<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogEntityDetailRegistration<T extends CatalogEntity> {
|
||||||
kind: string;
|
kind: string;
|
||||||
apiVersions: string[];
|
apiVersions: string[];
|
||||||
components: CatalogEntityDetailComponents;
|
components: CatalogEntityDetailComponents<T>;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration> {
|
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration<CatalogEntity>> {
|
||||||
getItemsForKind(kind: string, apiVersion: string) {
|
getItemsForKind(kind: string, apiVersion: string) {
|
||||||
const items = this.getItems().filter((item) => {
|
const items = this.getItems().filter((item) => {
|
||||||
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
||||||
|
|||||||
@ -67,5 +67,5 @@ export * from "../../renderer/components/+events/kube-event-details";
|
|||||||
|
|
||||||
// specific exports
|
// specific exports
|
||||||
export * from "../../renderer/components/status-brick";
|
export * from "../../renderer/components/status-brick";
|
||||||
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
export { terminalStore, createTerminalTab, TerminalStore } from "../../renderer/components/dock/terminal.store";
|
||||||
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
||||||
|
|||||||
@ -207,7 +207,7 @@ describe("ContextHandler", () => {
|
|||||||
expect(service.id === "id_0");
|
expect(service.id === "id_0");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shouldn't pick the second provider of 2 succcess(es) after 1 failure(s)", async () => {
|
it("shouldn't pick the second provider of 2 success(es) after 1 failure(s)", async () => {
|
||||||
const reg = PrometheusProviderRegistry.getInstance();
|
const reg = PrometheusProviderRegistry.getInstance();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import "../common/cluster-ipc";
|
|||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||||
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import type { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
@ -32,6 +32,8 @@ import { KubernetesCluster, KubernetesClusterPrometheusMetrics, KubernetesCluste
|
|||||||
import { ipcMainOn } from "../common/ipc";
|
import { ipcMainOn } from "../common/ipc";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
|
|
||||||
|
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||||
|
|
||||||
export class ClusterManager extends Singleton {
|
export class ClusterManager extends Singleton {
|
||||||
private store = ClusterStore.getInstance();
|
private store = ClusterStore.getInstance();
|
||||||
deleting = observable.set<ClusterId>();
|
deleting = observable.set<ClusterId>();
|
||||||
@ -73,7 +75,7 @@ export class ClusterManager extends Singleton {
|
|||||||
if (removedClusters.length > 0) {
|
if (removedClusters.length > 0) {
|
||||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||||
|
|
||||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
logger.info(`${logPrefix} removing clusters`, meta);
|
||||||
removedClusters.forEach(cluster => cluster.disconnect());
|
removedClusters.forEach(cluster => cluster.disconnect());
|
||||||
this.store.removedClusters.clear();
|
this.store.removedClusters.clear();
|
||||||
}
|
}
|
||||||
@ -103,13 +105,13 @@ export class ClusterManager extends Singleton {
|
|||||||
|
|
||||||
this.updateEntityStatus(entity, cluster);
|
this.updateEntityStatus(entity, cluster);
|
||||||
|
|
||||||
entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels);
|
entity.metadata.labels = {
|
||||||
entity.metadata.labels.distro = cluster.distribution;
|
...entity.metadata.labels,
|
||||||
|
...cluster.labels,
|
||||||
if (cluster.preferences?.clusterName) {
|
};
|
||||||
entity.metadata.name = cluster.preferences.clusterName;
|
entity.metadata.distro = cluster.distribution;
|
||||||
}
|
entity.metadata.kubeVersion = cluster.version;
|
||||||
|
entity.metadata.name = cluster.name;
|
||||||
entity.spec.metrics ||= { source: "local" };
|
entity.spec.metrics ||= { source: "local" };
|
||||||
|
|
||||||
if (entity.spec.metrics.source === "local") {
|
if (entity.spec.metrics.source === "local") {
|
||||||
@ -161,14 +163,22 @@ export class ClusterManager extends Singleton {
|
|||||||
const cluster = this.store.getById(entity.metadata.uid);
|
const cluster = this.store.getById(entity.metadata.uid);
|
||||||
|
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
this.store.addCluster({
|
try {
|
||||||
id: entity.metadata.uid,
|
this.store.addCluster({
|
||||||
preferences: {
|
id: entity.metadata.uid,
|
||||||
clusterName: entity.metadata.name
|
preferences: {
|
||||||
},
|
clusterName: entity.metadata.name
|
||||||
kubeConfigPath: entity.spec.kubeconfigPath,
|
},
|
||||||
contextName: entity.spec.kubeconfigContext
|
kubeConfigPath: entity.spec.kubeconfigPath,
|
||||||
});
|
contextName: entity.spec.kubeconfigContext
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
||||||
|
logger.warn(`${logPrefix} kubeconfig file disappeared`, { path: entity.spec.kubeconfigPath });
|
||||||
|
} else {
|
||||||
|
logger.error(`${logPrefix} failed to add cluster: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||||
cluster.contextName = entity.spec.kubeconfigContext;
|
cluster.contextName = entity.spec.kubeconfigContext;
|
||||||
@ -179,7 +189,7 @@ export class ClusterManager extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onNetworkOffline = () => {
|
protected onNetworkOffline = () => {
|
||||||
logger.info("[CLUSTER-MANAGER]: network is offline");
|
logger.info(`${logPrefix} network is offline`);
|
||||||
this.store.clustersList.forEach((cluster) => {
|
this.store.clustersList.forEach((cluster) => {
|
||||||
if (!cluster.disconnected) {
|
if (!cluster.disconnected) {
|
||||||
cluster.online = false;
|
cluster.online = false;
|
||||||
@ -190,7 +200,7 @@ export class ClusterManager extends Singleton {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected onNetworkOnline = () => {
|
protected onNetworkOnline = () => {
|
||||||
logger.info("[CLUSTER-MANAGER]: network is online");
|
logger.info(`${logPrefix} network is online`);
|
||||||
this.store.clustersList.forEach((cluster) => {
|
this.store.clustersList.forEach((cluster) => {
|
||||||
if (!cluster.disconnected) {
|
if (!cluster.disconnected) {
|
||||||
cluster.refreshConnectionStatus().catch((e) => e);
|
cluster.refreshConnectionStatus().catch((e) => e);
|
||||||
@ -236,8 +246,10 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
|||||||
name: cluster.name,
|
name: cluster.name,
|
||||||
source: "local",
|
source: "local",
|
||||||
labels: {
|
labels: {
|
||||||
distro: cluster.distribution,
|
...cluster.labels,
|
||||||
}
|
},
|
||||||
|
distro: cluster.distribution,
|
||||||
|
kubeVersion: cluster.version,
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
kubeconfigPath: cluster.kubeConfigPath,
|
kubeconfigPath: cluster.kubeConfigPath,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { VersionDetector } from "./cluster-detectors/version-detector";
|
|||||||
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import { toJS } from "../common/utils";
|
import { toJS } from "../common/utils";
|
||||||
|
import { initialNodeShellImage } from "../common/cluster-store";
|
||||||
|
|
||||||
export enum ClusterStatus {
|
export enum ClusterStatus {
|
||||||
AccessGranted = 2,
|
AccessGranted = 2,
|
||||||
@ -234,8 +235,18 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return this.preferences.clusterName || this.contextName;
|
return this.preferences.clusterName || this.contextName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detected kubernetes distribution
|
||||||
|
*/
|
||||||
@computed get distribution(): string {
|
@computed get distribution(): string {
|
||||||
return this.metadata.distribution?.toString() || "unknown";
|
return this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detected kubernetes version
|
||||||
|
*/
|
||||||
|
@computed get version(): string {
|
||||||
|
return this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -250,13 +261,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return toJS({ prometheus, prometheusProvider });
|
return toJS({ prometheus, prometheusProvider });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Kubernetes version
|
|
||||||
*/
|
|
||||||
get version(): string {
|
|
||||||
return String(this.metadata?.version ?? "");
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.id = model.id;
|
this.id = model.id;
|
||||||
@ -745,4 +749,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
|
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
|
||||||
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
|
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get nodeShellImage(): string {
|
||||||
|
return this.preferences.nodeShellImage || initialNodeShellImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
get imagePullSecret(): string {
|
||||||
|
return this.preferences.imagePullSecret || "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,7 +146,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async addСustomRepo(repoAttributes : HelmRepo) {
|
public static async addCustomRepo(repoAttributes : HelmRepo) {
|
||||||
logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`);
|
logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`);
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,10 @@ import { ExtensionsStore } from "../extensions/extensions-store";
|
|||||||
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
||||||
import { SentryInit } from "../common/sentry";
|
import { SentryInit } from "../common/sentry";
|
||||||
|
|
||||||
|
// This has to be called before start using winton-based logger
|
||||||
|
// For example, before any logger.log
|
||||||
|
SentryInit();
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
const cleanup = disposer();
|
const cleanup = disposer();
|
||||||
|
|
||||||
@ -133,7 +137,7 @@ app.on("ready", async () => {
|
|||||||
/**
|
/**
|
||||||
* The following sync MUST be done before HotbarStore creation, because that
|
* The following sync MUST be done before HotbarStore creation, because that
|
||||||
* store has migrations that will remove items that previous migrations add
|
* store has migrations that will remove items that previous migrations add
|
||||||
* if this is not presant
|
* if this is not present
|
||||||
*/
|
*/
|
||||||
syncGeneralEntities();
|
syncGeneralEntities();
|
||||||
|
|
||||||
@ -141,14 +145,12 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
UserStore.createInstance().startMainReactions();
|
UserStore.createInstance().startMainReactions();
|
||||||
|
|
||||||
/**
|
// ClusterStore depends on: UserStore
|
||||||
* There is no point setting up sentry before UserStore is initialized as
|
|
||||||
* `allowErrorReporting` will always be falsy.
|
|
||||||
*/
|
|
||||||
await SentryInit();
|
|
||||||
|
|
||||||
ClusterStore.createInstance().provideInitialFromMain();
|
ClusterStore.createInstance().provideInitialFromMain();
|
||||||
|
|
||||||
|
// HotbarStore depends on: ClusterStore
|
||||||
HotbarStore.createInstance();
|
HotbarStore.createInstance();
|
||||||
|
|
||||||
ExtensionsStore.createInstance();
|
ExtensionsStore.createInstance();
|
||||||
FilesystemProvisionerStore.createInstance();
|
FilesystemProvisionerStore.createInstance();
|
||||||
WeblinkStore.createInstance();
|
WeblinkStore.createInstance();
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class PrometheusHelm extends PrometheusLens {
|
|||||||
readonly id: string = "helm";
|
readonly id: string = "helm";
|
||||||
readonly name: string = "Helm";
|
readonly name: string = "Helm";
|
||||||
readonly rateAccuracy: string = "5m";
|
readonly rateAccuracy: string = "5m";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class PrometheusLens extends PrometheusProvider {
|
|||||||
readonly id: string = "lens";
|
readonly id: string = "lens";
|
||||||
readonly name: string = "Lens";
|
readonly name: string = "Lens";
|
||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly isConfigurable: boolean = true;
|
readonly isConfigurable: boolean = false;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly id: string = "operator";
|
readonly id: string = "operator";
|
||||||
readonly name: string = "Prometheus Operator";
|
readonly name: string = "Prometheus Operator";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class PrometheusStacklight extends PrometheusProvider {
|
|||||||
readonly id: string = "stacklight";
|
readonly id: string = "stacklight";
|
||||||
readonly name: string = "Stacklight";
|
readonly name: string = "Stacklight";
|
||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -61,7 +61,8 @@ export class LensProxy extends Singleton {
|
|||||||
if (req.url.startsWith(`${apiPrefix}?`)) {
|
if (req.url.startsWith(`${apiPrefix}?`)) {
|
||||||
handleWsUpgrade(req, socket, head);
|
handleWsUpgrade(req, socket, head);
|
||||||
} else {
|
} else {
|
||||||
this.handleProxyUpgrade(proxy, req, socket, head);
|
this.handleProxyUpgrade(proxy, req, socket, head)
|
||||||
|
.catch(error => logger.error(`[LENS-PROXY]: failed to handle proxy upgrade: ${error}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -168,7 +169,10 @@ export class LensProxy extends Singleton {
|
|||||||
if (!res.headersSent && req.url) {
|
if (!res.headersSent && req.url) {
|
||||||
const url = new URL(req.url, "http://localhost");
|
const url = new URL(req.url, "http://localhost");
|
||||||
|
|
||||||
if (url.searchParams.has("watch")) res.flushHeaders();
|
if (url.searchParams.has("watch")) {
|
||||||
|
res.statusCode = proxyRes.statusCode;
|
||||||
|
res.flushHeaders();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -191,7 +195,8 @@ export class LensProxy extends Singleton {
|
|||||||
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.retryCounters.set(reqId, retryCount + 1);
|
this.retryCounters.set(reqId, retryCount + 1);
|
||||||
this.handleRequest(proxy, req, res);
|
this.handleRequest(proxy, req, res)
|
||||||
|
.catch(error => logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,8 +31,11 @@ export class LocalShellSession extends ShellSession {
|
|||||||
return [helmCli.getBinaryDir()];
|
return [helmCli.getBinaryDir()];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
protected get cwd(): string | undefined {
|
||||||
|
return this.cluster.preferences?.terminalCWD;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async open() {
|
||||||
const env = await this.getCachedShellEnv();
|
const env = await this.getCachedShellEnv();
|
||||||
const shell = env.PTYSHELL;
|
const shell = env.PTYSHELL;
|
||||||
const args = await this.getShellArgs(shell);
|
const args = await this.getShellArgs(shell);
|
||||||
|
|||||||
@ -32,6 +32,10 @@ export class NodeShellSession extends ShellSession {
|
|||||||
protected podId = `node-shell-${uuid()}`;
|
protected podId = `node-shell-${uuid()}`;
|
||||||
protected kc: KubeConfig;
|
protected kc: KubeConfig;
|
||||||
|
|
||||||
|
protected get cwd(): string | undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
|
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
|
||||||
super(socket, cluster);
|
super(socket, cluster);
|
||||||
}
|
}
|
||||||
@ -77,13 +81,16 @@ export class NodeShellSession extends ShellSession {
|
|||||||
}],
|
}],
|
||||||
containers: [{
|
containers: [{
|
||||||
name: "shell",
|
name: "shell",
|
||||||
image: "docker.io/alpine:3.13",
|
image: this.cluster.nodeShellImage,
|
||||||
securityContext: {
|
securityContext: {
|
||||||
privileged: true,
|
privileged: true,
|
||||||
},
|
},
|
||||||
command: ["nsenter"],
|
command: ["nsenter"],
|
||||||
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"]
|
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"]
|
||||||
}],
|
}],
|
||||||
|
imagePullSecrets: [{
|
||||||
|
name: this.cluster.imagePullSecret,
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fse from "fs-extra";
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import { Kubectl } from "../kubectl";
|
import { Kubectl } from "../kubectl";
|
||||||
import type * as WebSocket from "ws";
|
import type * as WebSocket from "ws";
|
||||||
@ -50,9 +51,7 @@ export abstract class ShellSession {
|
|||||||
protected kubectlBinDirP: Promise<string>;
|
protected kubectlBinDirP: Promise<string>;
|
||||||
protected kubeconfigPathP: Promise<string>;
|
protected kubeconfigPathP: Promise<string>;
|
||||||
|
|
||||||
protected get cwd(): string | undefined {
|
protected abstract get cwd(): string | undefined;
|
||||||
return this.cluster.preferences?.terminalCWD;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(protected websocket: WebSocket, protected cluster: Cluster) {
|
constructor(protected websocket: WebSocket, protected cluster: Cluster) {
|
||||||
this.kubectl = new Kubectl(cluster.version);
|
this.kubectl = new Kubectl(cluster.version);
|
||||||
@ -60,10 +59,14 @@ export abstract class ShellSession {
|
|||||||
this.kubectlBinDirP = this.kubectl.binDir();
|
this.kubectlBinDirP = this.kubectl.binDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
open(shell: string, args: string[], env: Record<string, any>): void {
|
protected async open(shell: string, args: string[], env: Record<string, any>) {
|
||||||
|
const cwd = (this.cwd && await fse.pathExists(this.cwd))
|
||||||
|
? this.cwd
|
||||||
|
: env.HOME;
|
||||||
|
|
||||||
this.shellProcess = pty.spawn(shell, args, {
|
this.shellProcess = pty.spawn(shell, args, {
|
||||||
cols: 80,
|
cols: 80,
|
||||||
cwd: this.cwd || env.HOME,
|
cwd,
|
||||||
env,
|
env,
|
||||||
name: "xterm-256color",
|
name: "xterm-256color",
|
||||||
rows: 30,
|
rows: 30,
|
||||||
|
|||||||
@ -93,7 +93,7 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
click() {
|
click() {
|
||||||
windowManager
|
windowManager
|
||||||
.navigate(preferencesURL())
|
.navigate(preferencesURL())
|
||||||
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to nativate to Preferences`, { error }));
|
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to navigate to Preferences`, { error }));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import type { Readable } from "stream";
|
import type { Readable } from "stream";
|
||||||
import URLParse from "url-parse";
|
import URLParse from "url-parse";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
interface GetPortArgs {
|
interface GetPortArgs {
|
||||||
/**
|
/**
|
||||||
@ -47,11 +48,15 @@ interface GetPortArgs {
|
|||||||
* @returns A Promise for port number
|
* @returns A Promise for port number
|
||||||
*/
|
*/
|
||||||
export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number> {
|
export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number> {
|
||||||
|
const logLines: string[] = [];
|
||||||
|
|
||||||
return new Promise<number>((resolve, reject) => {
|
return new Promise<number>((resolve, reject) => {
|
||||||
const handler = (data: any) => {
|
const handler = (data: any) => {
|
||||||
const logItem: string = data.toString();
|
const logItem: string = data.toString();
|
||||||
const match = logItem.match(args.lineRegex);
|
const match = logItem.match(args.lineRegex);
|
||||||
|
|
||||||
|
logLines.push(logItem);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
// use unknown protocol so that there is no default port
|
// use unknown protocol so that there is no default port
|
||||||
const addr = new URLParse(`s://${match.groups.address.trim()}`);
|
const addr = new URLParse(`s://${match.groups.address.trim()}`);
|
||||||
@ -64,6 +69,7 @@ export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number
|
|||||||
};
|
};
|
||||||
const timeoutID = setTimeout(() => {
|
const timeoutID = setTimeout(() => {
|
||||||
stream.off("data", handler);
|
stream.off("data", handler);
|
||||||
|
logger.warn(`[getPortFrom]: failed to retrieve port via ${args.lineRegex.toString()}: ${logLines}`);
|
||||||
reject(new Error("failed to retrieve port from stream"));
|
reject(new Error("failed to retrieve port from stream"));
|
||||||
}, args.timeout ?? 5000);
|
}, args.timeout ?? 5000);
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default {
|
|||||||
workspaces.set(id, name);
|
workspaces.set(id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clusters: ClusterModel[] = store.get("clusters");
|
const clusters: ClusterModel[] = store.get("clusters") ?? [];
|
||||||
|
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
if (cluster.workspace && workspaces.has(cluster.workspace)) {
|
if (cluster.workspace && workspaces.has(cluster.workspace)) {
|
||||||
|
|||||||
@ -20,19 +20,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { existsSync, readJsonSync } from "fs-extra";
|
import { existsSync, readFileSync } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { ClusterStore, ClusterStoreModel } from "../../common/cluster-store";
|
import { ClusterStore, ClusterStoreModel } from "../../common/cluster-store";
|
||||||
import type { KubeconfigSyncEntry, UserPreferencesModel } from "../../common/user-store";
|
import type { KubeconfigSyncEntry, UserPreferencesModel } from "../../common/user-store";
|
||||||
import { MigrationDeclaration, migrationLog } from "../helpers";
|
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||||
|
import { isLogicalChildPath } from "../../common/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
version: "5.0.3-beta.1",
|
version: "5.0.3-beta.1",
|
||||||
run(store) {
|
run(store) {
|
||||||
try {
|
try {
|
||||||
const { syncKubeconfigEntries = [], ...preferences }: UserPreferencesModel = store.get("preferences") ?? {};
|
const { syncKubeconfigEntries = [], ...preferences }: UserPreferencesModel = store.get("preferences") ?? {};
|
||||||
const { clusters = [] }: ClusterStoreModel = readJsonSync(path.resolve(app.getPath("userData"), "lens-cluster-store.json")) ?? {};
|
const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(path.resolve(app.getPath("userData"), "lens-cluster-store.json"), "utf-8")) ?? {};
|
||||||
|
const extensionDataDir = path.resolve(app.getPath("userData"), "extension_data");
|
||||||
const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath));
|
const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath));
|
||||||
|
|
||||||
syncPaths.add(path.join(os.homedir(), ".kube"));
|
syncPaths.add(path.join(os.homedir(), ".kube"));
|
||||||
@ -50,6 +52,11 @@ export default {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLogicalChildPath(extensionDataDir, cluster.kubeConfigPath)) {
|
||||||
|
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is placed under an extension_data folder`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!existsSync(cluster.kubeConfigPath)) {
|
if (!existsSync(cluster.kubeConfigPath)) {
|
||||||
migrationLog(`Skipping ${cluster.id} because kubeConfigPath no longer exists`);
|
migrationLog(`Skipping ${cluster.id} because kubeConfigPath no longer exists`);
|
||||||
continue;
|
continue;
|
||||||
@ -64,8 +71,6 @@ export default {
|
|||||||
migrationLog("Final list of synced paths", updatedSyncEntries);
|
migrationLog("Final list of synced paths", updatedSyncEntries);
|
||||||
store.set("preferences", { ...preferences, syncKubeconfigEntries: updatedSyncEntries });
|
store.set("preferences", { ...preferences, syncKubeconfigEntries: updatedSyncEntries });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
|
|
||||||
if (error.code !== "ENOENT") {
|
if (error.code !== "ENOENT") {
|
||||||
// ignore files being missing
|
// ignore files being missing
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -129,8 +129,18 @@ const tests: KubeApiParseTestData[] = [
|
|||||||
}],
|
}],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const throwtests = [
|
||||||
|
undefined,
|
||||||
|
"",
|
||||||
|
"ajklsmh"
|
||||||
|
];
|
||||||
|
|
||||||
describe("parseApi unit tests", () => {
|
describe("parseApi unit tests", () => {
|
||||||
it.each(tests)("testing %s", (url, expected) => {
|
it.each(tests)("testing %j", (url, expected) => {
|
||||||
expect(parseKubeApi(url)).toStrictEqual(expected);
|
expect(parseKubeApi(url)).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(throwtests)("testing %j should throw", (url) => {
|
||||||
|
expect(() => parseKubeApi(url)).toThrowError("invalid apiPath");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,11 +19,20 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.AppInit {
|
import { Pod } from "../endpoints";
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.waiting-services {
|
describe("Pod tests", () => {
|
||||||
font-size: small;
|
it("getAllContainers() should never throw", () => {
|
||||||
opacity: .75;
|
const pod = new Pod({
|
||||||
}
|
apiVersion: "foo",
|
||||||
}
|
kind: "Pod",
|
||||||
|
metadata: {
|
||||||
|
name: "foobar",
|
||||||
|
resourceVersion: "foobar",
|
||||||
|
uid: "foobar",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pod.getAllContainers()).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -58,8 +58,14 @@ export class ApiManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected resolveApi(api: string | KubeApi): KubeApi {
|
protected resolveApi(api?: string | KubeApi): KubeApi | undefined {
|
||||||
if (typeof api === "string") return this.getApi(api);
|
if (!api) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof api === "string") {
|
||||||
|
return this.getApi(api);
|
||||||
|
}
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,9 +156,13 @@ export class Node extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRoleLabels() {
|
getRoleLabels() {
|
||||||
const roleLabels = Object.keys(this.metadata.labels).filter(key =>
|
if (!this.metadata?.labels || typeof this.metadata.labels !== "object") {
|
||||||
key.includes("node-role.kubernetes.io")
|
return "";
|
||||||
).map(key => key.match(/([^/]+$)/)[0]); // all after last slash
|
}
|
||||||
|
|
||||||
|
const roleLabels = Object.keys(this.metadata.labels)
|
||||||
|
.filter(key => key.includes("node-role.kubernetes.io"))
|
||||||
|
.map(key => key.match(/([^/]+$)/)[0]); // all after last slash
|
||||||
|
|
||||||
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
|
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
|
||||||
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
|
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
|
||||||
|
|||||||
@ -213,7 +213,7 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
autoBind(this);
|
autoBind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
declare spec: {
|
declare spec?: {
|
||||||
volumes?: {
|
volumes?: {
|
||||||
name: string;
|
name: string;
|
||||||
persistentVolumeClaim: {
|
persistentVolumeClaim: {
|
||||||
@ -291,11 +291,11 @@ export class Pod extends WorkloadKubeObject {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getInitContainers() {
|
getInitContainers() {
|
||||||
return this.spec.initContainers || [];
|
return this.spec?.initContainers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getContainers() {
|
getContainers() {
|
||||||
return this.spec.containers || [];
|
return this.spec?.containers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllContainers() {
|
getAllContainers() {
|
||||||
|
|||||||
@ -24,6 +24,9 @@
|
|||||||
import type { KubeObject } from "./kube-object";
|
import type { KubeObject } from "./kube-object";
|
||||||
import { splitArray } from "../../common/utils";
|
import { splitArray } from "../../common/utils";
|
||||||
import { apiManager } from "./api-manager";
|
import { apiManager } from "./api-manager";
|
||||||
|
import { isDebugging } from "../../common/vars";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
import { inspect } from "util";
|
||||||
|
|
||||||
export interface IKubeObjectRef {
|
export interface IKubeObjectRef {
|
||||||
kind: string;
|
kind: string;
|
||||||
@ -47,8 +50,26 @@ export interface IKubeApiParsed extends IKubeApiLinkRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseKubeApi(path: string): IKubeApiParsed {
|
export function parseKubeApi(path: string): IKubeApiParsed {
|
||||||
path = new URL(path, location.origin).pathname;
|
if (!isDebugging) {
|
||||||
const [, prefix, ...parts] = path.split("/");
|
return _parseKubeApi(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = _parseKubeApi(path);
|
||||||
|
|
||||||
|
logger.debug(`parseKubeApi(${inspect(path, false, null, false)}) -> ${inspect(res, false, null, false)}`);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`parseKubeApi(${inspect(path, false, null, false)}) threw: ${error}`);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _parseKubeApi(path: string): IKubeApiParsed {
|
||||||
|
const apiPath = new URL(path, location.origin).pathname;
|
||||||
|
const [, prefix, ...parts] = apiPath.split("/");
|
||||||
const apiPrefix = `/${prefix}`;
|
const apiPrefix = `/${prefix}`;
|
||||||
const [left, right, namespaced] = splitArray(parts, "namespaces");
|
const [left, right, namespaced] = splitArray(parts, "namespaces");
|
||||||
let apiGroup, apiVersion, namespace, resource, name;
|
let apiGroup, apiVersion, namespace, resource, name;
|
||||||
@ -70,12 +91,14 @@ export function parseKubeApi(path: string): IKubeApiParsed {
|
|||||||
apiGroup = left.join("/");
|
apiGroup = left.join("/");
|
||||||
} else {
|
} else {
|
||||||
switch (left.length) {
|
switch (left.length) {
|
||||||
|
case 0:
|
||||||
|
throw new Error(`invalid apiPath: ${apiPath}`);
|
||||||
case 4:
|
case 4:
|
||||||
[apiGroup, apiVersion, resource, name] = left;
|
[apiGroup, apiVersion, resource, name] = left;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
resource = left.pop();
|
resource = left.pop();
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case 1:
|
case 1:
|
||||||
apiVersion = left.pop();
|
apiVersion = left.pop();
|
||||||
apiGroup = "";
|
apiGroup = "";
|
||||||
@ -91,7 +114,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
|
|||||||
* There is no well defined selection from an array of items that were
|
* There is no well defined selection from an array of items that were
|
||||||
* separated by '/'
|
* separated by '/'
|
||||||
*
|
*
|
||||||
* Solution is to create a huristic. Namely:
|
* Solution is to create a heuristic. Namely:
|
||||||
* 1. if '.' in left[0] then apiGroup <- left[0]
|
* 1. if '.' in left[0] then apiGroup <- left[0]
|
||||||
* 2. if left[1] matches /^v[0-9]/ then apiGroup, apiVersion <- left[0], left[1]
|
* 2. if left[1] matches /^v[0-9]/ then apiGroup, apiVersion <- left[0], left[1]
|
||||||
* 3. otherwise assume apiVersion <- left[0]
|
* 3. otherwise assume apiVersion <- left[0]
|
||||||
@ -113,7 +136,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
|
|||||||
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
|
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
|
||||||
|
|
||||||
if (!apiBase) {
|
if (!apiBase) {
|
||||||
throw new Error(`invalid apiPath: ${path}`);
|
throw new Error(`invalid apiPath: ${apiPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -254,12 +254,10 @@ export class KubeObject<Metadata extends IKubeObjectMetadata = IKubeObjectMetada
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOwnerRefs() {
|
getOwnerRefs() {
|
||||||
const refs = this.metadata.ownerReferences || [];
|
const refs = this.metadata?.ownerReferences || [];
|
||||||
|
const namespace = this.getNs();
|
||||||
|
|
||||||
return refs.map(ownerRef => ({
|
return refs.map(ownerRef => ({ ...ownerRef, namespace }));
|
||||||
...ownerRef,
|
|
||||||
namespace: this.getNs(),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchFields() {
|
getSearchFields() {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export interface IToleration {
|
|||||||
key?: string;
|
key?: string;
|
||||||
operator?: string;
|
operator?: string;
|
||||||
effect?: string;
|
effect?: string;
|
||||||
|
value?: string;
|
||||||
tolerationSeconds?: number;
|
tolerationSeconds?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,11 +48,12 @@ import { ExtensionsStore } from "../extensions/extensions-store";
|
|||||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||||
import { ThemeStore } from "./theme.store";
|
import { ThemeStore } from "./theme.store";
|
||||||
import { SentryInit } from "../common/sentry";
|
import { SentryInit } from "../common/sentry";
|
||||||
|
import { TerminalStore } from "./components/dock/terminal.store";
|
||||||
|
|
||||||
configurePackages();
|
configurePackages();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If this is a development buid, wait a second to attach
|
* If this is a development build, wait a second to attach
|
||||||
* Chrome Debugger to renderer process
|
* Chrome Debugger to renderer process
|
||||||
* https://stackoverflow.com/questions/52844870/debugging-electron-renderer-process-with-vscode
|
* https://stackoverflow.com/questions/52844870/debugging-electron-renderer-process-with-vscode
|
||||||
*/
|
*/
|
||||||
@ -79,6 +80,7 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
initializers.intiKubeObjectDetailRegistry();
|
initializers.intiKubeObjectDetailRegistry();
|
||||||
initializers.initWelcomeMenuRegistry();
|
initializers.initWelcomeMenuRegistry();
|
||||||
initializers.initWorkloadsOverviewDetailRegistry();
|
initializers.initWorkloadsOverviewDetailRegistry();
|
||||||
|
initializers.initCatalogEntityDetailRegistry();
|
||||||
initializers.initCatalog();
|
initializers.initCatalog();
|
||||||
initializers.initIpcRendererListeners();
|
initializers.initIpcRendererListeners();
|
||||||
|
|
||||||
@ -87,24 +89,30 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
|
|
||||||
UserStore.createInstance();
|
UserStore.createInstance();
|
||||||
|
|
||||||
/**
|
SentryInit();
|
||||||
* There is no point setting up sentry before UserStore is initialized as
|
|
||||||
* `allowErrorReporting` will always be falsy.
|
|
||||||
*/
|
|
||||||
await SentryInit();
|
|
||||||
|
|
||||||
await ClusterStore.createInstance().loadInitialOnRenderer();
|
// ClusterStore depends on: UserStore
|
||||||
|
const cs = ClusterStore.createInstance();
|
||||||
|
|
||||||
|
await cs.loadInitialOnRenderer();
|
||||||
|
|
||||||
|
// HotbarStore depends on: ClusterStore
|
||||||
HotbarStore.createInstance();
|
HotbarStore.createInstance();
|
||||||
ExtensionsStore.createInstance();
|
ExtensionsStore.createInstance();
|
||||||
FilesystemProvisionerStore.createInstance();
|
FilesystemProvisionerStore.createInstance();
|
||||||
|
|
||||||
|
// ThemeStore depends on: UserStore
|
||||||
ThemeStore.createInstance();
|
ThemeStore.createInstance();
|
||||||
|
|
||||||
|
// TerminalStore depends on: ThemeStore
|
||||||
|
TerminalStore.createInstance();
|
||||||
WeblinkStore.createInstance();
|
WeblinkStore.createInstance();
|
||||||
|
|
||||||
ExtensionInstallationStateStore.bindIpcListeners();
|
ExtensionInstallationStateStore.bindIpcListeners();
|
||||||
HelmRepoManager.createInstance(); // initialize the manager
|
HelmRepoManager.createInstance(); // initialize the manager
|
||||||
|
|
||||||
// Register additional store listeners
|
// Register additional store listeners
|
||||||
ClusterStore.getInstance().registerIpcListener();
|
cs.registerIpcListener();
|
||||||
|
|
||||||
// init app's dependencies if any
|
// init app's dependencies if any
|
||||||
if (App.init) {
|
if (App.init) {
|
||||||
|
|||||||
@ -27,13 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
.Badge {
|
@include release-status-bgs;
|
||||||
@include release-status-bgs;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
|
|||||||
@ -272,7 +272,7 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
<DrawerItem name="Status" className="status" labelsOnly>
|
<DrawerItem name="Status" className="status" labelsOnly>
|
||||||
<Badge
|
<Badge
|
||||||
label={release.getStatus()}
|
label={release.getStatus()}
|
||||||
className={cssNames("status", kebabCase(release.getStatus()))}
|
className={kebabCase(release.getStatus())}
|
||||||
/>
|
/>
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
{this.renderValues()}
|
{this.renderValues()}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watchSelecteNamespaces(): (() => void) {
|
watchSelectedNamespaces(): (() => void) {
|
||||||
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
|
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
|
||||||
this.loadAll(namespaces);
|
this.loadAll(namespaces);
|
||||||
});
|
});
|
||||||
@ -91,7 +91,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
|||||||
this.failedLoading = false;
|
this.failedLoading = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.failedLoading = true;
|
this.failedLoading = true;
|
||||||
console.error("Loading Helm Chart releases has failed", error);
|
console.warn("Loading Helm Chart releases has failed", error);
|
||||||
|
|
||||||
if (error.error) {
|
if (error.error) {
|
||||||
Notifications.error(error.error);
|
Notifications.error(error.error);
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export class HelmReleases extends Component<Props> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
releaseStore.watchAssociatedSecrets(),
|
releaseStore.watchAssociatedSecrets(),
|
||||||
releaseStore.watchSelecteNamespaces(),
|
releaseStore.watchSelectedNamespaces(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -88,6 +88,7 @@ export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
|
|||||||
onClick?.(event);
|
onClick?.(event);
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
|
expandable={false}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
@import "autoscaler.mixins";
|
@import "autoscaler.mixins";
|
||||||
|
|
||||||
.HpaDetails {
|
.HpaDetails {
|
||||||
.Badge {
|
.status {
|
||||||
@include hpa-status-bgc;
|
@include hpa-status-bgc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -125,7 +125,7 @@ export class HpaDetails extends React.Component<Props> {
|
|||||||
{hpa.getReplicas()}
|
{hpa.getReplicas()}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
|
|
||||||
<DrawerItem name="Status" labelsOnly>
|
<DrawerItem name="Status" className="status" labelsOnly>
|
||||||
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
||||||
if (!isReady) return null;
|
if (!isReady) return null;
|
||||||
|
|
||||||
|
|||||||
@ -38,10 +38,7 @@
|
|||||||
&.status {
|
&.status {
|
||||||
flex: 1.5;
|
flex: 1.5;
|
||||||
@include table-cell-labels-offsets;
|
@include table-cell-labels-offsets;
|
||||||
|
@include hpa-status-bgc;
|
||||||
.Badge {
|
|
||||||
@include hpa-status-bgc;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.age {
|
&.age {
|
||||||
|
|||||||
@ -104,6 +104,7 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
|||||||
label={type}
|
label={type}
|
||||||
tooltip={tooltip}
|
tooltip={tooltip}
|
||||||
className={cssNames(type.toLowerCase())}
|
className={cssNames(type.toLowerCase())}
|
||||||
|
expandable={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@ -218,7 +218,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
|||||||
<div className="quota-entries">
|
<div className="quota-entries">
|
||||||
{this.quotaEntries.map(([quota, value]) => {
|
{this.quotaEntries.map(([quota, value]) => {
|
||||||
return (
|
return (
|
||||||
<div key={quota} className="quota flex gaps inline align-center">
|
<div key={quota} className="quota gaps inline align-center">
|
||||||
<div className="name">{quota}</div>
|
<div className="name">{quota}</div>
|
||||||
<div className="value">{value}</div>
|
<div className="value">{value}</div>
|
||||||
<Icon material="clear" onClick={() => this.quotas[quota] = ""} />
|
<Icon material="clear" onClick={() => this.quotas[quota] = ""} />
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export class Secrets extends React.Component<Props> {
|
|||||||
secret.getName(),
|
secret.getName(),
|
||||||
<KubeObjectStatusIcon key="icon" object={secret} />,
|
<KubeObjectStatusIcon key="icon" object={secret} />,
|
||||||
secret.getNs(),
|
secret.getNs(),
|
||||||
secret.getLabels().map(label => <Badge key={label} label={label}/>),
|
secret.getLabels().map(label => <Badge key={label} label={label} expandable={false}/>),
|
||||||
secret.getKeys().join(", "),
|
secret.getKeys().join(", "),
|
||||||
secret.type,
|
secret.type,
|
||||||
secret.getAge(),
|
secret.getAge(),
|
||||||
|
|||||||
@ -23,9 +23,7 @@
|
|||||||
|
|
||||||
.CRDDetails {
|
.CRDDetails {
|
||||||
.conditions {
|
.conditions {
|
||||||
.Badge {
|
@include crd-condition-bgc;
|
||||||
@include crd-condition-bgc;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Table {
|
.Table {
|
||||||
@ -41,21 +39,8 @@
|
|||||||
flex: 0.5;
|
flex: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
|
||||||
flex: 1.5;
|
|
||||||
|
|
||||||
.Badge {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-path {
|
.json-path {
|
||||||
flex: 3;
|
flex: 3;
|
||||||
|
|
||||||
.Badge {
|
|
||||||
white-space: pre-line;
|
|
||||||
text-overflow: initial;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,6 @@ import React from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import type { CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
import type { CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
||||||
import { cssNames } from "../../utils";
|
|
||||||
import { AceEditor } from "../ace-editor";
|
import { AceEditor } from "../ace-editor";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
@ -86,7 +85,8 @@ export class CRDDetails extends React.Component<Props> {
|
|||||||
<Badge
|
<Badge
|
||||||
key={type}
|
key={type}
|
||||||
label={type}
|
label={type}
|
||||||
className={cssNames({ disabled: status === "False" }, type)}
|
disabled={status === "False"}
|
||||||
|
className={type}
|
||||||
tooltip={(
|
tooltip={(
|
||||||
<>
|
<>
|
||||||
<p>{message}</p>
|
<p>{message}</p>
|
||||||
|
|||||||
@ -139,7 +139,7 @@ export class CrdList extends React.Component {
|
|||||||
]}
|
]}
|
||||||
renderTableContents={(crd: CustomResourceDefinition) => [
|
renderTableContents={(crd: CustomResourceDefinition) => [
|
||||||
<Link key="link" to={crd.getResourceUrl()} onClick={stopPropagation}>
|
<Link key="link" to={crd.getResourceUrl()} onClick={stopPropagation}>
|
||||||
{crd.getResourceTitle()}
|
{crd.getResourceKind()}
|
||||||
</Link>,
|
</Link>,
|
||||||
crd.getGroup(),
|
crd.getGroup(),
|
||||||
crd.getVersion(),
|
crd.getVersion(),
|
||||||
|
|||||||
@ -21,13 +21,9 @@
|
|||||||
|
|
||||||
.CrdResourceDetails {
|
.CrdResourceDetails {
|
||||||
.status {
|
.status {
|
||||||
.value {
|
.ready {
|
||||||
.Badge {
|
color: white;
|
||||||
&.ready {
|
background-color: $colorOk;
|
||||||
color: white;
|
|
||||||
background-color: $colorOk;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,7 +90,8 @@ export class CrdResourceDetails extends React.Component<Props> {
|
|||||||
.map(({ kind, message, status }, index) => (
|
.map(({ kind, message, status }, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={kind + index} label={kind}
|
key={kind + index} label={kind}
|
||||||
className={cssNames({ disabled: status === "False" }, kind.toLowerCase())}
|
disabled={status === "False"}
|
||||||
|
className={kind.toLowerCase()}
|
||||||
tooltip={message}
|
tooltip={message}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@ -30,7 +30,7 @@ $crd-condition-colors: (
|
|||||||
|
|
||||||
@mixin crd-condition-bgc {
|
@mixin crd-condition-bgc {
|
||||||
@each $status, $color in $crd-condition-colors {
|
@each $status, $color in $crd-condition-colors {
|
||||||
&.#{$status} {
|
.#{$status} {
|
||||||
background: $color;
|
background: $color;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -225,7 +225,7 @@ export class ExtensionInstallationStateStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If there is at least one extension currently ininstalling
|
* If there is at least one extension currently uninstalling
|
||||||
*/
|
*/
|
||||||
static get anyUninstalling(): boolean {
|
static get anyUninstalling(): boolean {
|
||||||
return ExtensionInstallationStateStore.uninstalling > 0;
|
return ExtensionInstallationStateStore.uninstalling > 0;
|
||||||
|
|||||||
@ -49,7 +49,7 @@ import { docsUrl } from "../../../common/vars";
|
|||||||
|
|
||||||
function getMessageFromError(error: any): string {
|
function getMessageFromError(error: any): string {
|
||||||
if (!error || typeof error !== "object") {
|
if (!error || typeof error !== "object") {
|
||||||
return "an error has occured";
|
return "an error has occurred";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.message) {
|
if (error.message) {
|
||||||
@ -63,7 +63,7 @@ function getMessageFromError(error: any): string {
|
|||||||
const rawMessage = String(error);
|
const rawMessage = String(error);
|
||||||
|
|
||||||
if (rawMessage === String({})) {
|
if (rawMessage === String({})) {
|
||||||
return "an error has occured";
|
return "an error has occurred";
|
||||||
}
|
}
|
||||||
|
|
||||||
return rawMessage;
|
return rawMessage;
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import { NamespaceSelect } from "./namespace-select";
|
|||||||
import { namespaceStore } from "./namespace.store";
|
import { namespaceStore } from "./namespace.store";
|
||||||
|
|
||||||
import type { SelectOption, SelectProps } from "../select";
|
import type { SelectOption, SelectProps } from "../select";
|
||||||
|
import { isLinux, isMac, isWindows } from "../../../common/vars";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
const Placeholder = observer((props: PlaceholderProps<any, boolean>) => {
|
const Placeholder = observer((props: PlaceholderProps<any, boolean>) => {
|
||||||
const getPlaceholder = (): React.ReactNode => {
|
const getPlaceholder = (): React.ReactNode => {
|
||||||
@ -55,13 +57,16 @@ const Placeholder = observer((props: PlaceholderProps<any, boolean>) => {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
||||||
|
static isMultiSelection = observable.box(false);
|
||||||
|
static isMenuOpen = observable.box(false);
|
||||||
|
|
||||||
formatOptionLabel({ value: namespace, label }: SelectOption) {
|
formatOptionLabel({ value: namespace, label }: SelectOption) {
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
const isSelected = namespaceStore.hasContext(namespace);
|
const isSelected = namespaceStore.hasContext(namespace);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<Icon small material="layers" />
|
<Icon small material="layers"/>
|
||||||
<span>{namespace}</span>
|
<span>{namespace}</span>
|
||||||
{isSelected && <Icon small material="check" className="box right"/>}
|
{isSelected && <Icon small material="check" className="box right"/>}
|
||||||
</div>
|
</div>
|
||||||
@ -72,26 +77,55 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChange([{ value: namespace }]: SelectOption[]) {
|
onChange([{ value: namespace }]: SelectOption[]) {
|
||||||
if (namespace) {
|
if (NamespaceSelectFilter.isMultiSelection.get() && namespace) {
|
||||||
namespaceStore.toggleContext(namespace);
|
namespaceStore.toggleContext(namespace);
|
||||||
|
} else if (!NamespaceSelectFilter.isMultiSelection.get() && namespace) {
|
||||||
|
namespaceStore.toggleSingle(namespace);
|
||||||
} else {
|
} else {
|
||||||
namespaceStore.toggleAll(false); // "All namespaces" clicked
|
namespaceStore.toggleAll(true); // "All namespaces" clicked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown = (e: React.KeyboardEvent<any>) => {
|
||||||
|
if (isMac && e.metaKey || (isWindows || isLinux) && e.ctrlKey) {
|
||||||
|
NamespaceSelectFilter.isMultiSelection.set(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onKeyUp = (e: React.KeyboardEvent<any>) => {
|
||||||
|
if (isMac && e.key === "Meta" || (isWindows || isLinux) && e.key === "Control") {
|
||||||
|
NamespaceSelectFilter.isMultiSelection.set(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onClick = () => {
|
||||||
|
if (!NamespaceSelectFilter.isMultiSelection.get()) {
|
||||||
|
NamespaceSelectFilter.isMenuOpen.set(!NamespaceSelectFilter.isMenuOpen.get());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reset = () => {
|
||||||
|
NamespaceSelectFilter.isMultiSelection.set(false);
|
||||||
|
NamespaceSelectFilter.isMenuOpen.set(false);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<NamespaceSelect
|
<div onKeyUp={this.onKeyUp} onKeyDown={this.onKeyDown} onClick={this.onClick}>
|
||||||
isMulti={true}
|
<NamespaceSelect
|
||||||
components={{ Placeholder }}
|
isMulti={true}
|
||||||
showAllNamespacesOption={true}
|
menuIsOpen={NamespaceSelectFilter.isMenuOpen.get()}
|
||||||
closeMenuOnSelect={false}
|
components={{ Placeholder }}
|
||||||
controlShouldRenderValue={false}
|
showAllNamespacesOption={true}
|
||||||
placeholder={""}
|
closeMenuOnSelect={false}
|
||||||
onChange={this.onChange}
|
controlShouldRenderValue={false}
|
||||||
formatOptionLabel={this.formatOptionLabel}
|
placeholder={""}
|
||||||
className="NamespaceSelectFilter"
|
onChange={this.onChange}
|
||||||
/>
|
onBlur={this.reset}
|
||||||
|
formatOptionLabel={this.formatOptionLabel}
|
||||||
|
className="NamespaceSelectFilter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,6 +161,12 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleSingle(namespace: string){
|
||||||
|
this.clearSelected();
|
||||||
|
this.selectNamespaces(namespace);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
toggleAll(showAll?: boolean) {
|
toggleAll(showAll?: boolean) {
|
||||||
if (typeof showAll === "boolean") {
|
if (typeof showAll === "boolean") {
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export class NodeDetailsResources extends React.Component<Props> {
|
|||||||
selectable
|
selectable
|
||||||
scrollable={false}
|
scrollable={false}
|
||||||
>
|
>
|
||||||
<TableHead>
|
<TableHead sticky={false}>
|
||||||
<TableCell className="cpu">CPU</TableCell>
|
<TableCell className="cpu">CPU</TableCell>
|
||||||
<TableCell className="memory">Memory</TableCell>
|
<TableCell className="memory">Memory</TableCell>
|
||||||
<TableCell className="ephemeral-storage">Ephemeral Storage</TableCell>
|
<TableCell className="ephemeral-storage">Ephemeral Storage</TableCell>
|
||||||
|
|||||||
@ -24,9 +24,6 @@
|
|||||||
--status-default-bg: #{$colorError};
|
--status-default-bg: #{$colorError};
|
||||||
|
|
||||||
.conditions {
|
.conditions {
|
||||||
.Badge {
|
@include node-status-bgs;
|
||||||
cursor: default;
|
|
||||||
@include node-status-bgs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,7 +33,7 @@ $node-status-color-list: (
|
|||||||
|
|
||||||
@mixin node-status-bgs {
|
@mixin node-status-bgs {
|
||||||
@each $status, $color in $node-status-color-list {
|
@each $status, $color in $node-status-color-list {
|
||||||
&.#{$status} {
|
.#{$status} {
|
||||||
background: $color;
|
background: $color;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,7 +107,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
|||||||
|
|
||||||
async addCustomRepo() {
|
async addCustomRepo() {
|
||||||
try {
|
try {
|
||||||
await HelmRepoManager.addСustomRepo(this.helmRepo);
|
await HelmRepoManager.addCustomRepo(this.helmRepo);
|
||||||
Notifications.ok(<>Helm repository <b>{this.helmRepo.name}</b> has added</>);
|
Notifications.ok(<>Helm repository <b>{this.helmRepo.name}</b> has added</>);
|
||||||
this.props.onAddRepo();
|
this.props.onAddRepo();
|
||||||
this.close();
|
this.close();
|
||||||
@ -145,7 +145,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
|
|||||||
/>
|
/>
|
||||||
{this.renderFileInput(`Key file`, FileType.KeyFile, AddHelmRepoDialog.keyExtensions)}
|
{this.renderFileInput(`Key file`, FileType.KeyFile, AddHelmRepoDialog.keyExtensions)}
|
||||||
{this.renderFileInput(`Ca file`, FileType.CaFile, AddHelmRepoDialog.certExtensions)}
|
{this.renderFileInput(`Ca file`, FileType.CaFile, AddHelmRepoDialog.certExtensions)}
|
||||||
{this.renderFileInput(`Cerificate file`, FileType.CertFile, AddHelmRepoDialog.certExtensions)}
|
{this.renderFileInput(`Certificate file`, FileType.CertFile, AddHelmRepoDialog.certExtensions)}
|
||||||
<SubTitle title="Chart Repository Credentials" />
|
<SubTitle title="Chart Repository Credentials" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
|
|||||||
@ -41,7 +41,7 @@ import { FormSwitch, Switcher } from "../switch";
|
|||||||
import { KubeconfigSyncs } from "./kubeconfig-syncs";
|
import { KubeconfigSyncs } from "./kubeconfig-syncs";
|
||||||
import { SettingLayout } from "../layout/setting-layout";
|
import { SettingLayout } from "../layout/setting-layout";
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { sentryIsInitialized } from "../../../common/sentry";
|
import { sentryDsn } from "../../../common/vars";
|
||||||
|
|
||||||
enum Pages {
|
enum Pages {
|
||||||
Application = "application",
|
Application = "application",
|
||||||
@ -81,7 +81,7 @@ export class Preferences extends React.Component {
|
|||||||
const fragment = hash.slice(1); // hash is /^(#\w.)?$/
|
const fragment = hash.slice(1); // hash is /^(#\w.)?$/
|
||||||
|
|
||||||
if (fragment) {
|
if (fragment) {
|
||||||
// ignore empty framents
|
// ignore empty fragments
|
||||||
document.getElementById(fragment)?.scrollIntoView();
|
document.getElementById(fragment)?.scrollIntoView();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
@ -259,7 +259,7 @@ export class Preferences extends React.Component {
|
|||||||
<section id="telemetry">
|
<section id="telemetry">
|
||||||
<h2 data-testid="telemetry-header">Telemetry</h2>
|
<h2 data-testid="telemetry-header">Telemetry</h2>
|
||||||
{telemetryExtensions.map(this.renderExtension)}
|
{telemetryExtensions.map(this.renderExtension)}
|
||||||
{sentryIsInitialized ? (
|
{sentryDsn ? (
|
||||||
<React.Fragment key='sentry'>
|
<React.Fragment key='sentry'>
|
||||||
<section id='sentry' className="small">
|
<section id='sentry' className="small">
|
||||||
<SubTitle title='Automatic Error Reporting' />
|
<SubTitle title='Automatic Error Reporting' />
|
||||||
|
|||||||
@ -33,8 +33,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.conditions {
|
.conditions {
|
||||||
.Badge {
|
@include job-condition-bgs;
|
||||||
@include job-condition-bgs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,21 +21,19 @@
|
|||||||
|
|
||||||
.DeploymentDetails {
|
.DeploymentDetails {
|
||||||
.conditions {
|
.conditions {
|
||||||
.Badge {
|
.available {
|
||||||
&.available {
|
color: white;
|
||||||
color: white;
|
background-color: $deployment-available;
|
||||||
background-color: $deployment-available;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.progressing {
|
.progressing {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $deployment-progressing;
|
background-color: $deployment-progressing;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.replica-failure {
|
.replica-failure {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $deployment-replicafailure;
|
background-color: $deployment-replicafailure;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,7 +27,6 @@ import { disposeOnUnmount, observer } from "mobx-react";
|
|||||||
import { DrawerItem } from "../drawer";
|
import { DrawerItem } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import type { Deployment } from "../../api/endpoints";
|
import type { Deployment } from "../../api/endpoints";
|
||||||
import { cssNames } from "../../utils";
|
|
||||||
import { PodDetailsTolerations } from "../+workloads-pods/pod-details-tolerations";
|
import { PodDetailsTolerations } from "../+workloads-pods/pod-details-tolerations";
|
||||||
import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities";
|
import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
@ -118,7 +117,8 @@ export class DeploymentDetails extends React.Component<Props> {
|
|||||||
<Badge
|
<Badge
|
||||||
key={type}
|
key={type}
|
||||||
label={type}
|
label={type}
|
||||||
className={cssNames({ disabled: status === "False" }, kebabCase(type))}
|
disabled={status === "False"}
|
||||||
|
className={kebabCase(type)}
|
||||||
tooltip={(
|
tooltip={(
|
||||||
<>
|
<>
|
||||||
<p>{message}</p>
|
<p>{message}</p>
|
||||||
|
|||||||
@ -22,8 +22,6 @@
|
|||||||
|
|
||||||
.JobDetails {
|
.JobDetails {
|
||||||
.conditions {
|
.conditions {
|
||||||
.Badge {
|
@include job-condition-bgs;
|
||||||
@include job-condition-bgs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -27,6 +27,7 @@ import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.ap
|
|||||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||||
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
|
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
|
||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
|
import { mapValues } from "lodash";
|
||||||
|
|
||||||
type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>;
|
type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>;
|
||||||
|
|
||||||
@ -37,10 +38,7 @@ export const ContainerCharts = observer(() => {
|
|||||||
if (!metrics) return null;
|
if (!metrics) return null;
|
||||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||||
|
|
||||||
const values = Object.values(metrics)
|
const {
|
||||||
.map(normalizeMetrics)
|
|
||||||
.map(({ data }) => data.result[0].values);
|
|
||||||
const [
|
|
||||||
cpuUsage,
|
cpuUsage,
|
||||||
cpuRequests,
|
cpuRequests,
|
||||||
cpuLimits,
|
cpuLimits,
|
||||||
@ -48,7 +46,7 @@ export const ContainerCharts = observer(() => {
|
|||||||
memoryRequests,
|
memoryRequests,
|
||||||
memoryLimits,
|
memoryLimits,
|
||||||
fsUsage
|
fsUsage
|
||||||
] = values;
|
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values);
|
||||||
|
|
||||||
const datasets = [
|
const datasets = [
|
||||||
// CPU
|
// CPU
|
||||||
|
|||||||
@ -19,15 +19,16 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useContext } from "react";
|
import { mapValues } from "lodash";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import type { IPodMetrics } from "../../api/endpoints";
|
import React, { useContext } from "react";
|
||||||
import { BarChart, cpuOptions, memoryOptions } from "../chart";
|
|
||||||
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
|
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
|
||||||
import { NoMetrics } from "../resource-metrics/no-metrics";
|
import { BarChart, cpuOptions, memoryOptions } from "../chart";
|
||||||
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
|
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
|
||||||
|
import { NoMetrics } from "../resource-metrics/no-metrics";
|
||||||
|
|
||||||
import type { WorkloadKubeObject } from "../../api/workload-kube-object";
|
import type { WorkloadKubeObject } from "../../api/workload-kube-object";
|
||||||
import { ThemeStore } from "../../theme.store";
|
import type { IPodMetrics } from "../../api/endpoints";
|
||||||
|
|
||||||
export const podMetricTabs = [
|
export const podMetricTabs = [
|
||||||
"CPU",
|
"CPU",
|
||||||
@ -40,27 +41,19 @@ type IContext = IResourceMetricsValue<WorkloadKubeObject, { metrics: IPodMetrics
|
|||||||
|
|
||||||
export const PodCharts = observer(() => {
|
export const PodCharts = observer(() => {
|
||||||
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
|
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
|
||||||
const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
|
|
||||||
const id = object.getId();
|
const id = object.getId();
|
||||||
|
|
||||||
if (!metrics) return null;
|
if (!metrics) return null;
|
||||||
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
|
||||||
|
|
||||||
const options = tabId == 0 ? cpuOptions : memoryOptions;
|
const options = tabId == 0 ? cpuOptions : memoryOptions;
|
||||||
const values = Object.values(metrics)
|
const {
|
||||||
.map(normalizeMetrics)
|
|
||||||
.map(({ data }) => data.result[0].values);
|
|
||||||
const [
|
|
||||||
cpuUsage,
|
cpuUsage,
|
||||||
cpuRequests,
|
|
||||||
cpuLimits,
|
|
||||||
memoryUsage,
|
memoryUsage,
|
||||||
memoryRequests,
|
|
||||||
memoryLimits,
|
|
||||||
fsUsage,
|
fsUsage,
|
||||||
networkReceive,
|
networkReceive,
|
||||||
networkTransmit
|
networkTransmit
|
||||||
] = values;
|
} = mapValues(metrics, metric => normalizeMetrics(metric).data.result[0].values);
|
||||||
|
|
||||||
const datasets = [
|
const datasets = [
|
||||||
// CPU
|
// CPU
|
||||||
@ -71,20 +64,6 @@ export const PodCharts = observer(() => {
|
|||||||
tooltip: `Container CPU cores usage`,
|
tooltip: `Container CPU cores usage`,
|
||||||
borderColor: "#3D90CE",
|
borderColor: "#3D90CE",
|
||||||
data: cpuUsage.map(([x, y]) => ({ x, y }))
|
data: cpuUsage.map(([x, y]) => ({ x, y }))
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `${id}-cpuRequests`,
|
|
||||||
label: `Requests`,
|
|
||||||
tooltip: `Container CPU requests`,
|
|
||||||
borderColor: "#30b24d",
|
|
||||||
data: cpuRequests.map(([x, y]) => ({ x, y }))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `${id}-cpuLimits`,
|
|
||||||
label: `Limits`,
|
|
||||||
tooltip: `CPU limits`,
|
|
||||||
borderColor: chartCapacityColor,
|
|
||||||
data: cpuLimits.map(([x, y]) => ({ x, y }))
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
// Memory
|
// Memory
|
||||||
@ -96,20 +75,6 @@ export const PodCharts = observer(() => {
|
|||||||
borderColor: "#c93dce",
|
borderColor: "#c93dce",
|
||||||
data: memoryUsage.map(([x, y]) => ({ x, y }))
|
data: memoryUsage.map(([x, y]) => ({ x, y }))
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: `${id}-memoryRequests`,
|
|
||||||
label: `Requests`,
|
|
||||||
tooltip: `Container memory requests`,
|
|
||||||
borderColor: "#30b24d",
|
|
||||||
data: memoryRequests.map(([x, y]) => ({ x, y }))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: `${id}-memoryLimits`,
|
|
||||||
label: `Limits`,
|
|
||||||
tooltip: `Container memory limits`,
|
|
||||||
borderColor: chartCapacityColor,
|
|
||||||
data: memoryLimits.map(([x, y]) => ({ x, y }))
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
// Network
|
// Network
|
||||||
[
|
[
|
||||||
|
|||||||
@ -58,8 +58,4 @@
|
|||||||
|
|
||||||
@include pod-status-colors;
|
@include pod-status-colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Badge {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -28,6 +28,10 @@
|
|||||||
&.virtual {
|
&.virtual {
|
||||||
height: 500px;
|
height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TableHead.sticky {
|
||||||
|
top: calc(var(--spacing) * -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.TableCell {
|
.TableCell {
|
||||||
|
|||||||
@ -142,7 +142,7 @@ export class PodDetails extends React.Component<Props> {
|
|||||||
<Badge
|
<Badge
|
||||||
key={type}
|
key={type}
|
||||||
label={type}
|
label={type}
|
||||||
className={cssNames({ disabled: status === "False" })}
|
disabled={status === "False"}
|
||||||
tooltip={`Last transition time: ${lastTransitionTime}`}
|
tooltip={`Last transition time: ${lastTransitionTime}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -35,6 +35,7 @@ enum sortBy {
|
|||||||
Operator = "operator",
|
Operator = "operator",
|
||||||
Effect = "effect",
|
Effect = "effect",
|
||||||
Seconds = "seconds",
|
Seconds = "seconds",
|
||||||
|
Value = "value",
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortingCallbacks = {
|
const sortingCallbacks = {
|
||||||
@ -45,7 +46,7 @@ const sortingCallbacks = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getTableRow = (toleration: IToleration) => {
|
const getTableRow = (toleration: IToleration) => {
|
||||||
const { key, operator, effect, tolerationSeconds } = toleration;
|
const { key, operator, effect, tolerationSeconds, value } = toleration;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
@ -55,6 +56,7 @@ const getTableRow = (toleration: IToleration) => {
|
|||||||
>
|
>
|
||||||
<TableCell className="key">{key}</TableCell>
|
<TableCell className="key">{key}</TableCell>
|
||||||
<TableCell className="operator">{operator}</TableCell>
|
<TableCell className="operator">{operator}</TableCell>
|
||||||
|
<TableCell className="value">{value}</TableCell>
|
||||||
<TableCell className="effect">{effect}</TableCell>
|
<TableCell className="effect">{effect}</TableCell>
|
||||||
<TableCell className="seconds">{tolerationSeconds}</TableCell>
|
<TableCell className="seconds">{tolerationSeconds}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -74,6 +76,7 @@ export function PodTolerations({ tolerations }: Props) {
|
|||||||
<TableHead sticky={false}>
|
<TableHead sticky={false}>
|
||||||
<TableCell className="key" sortBy={sortBy.Key}>Key</TableCell>
|
<TableCell className="key" sortBy={sortBy.Key}>Key</TableCell>
|
||||||
<TableCell className="operator" sortBy={sortBy.Operator}>Operator</TableCell>
|
<TableCell className="operator" sortBy={sortBy.Operator}>Operator</TableCell>
|
||||||
|
<TableCell className="value" sortBy={sortBy.Value}>Value</TableCell>
|
||||||
<TableCell className="effect" sortBy={sortBy.Effect}>Effect</TableCell>
|
<TableCell className="effect" sortBy={sortBy.Effect}>Effect</TableCell>
|
||||||
<TableCell className="seconds" sortBy={sortBy.Seconds}>Seconds</TableCell>
|
<TableCell className="seconds" sortBy={sortBy.Seconds}>Seconds</TableCell>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|||||||
@ -51,11 +51,9 @@ export class PodsStore extends KubeObjectStore<Pod> {
|
|||||||
|
|
||||||
async loadKubeMetrics(namespace?: string) {
|
async loadKubeMetrics(namespace?: string) {
|
||||||
try {
|
try {
|
||||||
const metrics = await podMetricsApi.list({ namespace });
|
this.kubeMetrics.replace(await podMetricsApi.list({ namespace }));
|
||||||
|
|
||||||
this.kubeMetrics.replace(metrics);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("loadKubeMetrics failed", error);
|
console.warn("loadKubeMetrics failed", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -127,7 +127,7 @@ export class Pods extends React.Component<Props> {
|
|||||||
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
|
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(pod: Pod) => [
|
renderTableContents={(pod: Pod) => [
|
||||||
<Badge flat key="name" label={pod.getName()} tooltip={pod.getName()} />,
|
<Badge flat key="name" label={pod.getName()} tooltip={pod.getName()} expandable={false} />,
|
||||||
<KubeObjectStatusIcon key="icon" object={pod} />,
|
<KubeObjectStatusIcon key="icon" object={pod} />,
|
||||||
pod.getNs(),
|
pod.getNs(),
|
||||||
this.renderContainersStatus(pod),
|
this.renderContainersStatus(pod),
|
||||||
@ -145,7 +145,7 @@ export class Pods extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
pod.getNodeName() ?
|
pod.getNodeName() ?
|
||||||
<Badge flat key="node" className="node" tooltip={pod.getNodeName()}>
|
<Badge flat key="node" className="node" tooltip={pod.getNodeName()} expandable={false}>
|
||||||
<Link to={getDetailsUrl(nodesApi.getUrl({ name: pod.getNodeName() }))} onClick={stopPropagation}>
|
<Link to={getDetailsUrl(nodesApi.getUrl({ name: pod.getNodeName() }))} onClick={stopPropagation}>
|
||||||
{pod.getNodeName()}
|
{pod.getNodeName()}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -98,7 +98,7 @@ $cronjob-condition-color-list: (
|
|||||||
|
|
||||||
@mixin job-condition-bgs {
|
@mixin job-condition-bgs {
|
||||||
@each $condition, $color in $job-condition-color-list {
|
@each $condition, $color in $job-condition-color-list {
|
||||||
&.#{$condition} {
|
.#{$condition} {
|
||||||
color: white;
|
color: white;
|
||||||
background: $color;
|
background: $color;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,73 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import "./app-init.scss";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { render } from "react-dom";
|
|
||||||
import { CubeSpinner } from "../spinner";
|
|
||||||
import { apiBase } from "../../api";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
serviceWaitingList?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AppInit extends React.Component<Props> {
|
|
||||||
static async start(rootElem: HTMLElement) {
|
|
||||||
render(<AppInit/>, rootElem); // show loading indicator asap
|
|
||||||
await AppInit.readyStateCheck(rootElem); // wait while all good to run
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static async readyStateCheck(rootElem: HTMLElement) {
|
|
||||||
const waitingList = await apiBase.get<string[]>("/ready");
|
|
||||||
|
|
||||||
if (waitingList.length > 0) {
|
|
||||||
// update waiting state
|
|
||||||
render(<AppInit serviceWaitingList={waitingList}/>, rootElem);
|
|
||||||
|
|
||||||
// check again in 1-5 seconds
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const timeoutDelay = 1000 + Math.random() * 4000;
|
|
||||||
|
|
||||||
setTimeout(() => resolve(AppInit.readyStateCheck(rootElem)), timeoutDelay);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { serviceWaitingList = [] } = this.props;
|
|
||||||
const serviceNames = serviceWaitingList.join(", ");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="AppInit flex column center">
|
|
||||||
<div className="box flex column gaps">
|
|
||||||
<h5>Lens - Loading..</h5>
|
|
||||||
{serviceWaitingList.length > 0 && (
|
|
||||||
<p className="waiting-services">
|
|
||||||
Waiting services to be running: <em className="text-secondary">{serviceNames}</em>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<CubeSpinner/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable, 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";
|
||||||
@ -69,6 +69,7 @@ import { Nodes } from "./+nodes";
|
|||||||
import { Workloads } from "./+workloads";
|
import { Workloads } from "./+workloads";
|
||||||
import { Config } from "./+config";
|
import { Config } from "./+config";
|
||||||
import { Storage } from "./+storage";
|
import { Storage } from "./+storage";
|
||||||
|
import { catalogEntityRegistry } from "../api/catalog-entity-registry";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -78,6 +79,7 @@ export class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async init() {
|
static async init() {
|
||||||
|
catalogEntityRegistry.init();
|
||||||
const frameId = webFrame.routingId;
|
const frameId = webFrame.routingId;
|
||||||
const clusterId = getHostedClusterId();
|
const clusterId = getHostedClusterId();
|
||||||
|
|
||||||
@ -86,6 +88,15 @@ export class App extends React.Component {
|
|||||||
|
|
||||||
await requestMain(clusterSetFrameIdHandler, clusterId);
|
await requestMain(clusterSetFrameIdHandler, clusterId);
|
||||||
await getHostedCluster().whenReady; // cluster.activate() is done at this point
|
await getHostedCluster().whenReady; // cluster.activate() is done at this point
|
||||||
|
|
||||||
|
const activeEntityDisposer = reaction(() => catalogEntityRegistry.getById(clusterId), (entity) => {
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catalogEntityRegistry.activeEntity = entity;
|
||||||
|
activeEntityDisposer();
|
||||||
|
}, {fireImmediately: true});
|
||||||
|
|
||||||
ExtensionLoader.getInstance().loadOnClusterRenderer();
|
ExtensionLoader.getInstance().loadOnClusterRenderer();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
appEventBus.emit({
|
appEventBus.emit({
|
||||||
|
|||||||
@ -19,25 +19,41 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.Badge {
|
.badge {
|
||||||
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge + .badge {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.interactive:hover {
|
||||||
|
background-color: var(--mainBackground);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge:not(.isExpanded) {
|
||||||
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
&:not(.flat) {
|
|
||||||
background: $colorVague;
|
.badge:not(.flat) {
|
||||||
color: $textColorSecondary;
|
background: var(--colorVague);
|
||||||
border-radius: $radius;
|
border-radius: 3px;
|
||||||
padding: .2em .4em;
|
padding: .2em .4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.small {
|
.small {
|
||||||
font-size: $font-size-small;
|
font-size: var(--font-size-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user