mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into fix/alibaba-detect
This commit is contained in:
commit
f4c59ba69e
@ -8,7 +8,7 @@ Lens provides the full situational awareness for everything that runs in Kuberne
|
|||||||
|
|
||||||
The Lens open source project is backed by a number of Kubernetes and cloud native ecosystem pioneers. It's a standalone application for MacOS, Windows and Linux operating systems. Lens is 100% open source and free of charge for any purpose.
|
The Lens open source project is backed by a number of Kubernetes and cloud native ecosystem pioneers. It's a standalone application for MacOS, Windows and Linux operating systems. Lens is 100% open source and free of charge for any purpose.
|
||||||
|
|
||||||
[](https://youtu.be/04v2ODsmtIs)
|
[](https://www.youtube.com/watch?v=eeDwdVXattc)
|
||||||
|
|
||||||
## What makes Lens special?
|
## What makes Lens special?
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ Lens is the most powerful Kubernetes IDE on the market. It is a standalone appli
|
|||||||
|
|
||||||
Watch this introductory video to see Lens in action:
|
Watch this introductory video to see Lens in action:
|
||||||
|
|
||||||
[](https://youtu.be/04v2ODsmtIs)
|
[](https://www.youtube.com/watch?v=eeDwdVXattc)
|
||||||
|
|
||||||
**Note:** Use CTRL+click (on Windows and Linux) or CMD+click (on MacOS) to open the above in a new tab
|
**Note:** Use CTRL+click (on Windows and Linux) or CMD+click (on MacOS) to open the above in a new tab
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,64 @@
|
|||||||
import { LensRendererExtension, Component } from "@k8slens/extensions";
|
|
||||||
import { CoffeeDoodle } from "react-open-doodles";
|
|
||||||
import path from "path";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { CoffeeDoodle } from "react-open-doodles";
|
||||||
|
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
|
||||||
|
|
||||||
export function ExampleIcon(props: Component.IconProps) {
|
export interface ExamplePageProps extends Interface.PageComponentProps<ExamplePageParams> {
|
||||||
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
|
extension: LensRendererExtension; // provided in "./renderer.tsx"
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
export interface ExamplePageParams {
|
||||||
|
exampleId: string;
|
||||||
|
selectedNamespaces: K8sApi.Namespace[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const namespaceStore = K8sApi.apiManager.getStore<K8sApi.NamespaceStore>(K8sApi.namespacesApi);
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class ExamplePage extends React.Component<ExamplePageProps> {
|
||||||
|
async componentDidMount() {
|
||||||
|
await namespaceStore.loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
deactivate = () => {
|
deactivate = () => {
|
||||||
const { extension } = this.props;
|
const { extension } = this.props;
|
||||||
|
|
||||||
extension.disable();
|
extension.disable();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
renderSelectedNamespaces() {
|
||||||
const doodleStyle = {
|
const { selectedNamespaces } = this.props.params;
|
||||||
width: "200px"
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex column gaps align-flex-start">
|
<div className="flex gaps inline">
|
||||||
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>
|
{selectedNamespaces.get().map(ns => {
|
||||||
<p>Hello from Example extension!</p>
|
const name = ns.getName();
|
||||||
<p>File: <i>{__filename}</i></p>
|
|
||||||
<Component.Button accent label="Deactivate" onClick={this.deactivate}/>
|
return <Component.Badge key={name} label={name} tooltip={`Created: ${ns.getAge()}`}/>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { exampleId } = this.props.params;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex column gaps align-flex-start" style={{ padding: 24 }}>
|
||||||
|
<div style={{ width: 200 }}>
|
||||||
|
<CoffeeDoodle accent="#3d90ce"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>Hello from Example extension!</div>
|
||||||
|
<div>Location: <i>{location.href}</i></div>
|
||||||
|
<div>Namespaces: {this.renderSelectedNamespaces()}</div>
|
||||||
|
|
||||||
|
<p className="url-params-demo flex column gaps">
|
||||||
|
<a onClick={() => exampleId.set("secret")}>Show secret button</a>
|
||||||
|
{exampleId.get() === "secret" && (
|
||||||
|
<Component.Button accent label="Deactivate" onClick={this.deactivate}/>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,45 @@
|
|||||||
import { LensRendererExtension } from "@k8slens/extensions";
|
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
|
||||||
import { ExampleIcon, ExamplePage } from "./page";
|
import { ExamplePage, ExamplePageParams, namespaceStore } from "./page";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
export default class ExampleExtension extends LensRendererExtension {
|
export default class ExampleExtension extends LensRendererExtension {
|
||||||
clusterPages = [
|
clusterPages: Interface.PageRegistration[] = [
|
||||||
{
|
{
|
||||||
id: "example",
|
|
||||||
title: "Example Extension",
|
|
||||||
components: {
|
components: {
|
||||||
Page: () => <ExamplePage extension={this}/>,
|
Page: (props: Interface.PageComponentProps<ExamplePageParams>) => {
|
||||||
|
return <ExamplePage {...props} extension={this}/>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
// setup basic param "exampleId" with default value "demo"
|
||||||
|
exampleId: "demo",
|
||||||
|
|
||||||
|
// setup advanced multi-values param "selectedNamespaces" with custom parsing/stringification
|
||||||
|
selectedNamespaces: {
|
||||||
|
defaultValueStringified: ["default", "kube-system"],
|
||||||
|
multiValues: true,
|
||||||
|
parse(values: string[]) { // from URL
|
||||||
|
return values.map(name => namespaceStore.getByName(name)).filter(Boolean);
|
||||||
|
},
|
||||||
|
stringify(values: K8sApi.Namespace[]) { // to URL
|
||||||
|
return values.map(namespace => namespace.getName());
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
clusterPageMenus = [
|
clusterPageMenus: Interface.ClusterPageMenuRegistration[] = [
|
||||||
{
|
{
|
||||||
target: { pageId: "example", params: {} },
|
title: "Example extension",
|
||||||
title: "Example Extension",
|
|
||||||
components: {
|
components: {
|
||||||
Icon: ExampleIcon,
|
Icon: ExampleIcon,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ExampleIcon(props: Component.IconProps) {
|
||||||
|
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
|
||||||
|
}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ spec:
|
|||||||
serviceAccountName: kube-state-metrics
|
serviceAccountName: kube-state-metrics
|
||||||
containers:
|
containers:
|
||||||
- name: kube-state-metrics
|
- name: kube-state-metrics
|
||||||
image: quay.io/coreos/kube-state-metrics:v1.9.5
|
image: quay.io/coreos/kube-state-metrics:v1.9.7
|
||||||
ports:
|
ports:
|
||||||
- name: metrics
|
- name: metrics
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export interface MetricsConfiguration {
|
|||||||
|
|
||||||
export class MetricsFeature extends ClusterFeature.Feature {
|
export class MetricsFeature extends ClusterFeature.Feature {
|
||||||
name = "metrics";
|
name = "metrics";
|
||||||
latestVersion = "v2.17.2-lens1";
|
latestVersion = "v2.17.2-lens2";
|
||||||
|
|
||||||
templateContext: MetricsConfiguration = {
|
templateContext: MetricsConfiguration = {
|
||||||
persistence: {
|
persistence: {
|
||||||
|
|||||||
33
package.json
33
package.json
@ -198,20 +198,6 @@
|
|||||||
"@hapi/call": "^8.0.0",
|
"@hapi/call": "^8.0.0",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.12.0",
|
"@kubernetes/client-node": "^0.12.0",
|
||||||
"@types/crypto-js": "^3.1.47",
|
|
||||||
"@types/electron-window-state": "^2.0.34",
|
|
||||||
"@types/fs-extra": "^9.0.1",
|
|
||||||
"@types/http-proxy": "^1.17.4",
|
|
||||||
"@types/js-yaml": "^3.12.4",
|
|
||||||
"@types/jsdom": "^16.2.4",
|
|
||||||
"@types/jsonpath": "^0.2.0",
|
|
||||||
"@types/lodash": "^4.14.155",
|
|
||||||
"@types/marked": "^0.7.4",
|
|
||||||
"@types/mock-fs": "^4.10.0",
|
|
||||||
"@types/node": "^12.12.45",
|
|
||||||
"@types/proper-lockfile": "^4.1.1",
|
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
|
||||||
"@types/tar": "^4.0.4",
|
|
||||||
"array-move": "^3.0.0",
|
"array-move": "^3.0.0",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
@ -235,12 +221,17 @@
|
|||||||
"md5-file": "^5.0.0",
|
"md5-file": "^5.0.0",
|
||||||
"mobx": "^5.15.7",
|
"mobx": "^5.15.7",
|
||||||
"mobx-observable-history": "^1.0.3",
|
"mobx-observable-history": "^1.0.3",
|
||||||
|
"mobx-react": "^6.2.2",
|
||||||
"mock-fs": "^4.12.0",
|
"mock-fs": "^4.12.0",
|
||||||
"node-pty": "^0.9.0",
|
"node-pty": "^0.9.0",
|
||||||
"npm": "^6.14.8",
|
"npm": "^6.14.8",
|
||||||
"openid-client": "^3.15.2",
|
"openid-client": "^3.15.2",
|
||||||
|
"p-limit": "^3.1.0",
|
||||||
"path-to-regexp": "^6.1.0",
|
"path-to-regexp": "^6.1.0",
|
||||||
"proper-lockfile": "^4.1.1",
|
"proper-lockfile": "^4.1.1",
|
||||||
|
"react": "^17.0.1",
|
||||||
|
"react-dom": "^17.0.1",
|
||||||
|
"react-router": "^5.2.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise-native": "^1.0.8",
|
"request-promise-native": "^1.0.8",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
@ -287,10 +278,10 @@
|
|||||||
"@types/http-proxy": "^1.17.4",
|
"@types/http-proxy": "^1.17.4",
|
||||||
"@types/jest": "^25.2.3",
|
"@types/jest": "^25.2.3",
|
||||||
"@types/js-yaml": "^3.12.4",
|
"@types/js-yaml": "^3.12.4",
|
||||||
|
"@types/jsdom": "^16.2.4",
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
"@types/lodash": "^4.14.155",
|
"@types/lodash": "^4.14.155",
|
||||||
"@types/marked": "^0.7.4",
|
"@types/marked": "^0.7.4",
|
||||||
"@types/material-ui": "^0.21.7",
|
|
||||||
"@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.10.0",
|
||||||
@ -299,9 +290,10 @@
|
|||||||
"@types/npm": "^2.0.31",
|
"@types/npm": "^2.0.31",
|
||||||
"@types/progress-bar-webpack-plugin": "^2.1.0",
|
"@types/progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"@types/proper-lockfile": "^4.1.1",
|
"@types/proper-lockfile": "^4.1.1",
|
||||||
"@types/react": "^16.9.35",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-router-dom": "^5.1.5",
|
"@types/react-dom": "^17.0.0",
|
||||||
|
"@types/react-router-dom": "^5.1.6",
|
||||||
"@types/react-select": "^3.0.13",
|
"@types/react-select": "^3.0.13",
|
||||||
"@types/react-window": "^1.8.2",
|
"@types/react-window": "^1.8.2",
|
||||||
"@types/request": "^2.48.5",
|
"@types/request": "^2.48.5",
|
||||||
@ -310,6 +302,7 @@
|
|||||||
"@types/sharp": "^0.26.0",
|
"@types/sharp": "^0.26.0",
|
||||||
"@types/shelljs": "^0.8.8",
|
"@types/shelljs": "^0.8.8",
|
||||||
"@types/spdy": "^3.4.4",
|
"@types/spdy": "^3.4.4",
|
||||||
|
"@types/tar": "^4.0.4",
|
||||||
"@types/tcp-port-used": "^1.0.0",
|
"@types/tcp-port-used": "^1.0.0",
|
||||||
"@types/tempy": "^0.3.0",
|
"@types/tempy": "^0.3.0",
|
||||||
"@types/terser-webpack-plugin": "^3.0.0",
|
"@types/terser-webpack-plugin": "^3.0.0",
|
||||||
@ -353,7 +346,6 @@
|
|||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.10",
|
||||||
"make-plural": "^6.2.1",
|
"make-plural": "^6.2.1",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"mobx-react": "^6.2.2",
|
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.26.0",
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
@ -363,11 +355,8 @@
|
|||||||
"prettier": "^2.2.0",
|
"prettier": "^2.2.0",
|
||||||
"progress-bar-webpack-plugin": "^2.1.0",
|
"progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"raw-loader": "^4.0.1",
|
"raw-loader": "^4.0.1",
|
||||||
"react": "^16.14.0",
|
|
||||||
"react-beautiful-dnd": "^13.0.0",
|
"react-beautiful-dnd": "^13.0.0",
|
||||||
"react-dom": "^16.13.1",
|
|
||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
"react-router": "^5.2.0",
|
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-select": "^3.1.0",
|
"react-select": "^3.1.0",
|
||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
@ -383,7 +372,7 @@
|
|||||||
"typedoc": "0.17.0-3",
|
"typedoc": "0.17.0-3",
|
||||||
"typedoc-plugin-markdown": "^2.4.0",
|
"typedoc-plugin-markdown": "^2.4.0",
|
||||||
"typeface-roboto": "^0.0.75",
|
"typeface-roboto": "^0.0.75",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "4.0.2",
|
||||||
"url-loader": "^4.1.0",
|
"url-loader": "^4.1.0",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.44.2",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
|
|||||||
@ -14,7 +14,6 @@ export * from "./splitArray";
|
|||||||
export * from "./saveToAppFiles";
|
export * from "./saveToAppFiles";
|
||||||
export * from "./singleton";
|
export * from "./singleton";
|
||||||
export * from "./openExternal";
|
export * from "./openExternal";
|
||||||
export * from "./rectify-array";
|
|
||||||
export * from "./downloadFile";
|
export * from "./downloadFile";
|
||||||
export * from "./escapeRegExp";
|
export * from "./escapeRegExp";
|
||||||
export * from "./tar";
|
export * from "./tar";
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* rectify condences the single item or array of T type, to an array.
|
|
||||||
* @param items either one item or an array of items
|
|
||||||
* @returns a list of items
|
|
||||||
*/
|
|
||||||
export function rectify<T>(items: T | T[]): T[] {
|
|
||||||
return Array.isArray(items) ? items : [items];
|
|
||||||
}
|
|
||||||
@ -41,7 +41,7 @@ export abstract class ClusterFeature {
|
|||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be installed. The implementation
|
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be installed. The implementation
|
||||||
* of this method should install kubernetes resources using the applyResources() method, or by directly accessing the kubernetes api (K8sApi)
|
* of this method should install kubernetes resources using the applyResources() method, or by directly accessing the kubernetes api (K8sApi)
|
||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be installed on
|
* @param cluster the cluster that the feature is to be installed on
|
||||||
*/
|
*/
|
||||||
abstract async install(cluster: Cluster): Promise<void>;
|
abstract async install(cluster: Cluster): Promise<void>;
|
||||||
@ -49,7 +49,7 @@ export abstract class ClusterFeature {
|
|||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
|
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
|
||||||
* of this method should upgrade the kubernetes resources already installed, if relevant to the feature
|
* of this method should upgrade the kubernetes resources already installed, if relevant to the feature
|
||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be upgraded on
|
* @param cluster the cluster that the feature is to be upgraded on
|
||||||
*/
|
*/
|
||||||
abstract async upgrade(cluster: Cluster): Promise<void>;
|
abstract async upgrade(cluster: Cluster): Promise<void>;
|
||||||
@ -57,26 +57,26 @@ export abstract class ClusterFeature {
|
|||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
|
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
|
||||||
* of this method should uninstall kubernetes resources using the kubernetes api (K8sApi)
|
* of this method should uninstall kubernetes resources using the kubernetes api (K8sApi)
|
||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature is to be uninstalled from
|
* @param cluster the cluster that the feature is to be uninstalled from
|
||||||
*/
|
*/
|
||||||
abstract async uninstall(cluster: Cluster): Promise<void>;
|
abstract async uninstall(cluster: Cluster): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
|
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
|
||||||
* of this method should provide the current status information. The currentVersion and latestVersion fields may be displayed by Lens in describing the feature.
|
* of this method should provide the current status information. The currentVersion and latestVersion fields may be displayed by Lens in describing the feature.
|
||||||
* The installed field should be set to true if the feature has been installed, otherwise false. Also, Lens relies on the canUpgrade field to determine if the feature
|
* The installed field should be set to true if the feature has been installed, otherwise false. Also, Lens relies on the canUpgrade field to determine if the feature
|
||||||
* can be upgraded so the implementation should set the canUpgrade field according to specific rules for the feature, if relevant.
|
* can be upgraded so the implementation should set the canUpgrade field according to specific rules for the feature, if relevant.
|
||||||
*
|
*
|
||||||
* @param cluster the cluster that the feature may be installed on
|
* @param cluster the cluster that the feature may be installed on
|
||||||
*
|
*
|
||||||
* @return a promise, resolved with the updated ClusterFeatureStatus
|
* @return a promise, resolved with the updated ClusterFeatureStatus
|
||||||
*/
|
*/
|
||||||
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* this is a helper method that conveniently applies kubernetes resources to the cluster.
|
* this is a helper method that conveniently applies kubernetes resources to the cluster.
|
||||||
*
|
*
|
||||||
* @param cluster the cluster that the resources are to be applied to
|
* @param cluster the cluster that the resources are to be applied to
|
||||||
* @param resourceSpec as a string type this is a folder path that is searched for files specifying kubernetes resources. The files are read and if any of the resource
|
* @param resourceSpec as a string type this is a folder path that is searched for files specifying kubernetes resources. The files are read and if any of the resource
|
||||||
* files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
|
* files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
|
||||||
@ -101,9 +101,9 @@ export abstract class ClusterFeature {
|
|||||||
/**
|
/**
|
||||||
* this is a helper method that conveniently reads kubernetes resource files into a string array. It also fills templated resource files with the template parameter values
|
* this is a helper method that conveniently reads kubernetes resource files into a string array. It also fills templated resource files with the template parameter values
|
||||||
* specified by the templateContext field. Templated files must end with the extension '.hb' and the template syntax must be compatible with handlebars.js
|
* specified by the templateContext field. Templated files must end with the extension '.hb' and the template syntax must be compatible with handlebars.js
|
||||||
*
|
*
|
||||||
* @param folderPath this is a folder path that is searched for files defining kubernetes resources.
|
* @param folderPath this is a folder path that is searched for files defining kubernetes resources.
|
||||||
*
|
*
|
||||||
* @return an array of strings, each string being the contents of a resource file found in the folder path. This can be passed directly to applyResources()
|
* @return an array of strings, each string being the contents of a resource file found in the folder path. This can be passed directly to applyResources()
|
||||||
*/
|
*/
|
||||||
protected renderTemplates(folderPath: string): string[] {
|
protected renderTemplates(folderPath: string): string[] {
|
||||||
|
|||||||
@ -3,6 +3,6 @@ export type { ClusterFeatureRegistration, ClusterFeatureComponents } from "../re
|
|||||||
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
|
export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry";
|
||||||
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
|
export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry";
|
||||||
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
|
export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry";
|
||||||
export type { PageRegistration, PageComponents } from "../registries/page-registry";
|
export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps, PageComponents, PageTarget } from "../registries/page-registry";
|
||||||
export type { PageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
|
export type { PageMenuRegistration, ClusterPageMenuRegistration, PageMenuComponents } from "../registries/page-menu-registry";
|
||||||
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
||||||
@ -1,14 +1,13 @@
|
|||||||
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } from "./registries";
|
import type { AppPreferenceRegistration, ClusterFeatureRegistration, ClusterPageMenuRegistration, KubeObjectDetailRegistration, KubeObjectMenuRegistration, KubeObjectStatusRegistration, PageMenuRegistration, PageRegistration, StatusBarRegistration, } 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";
|
||||||
|
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: PageRegistration[] = [];
|
globalPages: PageRegistration[] = [];
|
||||||
clusterPages: PageRegistration[] = [];
|
clusterPages: PageRegistration[] = [];
|
||||||
globalPageMenus: PageMenuRegistration[] = [];
|
globalPageMenus: PageMenuRegistration[] = [];
|
||||||
clusterPageMenus: PageMenuRegistration[] = [];
|
clusterPageMenus: ClusterPageMenuRegistration[] = [];
|
||||||
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
|
||||||
appPreferences: AppPreferenceRegistration[] = [];
|
appPreferences: AppPreferenceRegistration[] = [];
|
||||||
clusterFeatures: ClusterFeatureRegistration[] = [];
|
clusterFeatures: ClusterFeatureRegistration[] = [];
|
||||||
|
|||||||
@ -16,8 +16,10 @@
|
|||||||
"name": "Mirantis, Inc.",
|
"name": "Mirantis, Inc.",
|
||||||
"email": "info@k8slens.dev"
|
"email": "info@k8slens.dev"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^14.14.6",
|
"@types/node": "*",
|
||||||
|
"@types/react-select": "*",
|
||||||
|
"@material-ui/core": "*",
|
||||||
"conf": "^7.0.1"
|
"conf": "^7.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { getExtensionPageUrl, globalPageRegistry } from "../page-registry";
|
import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry";
|
||||||
import { LensExtension } from "../../lens-extension";
|
import { LensExtension } from "../../lens-extension";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
@ -17,6 +17,16 @@ describe("getPageUrl", () => {
|
|||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true
|
isEnabled: true
|
||||||
});
|
});
|
||||||
|
globalPageRegistry.add({
|
||||||
|
id: "page-with-params",
|
||||||
|
components: {
|
||||||
|
Page: () => React.createElement("Page with params")
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
test1: "test1-default",
|
||||||
|
test2: "" // no default value, just declaration
|
||||||
|
},
|
||||||
|
}, ext);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a page url for extension", () => {
|
it("returns a page url for extension", () => {
|
||||||
@ -34,6 +44,24 @@ describe("getPageUrl", () => {
|
|||||||
it("adds / prefix", () => {
|
it("adds / prefix", () => {
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
|
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<string> = { 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", () => {
|
describe("globalPageRegistry", () => {
|
||||||
@ -70,17 +98,17 @@ describe("globalPageRegistry", () => {
|
|||||||
], ext);
|
], ext);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getByPageMenuTarget", () => {
|
describe("getByPageTarget", () => {
|
||||||
it("matching to first registered page without id", () => {
|
it("matching to first registered page without id", () => {
|
||||||
const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
|
const page = globalPageRegistry.getByPageTarget({ extensionId: ext.name });
|
||||||
|
|
||||||
expect(page.id).toEqual(undefined);
|
expect(page.id).toEqual(undefined);
|
||||||
expect(page.extensionId).toEqual(ext.name);
|
expect(page.extensionId).toEqual(ext.name);
|
||||||
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns matching page", () => {
|
it("returns matching page", () => {
|
||||||
const page = globalPageRegistry.getByPageMenuTarget({
|
const page = globalPageRegistry.getByPageTarget({
|
||||||
pageId: "test-page",
|
pageId: "test-page",
|
||||||
extensionId: ext.name
|
extensionId: ext.name
|
||||||
});
|
});
|
||||||
@ -89,7 +117,7 @@ describe("globalPageRegistry", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns null if target not found", () => {
|
it("returns null if target not found", () => {
|
||||||
const page = globalPageRegistry.getByPageMenuTarget({
|
const page = globalPageRegistry.getByPageTarget({
|
||||||
pageId: "wrong-page",
|
pageId: "wrong-page",
|
||||||
extensionId: ext.name
|
extensionId: ext.name
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,29 +1,34 @@
|
|||||||
// Base class for extensions-api registries
|
// Base class for extensions-api registries
|
||||||
import { action, observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { LensExtension } from "../lens-extension";
|
import { LensExtension } from "../lens-extension";
|
||||||
import { rectify } from "../../common/utils";
|
|
||||||
|
|
||||||
export class BaseRegistry<T> {
|
export class BaseRegistry<T, I = T> {
|
||||||
private items = observable<T>([], { deep: false });
|
private items = observable.map<T, I>();
|
||||||
|
|
||||||
getItems(): T[] {
|
getItems(): I[] {
|
||||||
return this.items.toJS();
|
return Array.from(this.items.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
add(items: T | T[], ext?: LensExtension): () => void; // allow method overloading with required "ext"
|
|
||||||
@action
|
@action
|
||||||
add(items: T | T[]) {
|
add(items: T | T[], extension?: LensExtension) {
|
||||||
const itemArray = rectify(items);
|
const itemArray = [items].flat() as T[];
|
||||||
|
|
||||||
this.items.push(...itemArray);
|
itemArray.forEach(item => {
|
||||||
|
this.items.set(item, this.getRegisteredItem(item, extension));
|
||||||
|
});
|
||||||
|
|
||||||
return () => this.remove(...itemArray);
|
return () => this.remove(...itemArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||||
|
protected getRegisteredItem(item: T, extension?: LensExtension): I {
|
||||||
|
return item as any;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
remove(...items: T[]) {
|
remove(...items: T[]) {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
this.items.remove(item); // works because of {deep: false};
|
this.items.delete(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,13 @@
|
|||||||
// Extensions-api -> Register page menu items
|
// Extensions-api -> Register page menu items
|
||||||
import type { IconProps } from "../../renderer/components/icon";
|
import type { IconProps } from "../../renderer/components/icon";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
import type { PageTarget, RegisteredPage } from "./page-registry";
|
||||||
import { action } from "mobx";
|
import { action } from "mobx";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
import { LensExtension } from "../lens-extension";
|
import { LensExtension } from "../lens-extension";
|
||||||
import { RegisteredPage } from "./page-registry";
|
|
||||||
|
|
||||||
export interface PageMenuTarget<P extends object = any> {
|
|
||||||
extensionId?: string;
|
|
||||||
pageId?: string;
|
|
||||||
params?: P;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageMenuRegistration {
|
export interface PageMenuRegistration {
|
||||||
target?: PageMenuTarget;
|
target?: PageTarget;
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
components: PageMenuComponents;
|
components: PageMenuComponents;
|
||||||
}
|
}
|
||||||
@ -27,9 +21,9 @@ export interface PageMenuComponents {
|
|||||||
Icon: React.ComponentType<IconProps>;
|
Icon: React.ComponentType<IconProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
|
export class PageMenuRegistry<T extends PageMenuRegistration> extends BaseRegistry<T> {
|
||||||
@action
|
@action
|
||||||
add(items: PageMenuRegistration[], ext: LensExtension) {
|
add(items: T[], ext: LensExtension) {
|
||||||
const normalizedItems = items.map(menuItem => {
|
const normalizedItems = items.map(menuItem => {
|
||||||
menuItem.target = {
|
menuItem.target = {
|
||||||
extensionId: ext.name,
|
extensionId: ext.name,
|
||||||
@ -43,33 +37,25 @@ export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClusterPageMenuRegistry extends BaseRegistry<ClusterPageMenuRegistration> {
|
export class ClusterPageMenuRegistry extends PageMenuRegistry<ClusterPageMenuRegistration> {
|
||||||
@action
|
|
||||||
add(items: PageMenuRegistration[], ext: LensExtension) {
|
|
||||||
const normalizedItems = items.map(menuItem => {
|
|
||||||
menuItem.target = {
|
|
||||||
extensionId: ext.name,
|
|
||||||
...(menuItem.target || {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return menuItem;
|
|
||||||
});
|
|
||||||
|
|
||||||
return super.add(normalizedItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRootItems() {
|
getRootItems() {
|
||||||
return this.getItems().filter((item) => !item.parentId);
|
return this.getItems().filter((item) => !item.parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSubItems(parent: ClusterPageMenuRegistration) {
|
getSubItems(parent: ClusterPageMenuRegistration) {
|
||||||
return this.getItems().filter((item) => item.parentId === parent.id && item.target.extensionId === parent.target.extensionId);
|
return this.getItems().filter((item) => (
|
||||||
|
item.parentId === parent.id &&
|
||||||
|
item.target.extensionId === parent.target.extensionId
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
getByPage(page: RegisteredPage) {
|
getByPage({ id: pageId, extensionId }: RegisteredPage) {
|
||||||
return this.getItems().find((item) => item.target?.pageId == page.id && item.target?.extensionId === page.extensionId);
|
return this.getItems().find((item) => (
|
||||||
|
item.target.pageId == pageId &&
|
||||||
|
item.target.extensionId === extensionId
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const globalPageMenuRegistry = new GlobalPageMenuRegistry();
|
export const globalPageMenuRegistry = new PageMenuRegistry();
|
||||||
export const clusterPageMenuRegistry = new ClusterPageMenuRegistry();
|
export const clusterPageMenuRegistry = new ClusterPageMenuRegistry();
|
||||||
|
|||||||
@ -1,93 +1,120 @@
|
|||||||
// Extensions-api -> Custom page registration
|
// Extensions-api -> Custom page registration
|
||||||
import type { PageMenuTarget } from "./page-menu-registry";
|
|
||||||
import type React from "react";
|
import React from "react";
|
||||||
import path from "path";
|
import { observer } from "mobx-react";
|
||||||
import { action } from "mobx";
|
|
||||||
import { compile } from "path-to-regexp";
|
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
|
||||||
import logger from "../../main/logger";
|
import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
||||||
import { rectify } from "../../common/utils";
|
import { createPageParam } from "../../renderer/navigation/helpers";
|
||||||
|
|
||||||
export interface PageRegistration {
|
export interface PageRegistration {
|
||||||
/**
|
/**
|
||||||
* Page ID or additional route path to indicate uniqueness within current extension registered pages
|
* Page ID, part of extension's page url, must be unique within same extension
|
||||||
* Might contain special url placeholders, e.g. "/users/:userId?" (? - marks as optional param)
|
|
||||||
* When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension
|
* When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension
|
||||||
*/
|
*/
|
||||||
id?: string;
|
id?: string;
|
||||||
/**
|
params?: PageParams<string | ExtensionPageParamInit>;
|
||||||
* Strict route matching to provided page-id, read also: https://reactrouter.com/web/api/NavLink/exact-bool
|
|
||||||
* In case when more than one page registered at same extension "pageId" is required to identify different pages,
|
|
||||||
* It might be useful to provide `exact: true` in some cases to avoid overlapping routes.
|
|
||||||
* Without {exact:true} second page never matches since first page-id/route already includes partial route.
|
|
||||||
* @example const pages = [
|
|
||||||
* {id: "/users", exact: true},
|
|
||||||
* {id: "/users/:userId?"}
|
|
||||||
* ]
|
|
||||||
* Pro-tip: registering pages in opposite order will make same effect without "exact".
|
|
||||||
*/
|
|
||||||
exact?: boolean;
|
|
||||||
components: PageComponents;
|
components: PageComponents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisteredPage extends PageRegistration {
|
// exclude "name" field since provided as key in page.params
|
||||||
extensionId: string; // required for compiling registered page to url with page-menu-target to compare
|
export type ExtensionPageParamInit = Omit<PageParamInit, "name" | "isSystem">;
|
||||||
routePath: string; // full route-path to registered extension page
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageComponents {
|
export interface PageComponents {
|
||||||
Page: React.ComponentType<any>;
|
Page: React.ComponentType<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExtensionPageUrl<P extends object>({ extensionId, pageId = "", params }: PageMenuTarget<P>): string {
|
export interface PageTarget<P = PageParams> {
|
||||||
const extensionBaseUrl = compile(`/extension/:name`)({
|
extensionId?: string;
|
||||||
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
|
pageId?: string;
|
||||||
});
|
params?: P;
|
||||||
const extPageRoutePath = path.posix.join(extensionBaseUrl, pageId);
|
|
||||||
|
|
||||||
if (params) {
|
|
||||||
return compile(extPageRoutePath)(params); // might throw error when required params not passed
|
|
||||||
}
|
|
||||||
|
|
||||||
return extPageRoutePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
export interface PageParams<V = any> {
|
||||||
@action
|
[paramName: string]: V;
|
||||||
add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
|
}
|
||||||
const itemArray = rectify(items);
|
|
||||||
let registeredPages: RegisteredPage[] = [];
|
|
||||||
|
|
||||||
try {
|
export interface PageComponentProps<P extends PageParams = {}> {
|
||||||
registeredPages = itemArray.map(page => ({
|
params?: {
|
||||||
...page,
|
[N in keyof P]: PageParam<P[N]>;
|
||||||
extensionId: ext.name,
|
}
|
||||||
routePath: getExtensionPageUrl({ extensionId: ext.name, pageId: page.id }),
|
}
|
||||||
}));
|
|
||||||
} catch (err) {
|
export interface RegisteredPage {
|
||||||
logger.error(`[EXTENSION]: page-registration failed`, {
|
id: string;
|
||||||
items,
|
extensionId: string;
|
||||||
extension: ext,
|
url: string; // registered extension's page URL (without page params)
|
||||||
error: String(err),
|
params: PageParams<PageParam>; // normalized params
|
||||||
});
|
components: PageComponents; // normalized components
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExtensionPageUrl(target: PageTarget): string {
|
||||||
|
const { extensionId, pageId = "", params: targetParams = {} } = target;
|
||||||
|
|
||||||
|
const pagePath = ["/extension", sanitizeExtensionName(extensionId), pageId]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id)
|
||||||
|
|
||||||
|
const pageUrl = new URL(pagePath, `http://localhost`);
|
||||||
|
|
||||||
|
// stringify params to matched target page
|
||||||
|
const registeredPage = globalPageRegistry.getByPageTarget(target) || clusterPageRegistry.getByPageTarget(target);
|
||||||
|
|
||||||
|
if (registeredPage?.params) {
|
||||||
|
Object.entries(registeredPage.params).forEach(([name, param]) => {
|
||||||
|
const paramValue = param.stringify(targetParams[name]);
|
||||||
|
|
||||||
|
if (param.init.skipEmpty && param.isEmpty(paramValue)) {
|
||||||
|
pageUrl.searchParams.delete(name);
|
||||||
|
} else {
|
||||||
|
pageUrl.searchParams.set(name, paramValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageUrl.href.replace(pageUrl.origin, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
|
||||||
|
protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage {
|
||||||
|
const { id: pageId } = page;
|
||||||
|
const extensionId = ext.name;
|
||||||
|
const params = this.normalizeParams(page.params);
|
||||||
|
const components = this.normalizeComponents(page.components, params);
|
||||||
|
const url = getExtensionPageUrl({ extensionId, pageId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: pageId, extensionId, params, components, url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected normalizeComponents(components: PageComponents, params?: PageParams<PageParam>): PageComponents {
|
||||||
|
if (params) {
|
||||||
|
const { Page } = components;
|
||||||
|
|
||||||
|
components.Page = observer((props: object) => React.createElement(Page, { params, ...props }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.add(registeredPages);
|
return components;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrl<P extends object>({ extensionId, id: pageId }: RegisteredPage, params?: P) {
|
protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> {
|
||||||
return getExtensionPageUrl({ extensionId, pageId, params });
|
if (!params) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.entries(params).forEach(([name, value]) => {
|
||||||
|
const paramInit: PageParamInit = typeof value === "object"
|
||||||
|
? { name, ...value }
|
||||||
|
: { name, defaultValue: value };
|
||||||
|
|
||||||
|
params[paramInit.name] = createPageParam(paramInit);
|
||||||
|
});
|
||||||
|
|
||||||
|
return params as PageParams<PageParam>;
|
||||||
}
|
}
|
||||||
|
|
||||||
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null {
|
getByPageTarget(target: PageTarget): RegisteredPage | null {
|
||||||
const targetUrl = getExtensionPageUrl(target);
|
return this.getItems().find(page => page.extensionId === target.extensionId && page.id === target.pageId) || null;
|
||||||
|
|
||||||
return this.getItems().find(({ id: pageId, extensionId }) => {
|
|
||||||
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params
|
|
||||||
|
|
||||||
return targetUrl === pageUrl;
|
|
||||||
}) || null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,12 @@
|
|||||||
export { navigate } from "../../renderer/navigation";
|
import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
|
||||||
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/navigation";
|
import { navigation } from "../../renderer/navigation";
|
||||||
|
|
||||||
|
export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
|
||||||
|
export { navigate, isActiveRoute } from "../../renderer/navigation/helpers";
|
||||||
|
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
|
||||||
export { IURLParams } from "../../common/utils/buildUrl";
|
export { IURLParams } from "../../common/utils/buildUrl";
|
||||||
|
|
||||||
|
// exporting to extensions-api version of helper without `isSystem` flag
|
||||||
|
export function createPageParam<V = string>(init: PageParamInit<V>) {
|
||||||
|
return new PageParam<V>(init, navigation);
|
||||||
|
}
|
||||||
|
|||||||
@ -44,6 +44,10 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
return { value: "alibaba", accuracy: 90};
|
return { value: "alibaba", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isHuawei()) {
|
||||||
|
return { value: "huawei", accuracy: 90};
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isMinikube()) {
|
if (this.isMinikube()) {
|
||||||
return { value: "minikube", accuracy: 80};
|
return { value: "minikube", accuracy: 80};
|
||||||
}
|
}
|
||||||
@ -135,6 +139,10 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
return this.version.includes("-aliyun");
|
return this.version.includes("-aliyun");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isHuawei() {
|
||||||
|
return this.version.includes("-CCE");
|
||||||
|
}
|
||||||
|
|
||||||
protected async isOpenshift() {
|
protected async isOpenshift() {
|
||||||
try {
|
try {
|
||||||
const response = await this.k8sRequest("");
|
const response = await this.k8sRequest("");
|
||||||
|
|||||||
@ -11,10 +11,11 @@ import { Kubectl } from "./kubectl";
|
|||||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||||
import { loadConfig } from "../common/kube-helpers";
|
import { loadConfig } from "../common/kube-helpers";
|
||||||
import request, { RequestPromiseOptions } from "request-promise-native";
|
import request, { RequestPromiseOptions } from "request-promise-native";
|
||||||
import { apiResources } from "../common/rbac";
|
import { apiResources, KubeApiResource } from "../common/rbac";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { VersionDetector } from "./cluster-detectors/version-detector";
|
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";
|
||||||
|
|
||||||
export enum ClusterStatus {
|
export enum ClusterStatus {
|
||||||
AccessGranted = 2,
|
AccessGranted = 2,
|
||||||
@ -78,6 +79,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
protected kubeconfigManager: KubeconfigManager;
|
protected kubeconfigManager: KubeconfigManager;
|
||||||
protected eventDisposers: Function[] = [];
|
protected eventDisposers: Function[] = [];
|
||||||
protected activated = false;
|
protected activated = false;
|
||||||
|
private resourceAccessStatuses: Map<KubeApiResource, boolean> = new Map();
|
||||||
|
|
||||||
whenInitialized = when(() => this.initialized);
|
whenInitialized = when(() => this.initialized);
|
||||||
whenReady = when(() => this.ready);
|
whenReady = when(() => this.ready);
|
||||||
@ -379,6 +381,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.accessible = false;
|
this.accessible = false;
|
||||||
this.ready = false;
|
this.ready = false;
|
||||||
this.activated = false;
|
this.activated = false;
|
||||||
|
this.resourceAccessStatuses.clear();
|
||||||
this.pushState();
|
this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,6 +487,8 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
this.metadata.version = versionData.value;
|
this.metadata.version = versionData.value;
|
||||||
|
|
||||||
|
this.failureReason = null;
|
||||||
|
|
||||||
return ClusterStatus.AccessGranted;
|
return ClusterStatus.AccessGranted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
|
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
|
||||||
@ -643,17 +648,30 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
if (!this.allowedNamespaces.length) {
|
if (!this.allowedNamespaces.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const resourceAccessStatuses = await Promise.all(
|
const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined);
|
||||||
apiResources.map(apiResource => this.canI({
|
const apiLimit = plimit(5); // 5 concurrent api requests
|
||||||
resource: apiResource.resource,
|
const requests = [];
|
||||||
group: apiResource.group,
|
|
||||||
verb: "list",
|
for (const apiResource of resources) {
|
||||||
namespace: this.allowedNamespaces[0]
|
requests.push(apiLimit(async () => {
|
||||||
}))
|
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
|
||||||
);
|
if (!this.resourceAccessStatuses.get(apiResource)) {
|
||||||
|
const result = await this.canI({
|
||||||
|
resource: apiResource.resource,
|
||||||
|
group: apiResource.group,
|
||||||
|
verb: "list",
|
||||||
|
namespace
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resourceAccessStatuses.set(apiResource, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
await Promise.all(requests);
|
||||||
|
|
||||||
return apiResources
|
return apiResources
|
||||||
.filter((resource, i) => resourceAccessStatuses[i])
|
.filter((resource) => this.resourceAccessStatuses.get(resource))
|
||||||
.map(apiResource => apiResource.resource);
|
.map(apiResource => apiResource.resource);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@ -22,10 +22,11 @@ const kubectlMap: Map<string, string> = new Map([
|
|||||||
["1.13", "1.13.12"],
|
["1.13", "1.13.12"],
|
||||||
["1.14", "1.14.10"],
|
["1.14", "1.14.10"],
|
||||||
["1.15", "1.15.11"],
|
["1.15", "1.15.11"],
|
||||||
["1.16", "1.16.14"],
|
["1.16", "1.16.15"],
|
||||||
["1.17", bundledVersion],
|
["1.17", bundledVersion],
|
||||||
["1.18", "1.18.8"],
|
["1.18", "1.18.15"],
|
||||||
["1.19", "1.19.0"]
|
["1.19", "1.19.5"],
|
||||||
|
["1.20", "1.20.0"]
|
||||||
]);
|
]);
|
||||||
const packageMirrors: Map<string, string> = new Map([
|
const packageMirrors: Map<string, string> = new Map([
|
||||||
["default", "https://storage.googleapis.com/kubernetes-release/release"],
|
["default", "https://storage.googleapis.com/kubernetes-release/release"],
|
||||||
|
|||||||
@ -120,6 +120,14 @@ export class LensProxy {
|
|||||||
protected createProxy(): httpProxy {
|
protected createProxy(): httpProxy {
|
||||||
const proxy = httpProxy.createProxyServer();
|
const proxy = httpProxy.createProxyServer();
|
||||||
|
|
||||||
|
proxy.on("proxyRes", (proxyRes, req) => {
|
||||||
|
const retryCounterId = this.getRequestId(req);
|
||||||
|
|
||||||
|
if (this.retryCounters.has(retryCounterId)) {
|
||||||
|
this.retryCounters.delete(retryCounterId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
proxy.on("error", (error, req, res, target) => {
|
proxy.on("error", (error, req, res, target) => {
|
||||||
if (this.closed) {
|
if (this.closed) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -50,8 +50,8 @@ export class ApiManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStore(api: string | KubeApi): KubeObjectStore {
|
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
|
||||||
return this.stores.get(this.resolveApi(api));
|
return this.stores.get(this.resolveApi(api)) as S;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,13 +20,13 @@ import { Button } from "../button";
|
|||||||
import { releaseStore } from "./release.store";
|
import { releaseStore } from "./release.store";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { themeStore } from "../../theme.store";
|
import { themeStore } from "../../theme.store";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
import { secretsStore } from "../+config-secrets/secrets.store";
|
||||||
import { Secret } from "../../api/endpoints";
|
import { Secret } from "../../api/endpoints";
|
||||||
|
import { getDetailsUrl } from "../kube-object";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
release: HelmRelease;
|
release: HelmRelease;
|
||||||
@ -161,10 +161,7 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
const name = item.getName();
|
const name = item.getName();
|
||||||
const namespace = item.getNs();
|
const namespace = item.getNs();
|
||||||
const api = apiManager.getApi(item.metadata.selfLink);
|
const api = apiManager.getApi(item.metadata.selfLink);
|
||||||
const detailsUrl = api ? getDetailsUrl(api.getUrl({
|
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
|
||||||
name,
|
|
||||||
namespace,
|
|
||||||
})) : "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={item.getId()}>
|
<TableRow key={item.getId()}>
|
||||||
|
|||||||
@ -4,12 +4,12 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||||
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
|
import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts";
|
||||||
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
|
import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Apps extends React.Component {
|
export class Apps extends React.Component {
|
||||||
static get tabRoutes(): TabLayoutRoute[] {
|
static get tabRoutes(): TabLayoutRoute[] {
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceUrlParam.toObjectParam();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,11 +10,11 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
|
|||||||
import { nodesStore } from "../+nodes/nodes.store";
|
import { nodesStore } from "../+nodes/nodes.store";
|
||||||
import { eventStore } from "../+events/event.store";
|
import { eventStore } from "../+events/event.store";
|
||||||
import { autobind, cssNames, prevDefault } from "../../utils";
|
import { autobind, cssNames, prevDefault } from "../../utils";
|
||||||
import { getSelectedDetails, showDetails } from "../../navigation";
|
|
||||||
import { ItemObject } from "../../item.store";
|
import { ItemObject } from "../../item.store";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { themeStore } from "../../theme.store";
|
import { themeStore } from "../../theme.store";
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
|
import { kubeSelectedUrlParam, showDetails } from "../kube-object";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -85,7 +85,7 @@ export class ClusterIssues extends React.Component<Props> {
|
|||||||
<TableRow
|
<TableRow
|
||||||
key={getId()}
|
key={getId()}
|
||||||
sortItem={warning}
|
sortItem={warning}
|
||||||
selected={selfLink === getSelectedDetails()}
|
selected={selfLink === kubeSelectedUrlParam.get()}
|
||||||
onClick={prevDefault(() => showDetails(selfLink))}
|
onClick={prevDefault(() => showDetails(selfLink))}
|
||||||
>
|
>
|
||||||
<TableCell className="message">
|
<TableCell className="message">
|
||||||
|
|||||||
@ -5,13 +5,12 @@ import { observer } from "mobx-react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
|
import { HorizontalPodAutoscaler, HpaMetricType, IHpaMetric } from "../../api/endpoints/hpa.api";
|
||||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
|||||||
@ -17,8 +17,8 @@ import { Icon } from "../icon";
|
|||||||
import { IKubeObjectMetadata } from "../../api/kube-object";
|
import { IKubeObjectMetadata } from "../../api/kube-object";
|
||||||
import { base64 } from "../../utils";
|
import { base64 } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { showDetails } from "../../navigation";
|
|
||||||
import upperFirst from "lodash/upperFirst";
|
import upperFirst from "lodash/upperFirst";
|
||||||
|
import { showDetails } from "../kube-object";
|
||||||
|
|
||||||
interface Props extends Partial<DialogProps> {
|
interface Props extends Partial<DialogProps> {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||||
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
|
import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps";
|
||||||
import { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
|
import { Secrets, secretsRoute, secretsURL } from "../+config-secrets";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||||
import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas";
|
import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas";
|
||||||
import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
|
import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets";
|
||||||
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers";
|
||||||
@ -13,7 +13,7 @@ import { isAllowedResource } from "../../../common/rbac";
|
|||||||
@observer
|
@observer
|
||||||
export class Config extends React.Component {
|
export class Config extends React.Component {
|
||||||
static get tabRoutes(): TabLayoutRoute[] {
|
static get tabRoutes(): TabLayoutRoute[] {
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceUrlParam.toObjectParam();
|
||||||
const routes: TabLayoutRoute[] = [];
|
const routes: TabLayoutRoute[] = [];
|
||||||
|
|
||||||
if (isAllowedResource("configmaps")) {
|
if (isAllowedResource("configmaps")) {
|
||||||
|
|||||||
@ -10,9 +10,16 @@ import { KubeObjectListLayout } from "../kube-object";
|
|||||||
import { crdStore } from "./crd.store";
|
import { crdStore } from "./crd.store";
|
||||||
import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
import { CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
||||||
import { Select, SelectOption } from "../select";
|
import { Select, SelectOption } from "../select";
|
||||||
import { navigation, setQueryParams } from "../../navigation";
|
import { createPageParam } from "../../navigation";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
|
export const crdGroupsUrlParam = createPageParam<string[]>({
|
||||||
|
name: "groups",
|
||||||
|
multiValues: true,
|
||||||
|
isSystem: true,
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
kind = "kind",
|
kind = "kind",
|
||||||
group = "group",
|
group = "group",
|
||||||
@ -23,17 +30,19 @@ enum sortBy {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class CrdList extends React.Component {
|
export class CrdList extends React.Component {
|
||||||
@computed get groups() {
|
@computed get groups(): string[] {
|
||||||
return navigation.searchParams.getAsArray("groups");
|
return crdGroupsUrlParam.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupChange(group: string) {
|
onSelectGroup(group: string) {
|
||||||
const groups = [...this.groups];
|
const groups = new Set(this.groups);
|
||||||
const index = groups.findIndex(item => item == group);
|
|
||||||
|
|
||||||
if (index !== -1) groups.splice(index, 1);
|
if (groups.has(group)) {
|
||||||
else groups.push(group);
|
groups.delete(group); // toggle selection
|
||||||
setQueryParams({ groups });
|
} else {
|
||||||
|
groups.add(group);
|
||||||
|
}
|
||||||
|
crdGroupsUrlParam.set(Array.from(groups));
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -71,7 +80,7 @@ export class CrdList extends React.Component {
|
|||||||
className="group-select"
|
className="group-select"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
options={Object.keys(crdStore.groups)}
|
options={Object.keys(crdStore.groups)}
|
||||||
onChange={({ value: group }: SelectOption) => this.onGroupChange(group)}
|
onChange={({ value: group }: SelectOption) => this.onSelectGroup(group)}
|
||||||
controlShouldRenderValue={false}
|
controlShouldRenderValue={false}
|
||||||
formatOptionLabel={({ value: group }: SelectOption) => {
|
formatOptionLabel={({ value: group }: SelectOption) => {
|
||||||
const isSelected = selectedGroups.includes(group);
|
const isSelected = selectedGroups.includes(group);
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { crdStore } from "./crd.store";
|
|||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
import { AdditionalPrinterColumnsV1, CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
||||||
|
import { parseJsonPath } from "../../utils/jsonPath";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
|
interface Props extends KubeObjectDetailsProps<CustomResourceDefinition> {
|
||||||
}
|
}
|
||||||
@ -46,7 +47,7 @@ export class CrdResourceDetails extends React.Component<Props> {
|
|||||||
renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {
|
renderAdditionalColumns(crd: CustomResourceDefinition, columns: AdditionalPrinterColumnsV1[]) {
|
||||||
return columns.map(({ name, jsonPath: jp }) => (
|
return columns.map(({ name, jsonPath: jp }) => (
|
||||||
<DrawerItem key={name} name={name} renderBoolean>
|
<DrawerItem key={name} name={name} renderBoolean>
|
||||||
{convertSpecValue(jsonPath.value(crd, jp.slice(1)))}
|
{convertSpecValue(jsonPath.value(crd, parseJsonPath(jp.slice(1))))}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { autorun, computed } from "mobx";
|
|||||||
import { crdStore } from "./crd.store";
|
import { crdStore } from "./crd.store";
|
||||||
import { TableSortCallback } from "../table";
|
import { TableSortCallback } from "../table";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
|
import { parseJsonPath } from "../../utils/jsonPath";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<ICRDRouteParams> {
|
interface Props extends RouteComponentProps<ICRDRouteParams> {
|
||||||
}
|
}
|
||||||
@ -61,7 +62,7 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
extraColumns.forEach(column => {
|
extraColumns.forEach(column => {
|
||||||
sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, column.jsonPath.slice(1));
|
sortingCallbacks[column.name] = (item: KubeObject) => jsonPath.value(item, parseJsonPath(column.jsonPath.slice(1)));
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -91,10 +92,18 @@ export class CrdResources extends React.Component<Props> {
|
|||||||
renderTableContents={(crdInstance: KubeObject) => [
|
renderTableContents={(crdInstance: KubeObject) => [
|
||||||
crdInstance.getName(),
|
crdInstance.getName(),
|
||||||
isNamespaced && crdInstance.getNs(),
|
isNamespaced && crdInstance.getNs(),
|
||||||
...extraColumns.map(column => ({
|
...extraColumns.map((column) => {
|
||||||
renderBoolean: true,
|
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
|
||||||
children: JSON.stringify(jsonPath.value(crdInstance, column.jsonPath.slice(1))),
|
|
||||||
})),
|
if (Array.isArray(value) || typeof value === "object") {
|
||||||
|
value = JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderBoolean: true,
|
||||||
|
children: value,
|
||||||
|
};
|
||||||
|
}),
|
||||||
crdInstance.getAge(),
|
crdInstance.getAge(),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -6,10 +6,9 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
import { KubeObjectDetailsProps, getDetailsUrl } from "../kube-object";
|
||||||
import { KubeEvent } from "../../api/endpoints/events.api";
|
import { KubeEvent } from "../../api/endpoints/events.api";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
|||||||
@ -4,14 +4,13 @@ import React, { Fragment } from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { TabLayout } from "../layout/tab-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
import { eventStore } from "./event.store";
|
import { eventStore } from "./event.store";
|
||||||
import { KubeObjectListLayout, KubeObjectListLayoutProps } from "../kube-object";
|
import { KubeObjectListLayout, KubeObjectListLayoutProps, getDetailsUrl } from "../kube-object";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { KubeEvent } from "../../api/endpoints/events.api";
|
import { KubeEvent } from "../../api/endpoints/events.api";
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { cssNames, IClassName, stopPropagation } from "../../utils";
|
import { cssNames, IClassName, stopPropagation } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
|
|||||||
@ -7,9 +7,8 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { DrawerItem } from "../drawer";
|
import { DrawerItem } from "../drawer";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Namespace } from "../../api/endpoints";
|
import { Namespace } from "../../api/endpoints";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
|
import { resourceQuotaStore } from "../+config-resource-quotas/resource-quotas.store";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
|
|||||||
@ -1,62 +1,66 @@
|
|||||||
import { action, observable, reaction } from "mobx";
|
import { action, comparer, observable, reaction } from "mobx";
|
||||||
import { autobind, createStorage } from "../../utils";
|
import { autobind, createStorage } from "../../utils";
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
import { KubeObjectStore } from "../../kube-object.store";
|
||||||
import { Namespace, namespacesApi } from "../../api/endpoints";
|
import { Namespace, namespacesApi } from "../../api/endpoints";
|
||||||
import { IQueryParams, navigation, setQueryParams } from "../../navigation";
|
import { createPageParam } from "../../navigation";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
import { getHostedCluster } from "../../../common/cluster-store";
|
import { getHostedCluster } from "../../../common/cluster-store";
|
||||||
|
|
||||||
|
const storage = createStorage<string[]>("context_namespaces", []);
|
||||||
|
|
||||||
|
export const namespaceUrlParam = createPageParam<string[]>({
|
||||||
|
name: "namespaces",
|
||||||
|
isSystem: true,
|
||||||
|
multiValues: true,
|
||||||
|
get defaultValue() {
|
||||||
|
return storage.get(); // initial namespaces coming from URL or local-storage (default)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
export class NamespaceStore extends KubeObjectStore<Namespace> {
|
||||||
api = namespacesApi;
|
api = namespacesApi;
|
||||||
contextNs = observable.array<string>();
|
contextNs = observable.array<string>();
|
||||||
|
|
||||||
protected storage = createStorage<string[]>("context_ns", this.contextNs);
|
|
||||||
|
|
||||||
get initNamespaces() {
|
|
||||||
const fromUrl = navigation.searchParams.getAsArray("namespaces");
|
|
||||||
|
|
||||||
return fromUrl.length ? fromUrl : this.storage.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
// restore context namespaces
|
private init() {
|
||||||
const { initNamespaces: namespaces } = this;
|
this.setContext(this.initNamespaces);
|
||||||
|
|
||||||
this.setContext(namespaces);
|
return reaction(() => this.contextNs.toJS(), namespaces => {
|
||||||
this.updateUrl(namespaces);
|
storage.set(namespaces); // save to local-storage
|
||||||
|
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
|
||||||
// sync with local-storage & url-search-params
|
}, {
|
||||||
reaction(() => this.contextNs.toJS(), namespaces => {
|
fireImmediately: true,
|
||||||
this.storage.set(namespaces);
|
equals: comparer.identity,
|
||||||
this.updateUrl(namespaces);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getContextParams(): Partial<IQueryParams> {
|
get initNamespaces() {
|
||||||
|
return namespaceUrlParam.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
getContextParams() {
|
||||||
return {
|
return {
|
||||||
namespaces: this.contextNs
|
namespaces: this.contextNs.toJS(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(apis = [this.api]) {
|
subscribe(apis = [this.api]) {
|
||||||
const { allowedNamespaces } = getHostedCluster();
|
const { accessibleNamespaces } = getHostedCluster();
|
||||||
|
|
||||||
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
|
||||||
if (allowedNamespaces.length > 0) {
|
if (accessibleNamespaces.length > 0) {
|
||||||
return () => { return; };
|
return Function; // no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.subscribe(apis);
|
return super.subscribe(apis);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateUrl(namespaces: string[]) {
|
|
||||||
setQueryParams({ namespaces }, { replace: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async loadItems(namespaces?: string[]) {
|
protected async loadItems(namespaces?: string[]) {
|
||||||
if (!isAllowedResource("namespaces")) {
|
if (!isAllowedResource("namespaces")) {
|
||||||
if (namespaces) return namespaces.map(this.getDummyNamespace);
|
if (namespaces) return namespaces.map(this.getDummyNamespace);
|
||||||
@ -84,6 +88,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
setContext(namespaces: string[]) {
|
setContext(namespaces: string[]) {
|
||||||
this.contextNs.replace(namespaces);
|
this.contextNs.replace(namespaces);
|
||||||
}
|
}
|
||||||
@ -94,6 +99,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
return context.every(namespace => this.contextNs.includes(namespace));
|
return context.every(namespace => this.contextNs.includes(namespace));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
toggleContext(namespace: string) {
|
toggleContext(namespace: string) {
|
||||||
if (this.hasContext(namespace)) this.contextNs.remove(namespace);
|
if (this.hasContext(namespace)) this.contextNs.remove(namespace);
|
||||||
else this.contextNs.push(namespace);
|
else this.contextNs.push(namespace);
|
||||||
@ -105,6 +111,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
|
|||||||
this.contextNs.clear();
|
this.contextNs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async remove(item: Namespace) {
|
async remove(item: Namespace) {
|
||||||
await super.remove(item);
|
await super.remove(item);
|
||||||
this.contextNs.remove(item.getName());
|
this.contextNs.remove(item.getName());
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { getDetailsUrl } from "../kube-object";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
subset: EndpointSubset;
|
subset: EndpointSubset;
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { observer } from "mobx-react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Table, TableHead, TableCell, TableRow } from "../table";
|
import { Table, TableHead, TableCell, TableRow } from "../table";
|
||||||
import { prevDefault } from "../../utils";
|
import { prevDefault } from "../../utils";
|
||||||
import { showDetails } from "../../navigation";
|
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
|
import { showDetails } from "../kube-object";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
endpoint: KubeObject;
|
endpoint: KubeObject;
|
||||||
|
|||||||
@ -8,13 +8,13 @@ import { Services, servicesRoute, servicesURL } from "../+network-services";
|
|||||||
import { endpointRoute, Endpoints, endpointURL } from "../+network-endpoints";
|
import { endpointRoute, Endpoints, endpointURL } from "../+network-endpoints";
|
||||||
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
|
import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses";
|
||||||
import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies";
|
import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Network extends React.Component {
|
export class Network extends React.Component {
|
||||||
static get tabRoutes(): TabLayoutRoute[] {
|
static get tabRoutes(): TabLayoutRoute[] {
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceUrlParam.toObjectParam();
|
||||||
const routes: TabLayoutRoute[] = [];
|
const routes: TabLayoutRoute[] = [];
|
||||||
|
|
||||||
if (isAllowedResource("services")) {
|
if (isAllowedResource("services")) {
|
||||||
|
|||||||
@ -134,7 +134,7 @@ export class Nodes extends React.Component<Props> {
|
|||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="Nodes"
|
className="Nodes"
|
||||||
store={nodesStore} isClusterScoped
|
store={nodesStore} isClusterScoped
|
||||||
isReady={nodesStore.isLoaded && nodesStore.metricsLoaded}
|
isReady={nodesStore.isLoaded}
|
||||||
dependentStores={[podsStore]}
|
dependentStores={[podsStore]}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
|
|||||||
@ -10,13 +10,11 @@ import { podsStore } from "../+workloads-pods/pods.store";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||||
import { volumeClaimStore } from "./volume-claim.store";
|
import { volumeClaimStore } from "./volume-claim.store";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { ResourceMetrics } from "../resource-metrics";
|
import { ResourceMetrics } from "../resource-metrics";
|
||||||
import { VolumeClaimDiskChart } from "./volume-claim-disk-chart";
|
import { VolumeClaimDiskChart } from "./volume-claim-disk-chart";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-object";
|
||||||
import { PersistentVolumeClaim } from "../../api/endpoints";
|
import { PersistentVolumeClaim } from "../../api/endpoints";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
|
||||||
|
|||||||
@ -7,11 +7,10 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { volumeClaimStore } from "./volume-claim.store";
|
import { volumeClaimStore } from "./volume-claim.store";
|
||||||
import { PersistentVolumeClaim } from "../../api/endpoints/persistent-volume-claims.api";
|
import { PersistentVolumeClaim } from "../../api/endpoints/persistent-volume-claims.api";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
|
||||||
import { IVolumeClaimsRouteParams } from "./volume-claims.route";
|
import { IVolumeClaimsRouteParams } from "./volume-claims.route";
|
||||||
import { unitsToBytes } from "../../utils/convertMemory";
|
import { unitsToBytes } from "../../utils/convertMemory";
|
||||||
import { stopPropagation } from "../../utils";
|
import { stopPropagation } from "../../utils";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { storageClassApi } from "../../api/endpoints";
|
import { storageClassApi } from "../../api/endpoints";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|
||||||
|
|||||||
@ -8,9 +8,8 @@ import { observer } from "mobx-react";
|
|||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { PersistentVolume, pvcApi } from "../../api/endpoints";
|
import { PersistentVolume, pvcApi } from "../../api/endpoints";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,9 @@ import { observer } from "mobx-react";
|
|||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { Link, RouteComponentProps } from "react-router-dom";
|
import { Link, RouteComponentProps } from "react-router-dom";
|
||||||
import { PersistentVolume } from "../../api/endpoints/persistent-volume.api";
|
import { PersistentVolume } from "../../api/endpoints/persistent-volume.api";
|
||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
|
||||||
import { IVolumesRouteParams } from "./volumes.route";
|
import { IVolumesRouteParams } from "./volumes.route";
|
||||||
import { stopPropagation } from "../../utils";
|
import { stopPropagation } from "../../utils";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { volumesStore } from "./volumes.store";
|
import { volumesStore } from "./volumes.store";
|
||||||
import { pvcApi, storageClassApi } from "../../api/endpoints";
|
import { pvcApi, storageClassApi } from "../../api/endpoints";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
|||||||
@ -7,14 +7,14 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
|||||||
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
|
import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes";
|
||||||
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes";
|
||||||
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Storage extends React.Component {
|
export class Storage extends React.Component {
|
||||||
static get tabRoutes() {
|
static get tabRoutes() {
|
||||||
const tabRoutes: TabLayoutRoute[] = [];
|
const tabRoutes: TabLayoutRoute[] = [];
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceUrlParam.toObjectParam();
|
||||||
|
|
||||||
tabRoutes.push({
|
tabRoutes.push({
|
||||||
title: <Trans>Persistent Volume Claims</Trans>,
|
title: <Trans>Persistent Volume Claims</Trans>,
|
||||||
|
|||||||
@ -16,11 +16,11 @@ import { NamespaceSelect } from "../+namespaces/namespace-select";
|
|||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { KubeObject } from "../../api/kube-object";
|
import { KubeObject } from "../../api/kube-object";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { showDetails } from "../../navigation";
|
|
||||||
import { rolesStore } from "../+user-management-roles/roles.store";
|
import { rolesStore } from "../+user-management-roles/roles.store";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
|
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
|
||||||
import { roleBindingsStore } from "./role-bindings.store";
|
import { roleBindingsStore } from "./role-bindings.store";
|
||||||
|
import { showDetails } from "../kube-object";
|
||||||
|
|
||||||
interface BindingSelectOption extends SelectOption {
|
interface BindingSelectOption extends SelectOption {
|
||||||
value: string; // binding name
|
value: string; // binding name
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { Wizard, WizardStep } from "../wizard";
|
|||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { rolesStore } from "./roles.store";
|
import { rolesStore } from "./roles.store";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { showDetails } from "../../navigation";
|
import { showDetails } from "../kube-object";
|
||||||
|
|
||||||
interface Props extends Partial<DialogProps> {
|
interface Props extends Partial<DialogProps> {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { Input } from "../input";
|
|||||||
import { systemName } from "../input/input_validators";
|
import { systemName } from "../input/input_validators";
|
||||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { showDetails } from "../../navigation";
|
import { showDetails } from "../kube-object";
|
||||||
|
|
||||||
interface Props extends Partial<DialogProps> {
|
interface Props extends Partial<DialogProps> {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,7 @@ import { secretsStore } from "../+config-secrets/secrets.store";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Secret, ServiceAccount } from "../../api/endpoints";
|
import { Secret, ServiceAccount } from "../../api/endpoints";
|
||||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { Roles } from "../+user-management-roles";
|
|||||||
import { RoleBindings } from "../+user-management-roles-bindings";
|
import { RoleBindings } from "../+user-management-roles-bindings";
|
||||||
import { ServiceAccounts } from "../+user-management-service-accounts";
|
import { ServiceAccounts } from "../+user-management-service-accounts";
|
||||||
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route";
|
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||||
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
|
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ import { isAllowedResource } from "../../../common/rbac";
|
|||||||
export class UserManagement extends React.Component {
|
export class UserManagement extends React.Component {
|
||||||
static get tabRoutes() {
|
static get tabRoutes() {
|
||||||
const tabRoutes: TabLayoutRoute[] = [];
|
const tabRoutes: TabLayoutRoute[] = [];
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceUrlParam.toObjectParam();
|
||||||
|
|
||||||
tabRoutes.push(
|
tabRoutes.push(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,8 +10,7 @@ import { jobStore } from "../+workloads-jobs/job.store";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||||
import { cronJobStore } from "./cronjob.store";
|
import { cronJobStore } from "./cronjob.store";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
|
||||||
import { CronJob, Job } from "../../api/endpoints";
|
import { CronJob, Job } from "../../api/endpoints";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
|||||||
@ -13,8 +13,7 @@ import { PodDetailsAffinities } from "../+workloads-pods/pod-details-affinities"
|
|||||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
import { jobStore } from "./job.store";
|
import { jobStore } from "./job.store";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
|
||||||
import { Job } from "../../api/endpoints";
|
import { Job } from "../../api/endpoints";
|
||||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import "./pod-details-list.scss";
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import kebabCase from "lodash/kebabCase";
|
import kebabCase from "lodash/kebabCase";
|
||||||
|
import { reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { podsStore } from "./pods.store";
|
import { podsStore } from "./pods.store";
|
||||||
@ -10,11 +11,10 @@ import { autobind, bytesToUnits, cssNames, interval, prevDefault } from "../../u
|
|||||||
import { LineProgress } from "../line-progress";
|
import { LineProgress } from "../line-progress";
|
||||||
import { KubeObject } from "../../api/kube-object";
|
import { KubeObject } from "../../api/kube-object";
|
||||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||||
import { showDetails } from "../../navigation";
|
|
||||||
import { reaction } from "mobx";
|
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { DrawerTitle } from "../drawer";
|
import { DrawerTitle } from "../drawer";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
import { showDetails } from "../kube-object";
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { autorun, observable } from "mobx";
|
import { autorun, observable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Pod, Secret, secretsApi } from "../../api/endpoints";
|
import { Pod, Secret, secretsApi } from "../../api/endpoints";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
import { getDetailsUrl } from "../kube-object";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pod: Pod;
|
pod: Pod;
|
||||||
|
|||||||
@ -18,8 +18,7 @@ import { KubeEventDetails } from "../+events/kube-event-details";
|
|||||||
import { PodDetailsSecrets } from "./pod-details-secrets";
|
import { PodDetailsSecrets } from "./pod-details-secrets";
|
||||||
import { ResourceMetrics } from "../resource-metrics";
|
import { ResourceMetrics } from "../resource-metrics";
|
||||||
import { podsStore } from "./pods.store";
|
import { podsStore } from "./pods.store";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
import { getDetailsUrl, KubeObjectDetailsProps } from "../kube-object";
|
||||||
import { KubeObjectDetailsProps } from "../kube-object";
|
|
||||||
import { getItemMetrics } from "../../api/endpoints/metrics.api";
|
import { getItemMetrics } from "../../api/endpoints/metrics.api";
|
||||||
import { PodCharts, podMetricTabs } from "./pod-charts";
|
import { PodCharts, podMetricTabs } from "./pod-charts";
|
||||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||||
|
|||||||
@ -6,6 +6,14 @@
|
|||||||
flex-grow: 2;
|
flex-grow: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.age {
|
||||||
|
flex-grow: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.qos {
|
||||||
|
flex-grow: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
@include table-cell-warning;
|
@include table-cell-warning;
|
||||||
}
|
}
|
||||||
@ -22,6 +30,7 @@
|
|||||||
|
|
||||||
&.status {
|
&.status {
|
||||||
@include pod-status-colors;
|
@include pod-status-colors;
|
||||||
|
flex-grow: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,16 +9,16 @@ import { RouteComponentProps } from "react-router";
|
|||||||
import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store";
|
import { volumeClaimStore } from "../+storage-volume-claims/volume-claim.store";
|
||||||
import { IPodsRouteParams } from "../+workloads";
|
import { IPodsRouteParams } from "../+workloads";
|
||||||
import { eventStore } from "../+events/event.store";
|
import { eventStore } from "../+events/event.store";
|
||||||
import { KubeObjectListLayout } from "../kube-object";
|
import { getDetailsUrl, KubeObjectListLayout } from "../kube-object";
|
||||||
import { Pod } from "../../api/endpoints";
|
import { nodesApi, Pod } from "../../api/endpoints";
|
||||||
import { StatusBrick } from "../status-brick";
|
import { StatusBrick } from "../status-brick";
|
||||||
import { cssNames, stopPropagation } from "../../utils";
|
import { cssNames, stopPropagation } from "../../utils";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import toPairs from "lodash/toPairs";
|
import toPairs from "lodash/toPairs";
|
||||||
import startCase from "lodash/startCase";
|
import startCase from "lodash/startCase";
|
||||||
import kebabCase from "lodash/kebabCase";
|
import kebabCase from "lodash/kebabCase";
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
import { Badge } from "../badge";
|
||||||
|
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
@ -28,6 +28,7 @@ enum sortBy {
|
|||||||
restarts = "restarts",
|
restarts = "restarts",
|
||||||
age = "age",
|
age = "age",
|
||||||
qos = "qos",
|
qos = "qos",
|
||||||
|
node = "node",
|
||||||
owners = "owners",
|
owners = "owners",
|
||||||
status = "status",
|
status = "status",
|
||||||
}
|
}
|
||||||
@ -81,6 +82,7 @@ export class Pods extends React.Component<Props> {
|
|||||||
[sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(),
|
[sortBy.restarts]: (pod: Pod) => pod.getRestartsCount(),
|
||||||
[sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
|
[sortBy.owners]: (pod: Pod) => pod.getOwnerRefs().map(ref => ref.kind),
|
||||||
[sortBy.qos]: (pod: Pod) => pod.getQosClass(),
|
[sortBy.qos]: (pod: Pod) => pod.getQosClass(),
|
||||||
|
[sortBy.node]: (pod: Pod) => pod.getNodeName(),
|
||||||
[sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp,
|
[sortBy.age]: (pod: Pod) => pod.metadata.creationTimestamp,
|
||||||
[sortBy.status]: (pod: Pod) => pod.getStatusMessage(),
|
[sortBy.status]: (pod: Pod) => pod.getStatusMessage(),
|
||||||
}}
|
}}
|
||||||
@ -88,6 +90,7 @@ export class Pods extends React.Component<Props> {
|
|||||||
(pod: Pod) => pod.getSearchFields(),
|
(pod: Pod) => pod.getSearchFields(),
|
||||||
(pod: Pod) => pod.getStatusMessage(),
|
(pod: Pod) => pod.getStatusMessage(),
|
||||||
(pod: Pod) => pod.status.podIP,
|
(pod: Pod) => pod.status.podIP,
|
||||||
|
(pod: Pod) => pod.getNodeName(),
|
||||||
]}
|
]}
|
||||||
renderHeaderTitle={<Trans>Pods</Trans>}
|
renderHeaderTitle={<Trans>Pods</Trans>}
|
||||||
renderTableHeader={[
|
renderTableHeader={[
|
||||||
@ -97,12 +100,13 @@ export class Pods extends React.Component<Props> {
|
|||||||
{ title: <Trans>Containers</Trans>, className: "containers", sortBy: sortBy.containers },
|
{ title: <Trans>Containers</Trans>, className: "containers", sortBy: sortBy.containers },
|
||||||
{ title: <Trans>Restarts</Trans>, className: "restarts", sortBy: sortBy.restarts },
|
{ title: <Trans>Restarts</Trans>, className: "restarts", sortBy: sortBy.restarts },
|
||||||
{ title: <Trans>Controlled By</Trans>, className: "owners", sortBy: sortBy.owners },
|
{ title: <Trans>Controlled By</Trans>, className: "owners", sortBy: sortBy.owners },
|
||||||
|
{ title: <Trans>Node</Trans>, className: "node", sortBy: sortBy.node },
|
||||||
{ title: <Trans>QoS</Trans>, className: "qos", sortBy: sortBy.qos },
|
{ title: <Trans>QoS</Trans>, className: "qos", sortBy: sortBy.qos },
|
||||||
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
||||||
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
|
{ title: <Trans>Status</Trans>, className: "status", sortBy: sortBy.status },
|
||||||
]}
|
]}
|
||||||
renderTableContents={(pod: Pod) => [
|
renderTableContents={(pod: Pod) => [
|
||||||
pod.getName(),
|
<Badge flat key="name" label={pod.getName()} tooltip={pod.getName()} />,
|
||||||
<KubeObjectStatusIcon key="icon" object={pod} />,
|
<KubeObjectStatusIcon key="icon" object={pod} />,
|
||||||
pod.getNs(),
|
pod.getNs(),
|
||||||
this.renderContainersStatus(pod),
|
this.renderContainersStatus(pod),
|
||||||
@ -112,11 +116,20 @@ export class Pods extends React.Component<Props> {
|
|||||||
const detailsLink = getDetailsUrl(lookupApiLink(ref, pod));
|
const detailsLink = getDetailsUrl(lookupApiLink(ref, pod));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={name} to={detailsLink} className="owner" onClick={stopPropagation}>
|
<Badge flat key={name} className="owner" tooltip={name}>
|
||||||
{kind}
|
<Link to={detailsLink} onClick={stopPropagation}>
|
||||||
</Link>
|
{kind}
|
||||||
|
</Link>
|
||||||
|
</Badge>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
pod.getNodeName() ?
|
||||||
|
<Badge flat key="node" className="node" tooltip={pod.getNodeName()}>
|
||||||
|
<Link to={getDetailsUrl(nodesApi.getUrl({ name: pod.getNodeName() }))} onClick={stopPropagation}>
|
||||||
|
{pod.getNodeName()}
|
||||||
|
</Link>
|
||||||
|
</Badge>
|
||||||
|
: "",
|
||||||
pod.getQosClass(),
|
pod.getQosClass(),
|
||||||
pod.getAge(),
|
pod.getAge(),
|
||||||
{ title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) }
|
{ title: pod.getStatusMessage(), className: kebabCase(pod.getStatusMessage()) }
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Trans } from "@lingui/macro";
|
|||||||
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
||||||
import { WorkloadsOverview } from "../+workloads-overview/overview";
|
import { WorkloadsOverview } from "../+workloads-overview/overview";
|
||||||
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, replicaSetsRoute, replicaSetsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route";
|
import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, replicaSetsRoute, replicaSetsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||||
import { Pods } from "../+workloads-pods";
|
import { Pods } from "../+workloads-pods";
|
||||||
import { Deployments } from "../+workloads-deployments";
|
import { Deployments } from "../+workloads-deployments";
|
||||||
import { DaemonSets } from "../+workloads-daemonsets";
|
import { DaemonSets } from "../+workloads-daemonsets";
|
||||||
@ -19,7 +19,7 @@ import { ReplicaSets } from "../+workloads-replicasets";
|
|||||||
@observer
|
@observer
|
||||||
export class Workloads extends React.Component {
|
export class Workloads extends React.Component {
|
||||||
static get tabRoutes(): TabLayoutRoute[] {
|
static get tabRoutes(): TabLayoutRoute[] {
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceUrlParam.toObjectParam();
|
||||||
const routes: TabLayoutRoute[] = [
|
const routes: TabLayoutRoute[] = [
|
||||||
{
|
{
|
||||||
title: <Trans>Overview</Trans>,
|
title: <Trans>Overview</Trans>,
|
||||||
|
|||||||
@ -41,10 +41,10 @@ import { broadcastMessage, requestMain } from "../../common/ipc";
|
|||||||
import whatInput from "what-input";
|
import whatInput from "what-input";
|
||||||
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
import { clusterSetFrameIdHandler } from "../../common/cluster-ipc";
|
||||||
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries";
|
||||||
import { TabLayoutRoute, TabLayout } from "./layout/tab-layout";
|
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
|
||||||
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
|
||||||
import { eventStore } from "./+events/event.store";
|
import { eventStore } from "./+events/event.store";
|
||||||
import { reaction, computed } from "mobx";
|
import { computed, reaction } from "mobx";
|
||||||
import { nodesStore } from "./+nodes/nodes.store";
|
import { nodesStore } from "./+nodes/nodes.store";
|
||||||
import { podsStore } from "./+workloads-pods/pods.store";
|
import { podsStore } from "./+workloads-pods/pods.store";
|
||||||
import { sum } from "lodash";
|
import { sum } from "lodash";
|
||||||
@ -129,16 +129,15 @@ export class App extends React.Component {
|
|||||||
if (!menuItem.id) {
|
if (!menuItem.id) {
|
||||||
return routes;
|
return routes;
|
||||||
}
|
}
|
||||||
clusterPageMenuRegistry.getSubItems(menuItem).forEach((item) => {
|
clusterPageMenuRegistry.getSubItems(menuItem).forEach((subMenu) => {
|
||||||
const page = clusterPageRegistry.getByPageMenuTarget(item.target);
|
const page = clusterPageRegistry.getByPageTarget(subMenu.target);
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
routes.push({
|
routes.push({
|
||||||
routePath: page.routePath,
|
routePath: page.url,
|
||||||
url: getExtensionPageUrl({ extensionId: page.extensionId, pageId: page.id, params: item.target.params }),
|
url: getExtensionPageUrl(subMenu.target),
|
||||||
title: item.title,
|
title: subMenu.title,
|
||||||
component: page.components.Page,
|
component: page.components.Page,
|
||||||
exact: page.exact
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -151,14 +150,14 @@ export class App extends React.Component {
|
|||||||
const tabRoutes = this.getTabLayoutRoutes(menu);
|
const tabRoutes = this.getTabLayoutRoutes(menu);
|
||||||
|
|
||||||
if (tabRoutes.length > 0) {
|
if (tabRoutes.length > 0) {
|
||||||
const pageComponent = () => <TabLayout tabs={tabRoutes} />;
|
const pageComponent = () => <TabLayout tabs={tabRoutes}/>;
|
||||||
|
|
||||||
return <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)} />;
|
return <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
|
||||||
} else {
|
} else {
|
||||||
const page = clusterPageRegistry.getByPageMenuTarget(menu.target);
|
const page = clusterPageRegistry.getByPageTarget(menu.target);
|
||||||
|
|
||||||
if (page) {
|
if (page) {
|
||||||
return <Route key={`extension-tab-layout-route-${index}`} path={page.routePath} exact={page.exact} component={page.components.Page}/>;
|
return <Route key={`extension-tab-layout-route-${index}`} path={page.url} component={page.components.Page}/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -169,7 +168,7 @@ export class App extends React.Component {
|
|||||||
const menu = clusterPageMenuRegistry.getByPage(page);
|
const menu = clusterPageMenuRegistry.getByPage(page);
|
||||||
|
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
return <Route key={`extension-route-${index}`} path={page.routePath} exact={page.exact} component={page.components.Page}/>;
|
return <Route key={`extension-route-${index}`} path={page.url} component={page.components.Page}/>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
.Badge {
|
.Badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: $colorVague;
|
|
||||||
color: $textColorSecondary;
|
|
||||||
border-radius: $radius;
|
|
||||||
padding: .2em .4em;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&:not(.flat) {
|
||||||
|
background: $colorVague;
|
||||||
|
color: $textColorSecondary;
|
||||||
|
border-radius: $radius;
|
||||||
|
padding: .2em .4em;
|
||||||
|
}
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,16 +6,17 @@ import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
|||||||
|
|
||||||
export interface BadgeProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
export interface BadgeProps extends React.HTMLAttributes<any>, TooltipDecoratorProps {
|
||||||
small?: boolean;
|
small?: boolean;
|
||||||
|
flat?: boolean;
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@withTooltip
|
@withTooltip
|
||||||
export class Badge extends React.Component<BadgeProps> {
|
export class Badge extends React.Component<BadgeProps> {
|
||||||
render() {
|
render() {
|
||||||
const { className, label, small, children, ...elemProps } = this.props;
|
const { className, label, small, flat, children, ...elemProps } = this.props;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<span className={cssNames("Badge", { small }, className)} {...elemProps}>
|
<span className={cssNames("Badge", { small, flat }, className)} {...elemProps}>
|
||||||
{label}
|
{label}
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -71,8 +71,8 @@ export class ClusterManager extends React.Component {
|
|||||||
<Route component={AddCluster} {...addClusterRoute} />
|
<Route component={AddCluster} {...addClusterRoute} />
|
||||||
<Route component={ClusterView} {...clusterViewRoute} />
|
<Route component={ClusterView} {...clusterViewRoute} />
|
||||||
<Route component={ClusterSettings} {...clusterSettingsRoute} />
|
<Route component={ClusterSettings} {...clusterSettingsRoute} />
|
||||||
{globalPageRegistry.getItems().map(({ routePath, exact, components: { Page } }) => {
|
{globalPageRegistry.getItems().map(({ url, components: { Page } }) => {
|
||||||
return <Route key={routePath} path={routePath} component={Page} exact={exact}/>;
|
return <Route key={url} path={url} component={Page}/>;
|
||||||
})}
|
})}
|
||||||
<Redirect exact to={this.startUrl}/>
|
<Redirect exact to={this.startUrl}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { ClusterIcon } from "../cluster-icon";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { autobind, cssNames, IClassName } from "../../utils";
|
import { autobind, cssNames, IClassName } from "../../utils";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { navigate, navigation } from "../../navigation";
|
import { isActiveRoute, navigate } from "../../navigation";
|
||||||
import { addClusterURL } from "../+add-cluster";
|
import { addClusterURL } from "../+add-cluster";
|
||||||
import { clusterSettingsURL } from "../+cluster-settings";
|
import { clusterSettingsURL } from "../+cluster-settings";
|
||||||
import { landingURL } from "../+landing-page";
|
import { landingURL } from "../+landing-page";
|
||||||
@ -158,12 +158,13 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="extensions">
|
<div className="extensions">
|
||||||
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
|
{globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => {
|
||||||
const registeredPage = globalPageRegistry.getByPageMenuTarget(target);
|
const registeredPage = globalPageRegistry.getByPageTarget(target);
|
||||||
|
|
||||||
if (!registeredPage) return;
|
if (!registeredPage){
|
||||||
const { extensionId, id: pageId } = registeredPage;
|
return;
|
||||||
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params });
|
}
|
||||||
const isActive = pageUrl === navigation.location.pathname;
|
const pageUrl = getExtensionPageUrl(target);
|
||||||
|
const isActive = isActiveRoute(registeredPage.url);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@ -27,6 +27,7 @@ interface OptionalProps {
|
|||||||
showSubmitClose?: boolean;
|
showSubmitClose?: boolean;
|
||||||
showInlineInfo?: boolean;
|
showInlineInfo?: boolean;
|
||||||
showNotifications?: boolean;
|
showNotifications?: boolean;
|
||||||
|
showStatusPanel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -38,6 +39,7 @@ export class InfoPanel extends Component<Props> {
|
|||||||
showSubmitClose: true,
|
showSubmitClose: true,
|
||||||
showInlineInfo: true,
|
showInlineInfo: true,
|
||||||
showNotifications: true,
|
showNotifications: true,
|
||||||
|
showStatusPanel: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observable error = "";
|
@observable error = "";
|
||||||
@ -93,7 +95,7 @@ export class InfoPanel extends Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose } = this.props;
|
const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showButtons, showSubmitClose, showStatusPanel } = this.props;
|
||||||
const { submit, close, submitAndClose, waiting } = this;
|
const { submit, close, submitAndClose, waiting } = this;
|
||||||
const isDisabled = !!(disableSubmit || waiting || error);
|
const isDisabled = !!(disableSubmit || waiting || error);
|
||||||
|
|
||||||
@ -102,9 +104,11 @@ export class InfoPanel extends Component<Props> {
|
|||||||
<div className="controls">
|
<div className="controls">
|
||||||
{controls}
|
{controls}
|
||||||
</div>
|
</div>
|
||||||
<div className="info flex gaps align-center">
|
{showStatusPanel && (
|
||||||
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderErrorIcon()}
|
<div className="flex gaps align-center">
|
||||||
</div>
|
{waiting ? <><Spinner /> {submittingMessage}</> : this.renderErrorIcon()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{showButtons && (
|
{showButtons && (
|
||||||
<>
|
<>
|
||||||
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />
|
<Button plain label={<Trans>Cancel</Trans>} onClick={close} />
|
||||||
|
|||||||
@ -22,10 +22,9 @@ interface Props extends PodLogSearchProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PodLogControls = observer((props: Props) => {
|
export const PodLogControls = observer((props: Props) => {
|
||||||
const { tabData, save, reload, tabId, logs } = props;
|
const { tabData, save, reload, logs } = props;
|
||||||
const { selectedContainer, showTimestamps, previous } = tabData;
|
const { selectedContainer, showTimestamps, previous } = tabData;
|
||||||
const rawLogs = podLogsStore.logs.get(tabId) || [];
|
const since = logs.length ? podLogsStore.getTimestamps(logs[0]) : null;
|
||||||
const since = rawLogs.length ? podLogsStore.getTimestamps(rawLogs[0]) : null;
|
|
||||||
const pod = new Pod(tabData.pod);
|
const pod = new Pod(tabData.pod);
|
||||||
|
|
||||||
const toggleTimestamps = () => {
|
const toggleTimestamps = () => {
|
||||||
@ -39,8 +38,9 @@ export const PodLogControls = observer((props: Props) => {
|
|||||||
|
|
||||||
const downloadLogs = () => {
|
const downloadLogs = () => {
|
||||||
const fileName = selectedContainer ? selectedContainer.name : pod.getName();
|
const fileName = selectedContainer ? selectedContainer.name : pod.getName();
|
||||||
|
const logsToDownload = showTimestamps ? logs : podLogsStore.logsWithoutTimestamps;
|
||||||
|
|
||||||
saveFileDialog(`${fileName}.log`, logs.join("\n"), "text/plain");
|
saveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
|
||||||
};
|
};
|
||||||
|
|
||||||
const onContainerChange = (option: SelectOption) => {
|
const onContainerChange = (option: SelectOption) => {
|
||||||
@ -118,7 +118,10 @@ export const PodLogControls = observer((props: Props) => {
|
|||||||
tooltip={_i18n._(t`Save`)}
|
tooltip={_i18n._(t`Save`)}
|
||||||
className="download-icon"
|
className="download-icon"
|
||||||
/>
|
/>
|
||||||
<PodLogSearch {...props} />
|
<PodLogSearch
|
||||||
|
{...props}
|
||||||
|
logs={showTimestamps ? logs : podLogsStore.logsWithoutTimestamps}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import AnsiUp from "ansi_up";
|
|||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { action, observable } from "mobx";
|
import { action, computed, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Align, ListOnScrollProps } from "react-window";
|
import { Align, ListOnScrollProps } from "react-window";
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ import { Button } from "../button";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import { VirtualList } from "../virtual-list";
|
import { VirtualList } from "../virtual-list";
|
||||||
import { logRange } from "./pod-logs.store";
|
import { podLogsStore } from "./pod-logs.store";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs: string[]
|
logs: string[]
|
||||||
@ -47,23 +47,25 @@ export class PodLogList extends React.Component<Props> {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logs == prevProps.logs || !this.virtualListDiv.current) return;
|
if (logs == prevProps.logs || !this.virtualListDiv.current) return;
|
||||||
|
|
||||||
const newLogsLoaded = prevProps.logs.length < logs.length;
|
const newLogsLoaded = prevProps.logs.length < logs.length;
|
||||||
const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0;
|
const scrolledToBeginning = this.virtualListDiv.current.scrollTop === 0;
|
||||||
const fewLogsLoaded = logs.length < logRange;
|
|
||||||
|
|
||||||
if (this.isLastLineVisible) {
|
if (this.isLastLineVisible || prevProps.logs.length == 0) {
|
||||||
this.scrollToBottom(); // Scroll down to keep user watching/reading experience
|
this.scrollToBottom(); // Scroll down to keep user watching/reading experience
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrolledToBeginning && newLogsLoaded) {
|
if (scrolledToBeginning && newLogsLoaded) {
|
||||||
this.virtualListDiv.current.scrollTop = (logs.length - prevProps.logs.length) * this.lineHeight;
|
const firstLineContents = prevProps.logs[0];
|
||||||
}
|
const lineToScroll = this.props.logs.findIndex((value) => value == firstLineContents);
|
||||||
|
|
||||||
if (fewLogsLoaded) {
|
if (lineToScroll !== -1) {
|
||||||
this.isJumpButtonVisible = false;
|
this.scrollToItem(lineToScroll, "start");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!logs.length) {
|
if (!logs.length) {
|
||||||
@ -71,6 +73,20 @@ export class PodLogList extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns logs with or without timestamps regarding to showTimestamps prop
|
||||||
|
*/
|
||||||
|
@computed
|
||||||
|
get logs() {
|
||||||
|
const showTimestamps = podLogsStore.getData(this.props.id).showTimestamps;
|
||||||
|
|
||||||
|
if (!showTimestamps) {
|
||||||
|
return podLogsStore.logsWithoutTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.logs;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if JumpToBottom button should be visible and sets its observable
|
* Checks if JumpToBottom button should be visible and sets its observable
|
||||||
* @param props Scrolling props from virtual list core
|
* @param props Scrolling props from virtual list core
|
||||||
@ -115,7 +131,6 @@ export class PodLogList extends React.Component<Props> {
|
|||||||
@action
|
@action
|
||||||
scrollToBottom = () => {
|
scrollToBottom = () => {
|
||||||
if (!this.virtualListDiv.current) return;
|
if (!this.virtualListDiv.current) return;
|
||||||
this.isJumpButtonVisible = false;
|
|
||||||
this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight;
|
this.virtualListDiv.current.scrollTop = this.virtualListDiv.current.scrollHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -123,7 +138,13 @@ export class PodLogList extends React.Component<Props> {
|
|||||||
this.virtualListRef.current.scrollToItem(index, align);
|
this.virtualListRef.current.scrollToItem(index, align);
|
||||||
};
|
};
|
||||||
|
|
||||||
onScroll = debounce((props: ListOnScrollProps) => {
|
onScroll = (props: ListOnScrollProps) => {
|
||||||
|
if (!this.virtualListDiv.current) return;
|
||||||
|
this.isLastLineVisible = false;
|
||||||
|
this.onScrollDebounced(props);
|
||||||
|
};
|
||||||
|
|
||||||
|
onScrollDebounced = debounce((props: ListOnScrollProps) => {
|
||||||
if (!this.virtualListDiv.current) return;
|
if (!this.virtualListDiv.current) return;
|
||||||
this.setButtonVisibility(props);
|
this.setButtonVisibility(props);
|
||||||
this.setLastLineVisibility(props);
|
this.setLastLineVisibility(props);
|
||||||
@ -137,7 +158,7 @@ export class PodLogList extends React.Component<Props> {
|
|||||||
*/
|
*/
|
||||||
getLogRow = (rowIndex: number) => {
|
getLogRow = (rowIndex: number) => {
|
||||||
const { searchQuery, isActiveOverlay } = searchStore;
|
const { searchQuery, isActiveOverlay } = searchStore;
|
||||||
const item = this.props.logs[rowIndex];
|
const item = this.logs[rowIndex];
|
||||||
const contents: React.ReactElement[] = [];
|
const contents: React.ReactElement[] = [];
|
||||||
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
|
const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi));
|
||||||
|
|
||||||
@ -179,15 +200,15 @@ export class PodLogList extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { logs, isLoading } = this.props;
|
const { isLoading } = this.props;
|
||||||
const isInitLoading = isLoading && !logs.length;
|
const isInitLoading = isLoading && !this.logs.length;
|
||||||
const rowHeights = new Array(logs.length).fill(this.lineHeight);
|
const rowHeights = new Array(this.logs.length).fill(this.lineHeight);
|
||||||
|
|
||||||
if (isInitLoading) {
|
if (isInitLoading) {
|
||||||
return <Spinner center/>;
|
return <Spinner center/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!logs.length) {
|
if (!this.logs.length) {
|
||||||
return (
|
return (
|
||||||
<div className="PodLogList flex box grow align-center justify-center">
|
<div className="PodLogList flex box grow align-center justify-center">
|
||||||
<Trans>There are no logs available for container</Trans>
|
<Trans>There are no logs available for container</Trans>
|
||||||
@ -198,7 +219,7 @@ export class PodLogList extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div className={cssNames("PodLogList flex", { isLoading })}>
|
<div className={cssNames("PodLogList flex", { isLoading })}>
|
||||||
<VirtualList
|
<VirtualList
|
||||||
items={logs}
|
items={this.logs}
|
||||||
rowHeights={rowHeights}
|
rowHeights={rowHeights}
|
||||||
getRow={this.getLogRow}
|
getRow={this.getLogRow}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
|
|||||||
@ -12,10 +12,13 @@ export interface PodLogSearchProps {
|
|||||||
onSearch: (query: string) => void
|
onSearch: (query: string) => void
|
||||||
toPrevOverlay: () => void
|
toPrevOverlay: () => void
|
||||||
toNextOverlay: () => void
|
toNextOverlay: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends PodLogSearchProps {
|
||||||
logs: string[]
|
logs: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PodLogSearch = observer((props: PodLogSearchProps) => {
|
export const PodLogSearch = observer((props: Props) => {
|
||||||
const { logs, onSearch, toPrevOverlay, toNextOverlay } = props;
|
const { logs, onSearch, toPrevOverlay, toNextOverlay } = props;
|
||||||
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
|
const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore;
|
||||||
const jumpDisabled = !searchQuery || !occurrences.length;
|
const jumpDisabled = !searchQuery || !occurrences.length;
|
||||||
|
|||||||
@ -27,11 +27,11 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
private refresher = interval(10, () => {
|
private refresher = interval(10, () => {
|
||||||
const id = dockStore.selectedTabId;
|
const id = dockStore.selectedTabId;
|
||||||
|
|
||||||
if (!this.logs.get(id)) return;
|
if (!this.podLogs.get(id)) return;
|
||||||
this.loadMore(id);
|
this.loadMore(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
@observable logs = observable.map<TabId, PodLogLine[]>();
|
@observable podLogs = observable.map<TabId, PodLogLine[]>();
|
||||||
@observable newLogSince = observable.map<TabId, string>(); // Timestamp after which all logs are considered to be new
|
@observable newLogSince = observable.map<TabId, string>(); // Timestamp after which all logs are considered to be new
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -48,7 +48,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
}
|
}
|
||||||
}, { delay: 500 });
|
}, { delay: 500 });
|
||||||
|
|
||||||
reaction(() => this.logs.get(dockStore.selectedTabId), () => {
|
reaction(() => this.podLogs.get(dockStore.selectedTabId), () => {
|
||||||
this.setNewLogSince(dockStore.selectedTabId);
|
this.setNewLogSince(dockStore.selectedTabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.refresher.start();
|
this.refresher.start();
|
||||||
this.logs.set(tabId, logs);
|
this.podLogs.set(tabId, logs);
|
||||||
} catch ({error}) {
|
} catch ({error}) {
|
||||||
const message = [
|
const message = [
|
||||||
_i18n._(t`Failed to load logs: ${error.message}`),
|
_i18n._(t`Failed to load logs: ${error.message}`),
|
||||||
@ -80,7 +80,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
this.refresher.stop();
|
this.refresher.stop();
|
||||||
this.logs.set(tabId, message);
|
this.podLogs.set(tabId, message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -91,14 +91,14 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
* @param tabId
|
* @param tabId
|
||||||
*/
|
*/
|
||||||
loadMore = async (tabId: TabId) => {
|
loadMore = async (tabId: TabId) => {
|
||||||
if (!this.logs.get(tabId).length) return;
|
if (!this.podLogs.get(tabId).length) return;
|
||||||
const oldLogs = this.logs.get(tabId);
|
const oldLogs = this.podLogs.get(tabId);
|
||||||
const logs = await this.loadLogs(tabId, {
|
const logs = await this.loadLogs(tabId, {
|
||||||
sinceTime: this.getLastSinceTime(tabId)
|
sinceTime: this.getLastSinceTime(tabId)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add newly received logs to bottom
|
// Add newly received logs to bottom
|
||||||
this.logs.set(tabId, [...oldLogs, ...logs]);
|
this.podLogs.set(tabId, [...oldLogs, ...logs]);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,7 +134,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
* @param tabId
|
* @param tabId
|
||||||
*/
|
*/
|
||||||
setNewLogSince(tabId: TabId) {
|
setNewLogSince(tabId: TabId) {
|
||||||
if (!this.logs.has(tabId) || !this.logs.get(tabId).length || this.newLogSince.has(tabId)) return;
|
if (!this.podLogs.has(tabId) || !this.podLogs.get(tabId).length || this.newLogSince.has(tabId)) return;
|
||||||
const timestamp = this.getLastSinceTime(tabId);
|
const timestamp = this.getLastSinceTime(tabId);
|
||||||
|
|
||||||
this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string
|
this.newLogSince.set(tabId, timestamp.split(".")[0]); // Removing milliseconds from string
|
||||||
@ -147,18 +147,38 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
@computed
|
@computed
|
||||||
get lines() {
|
get lines() {
|
||||||
const id = dockStore.selectedTabId;
|
const id = dockStore.selectedTabId;
|
||||||
const logs = this.logs.get(id);
|
const logs = this.podLogs.get(id);
|
||||||
|
|
||||||
return logs ? logs.length : 0;
|
return logs ? logs.length : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns logs with timestamps for selected tab
|
||||||
|
*/
|
||||||
|
get logs() {
|
||||||
|
const id = dockStore.selectedTabId;
|
||||||
|
|
||||||
|
if (!this.podLogs.has(id)) return [];
|
||||||
|
|
||||||
|
return this.podLogs.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes timestamps from each log line and returns changed logs
|
||||||
|
* @returns Logs without timestamps
|
||||||
|
*/
|
||||||
|
get logsWithoutTimestamps() {
|
||||||
|
return this.logs.map(item => this.removeTimestamps(item));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It gets timestamps from all logs then returns last one + 1 second
|
* It gets timestamps from all logs then returns last one + 1 second
|
||||||
* (this allows to avoid getting the last stamp in the selection)
|
* (this allows to avoid getting the last stamp in the selection)
|
||||||
* @param tabId
|
* @param tabId
|
||||||
*/
|
*/
|
||||||
getLastSinceTime(tabId: TabId) {
|
getLastSinceTime(tabId: TabId) {
|
||||||
const logs = this.logs.get(tabId);
|
const logs = this.podLogs.get(tabId);
|
||||||
const timestamps = this.getTimestamps(logs[logs.length - 1]);
|
const timestamps = this.getTimestamps(logs[logs.length - 1]);
|
||||||
const stamp = new Date(timestamps ? timestamps[0] : null);
|
const stamp = new Date(timestamps ? timestamps[0] : null);
|
||||||
|
|
||||||
@ -176,7 +196,7 @@ export class PodLogsStore extends DockTabStore<IPodLogsData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clearLogs(tabId: TabId) {
|
clearLogs(tabId: TabId) {
|
||||||
this.logs.delete(tabId);
|
this.podLogs.delete(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearData(tabId: TabId) {
|
clearData(tabId: TabId) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { computed, observable, reaction } from "mobx";
|
import { observable, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
|
||||||
import { searchStore } from "../../../common/search-store";
|
import { searchStore } from "../../../common/search-store";
|
||||||
@ -79,31 +79,15 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed prop which returns logs with or without timestamps added to each line
|
|
||||||
* @returns {Array} An array log items
|
|
||||||
*/
|
|
||||||
@computed
|
|
||||||
get logs(): string[] {
|
|
||||||
if (!podLogsStore.logs.has(this.tabId)) return [];
|
|
||||||
const logs = podLogsStore.logs.get(this.tabId);
|
|
||||||
const { getData, removeTimestamps } = podLogsStore;
|
|
||||||
const { showTimestamps } = getData(this.tabId);
|
|
||||||
|
|
||||||
if (!showTimestamps) {
|
|
||||||
return logs.map(item => removeTimestamps(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
return logs;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const logs = podLogsStore.logs;
|
||||||
|
|
||||||
const controls = (
|
const controls = (
|
||||||
<PodLogControls
|
<PodLogControls
|
||||||
ready={!this.isLoading}
|
ready={!this.isLoading}
|
||||||
tabId={this.tabId}
|
tabId={this.tabId}
|
||||||
tabData={this.tabData}
|
tabData={this.tabData}
|
||||||
logs={this.logs}
|
logs={logs}
|
||||||
save={this.save}
|
save={this.save}
|
||||||
reload={this.reload}
|
reload={this.reload}
|
||||||
onSearch={this.onSearch}
|
onSearch={this.onSearch}
|
||||||
@ -119,11 +103,12 @@ export class PodLogs extends React.Component<Props> {
|
|||||||
controls={controls}
|
controls={controls}
|
||||||
showSubmitClose={false}
|
showSubmitClose={false}
|
||||||
showButtons={false}
|
showButtons={false}
|
||||||
|
showStatusPanel={false}
|
||||||
/>
|
/>
|
||||||
<PodLogList
|
<PodLogList
|
||||||
|
logs={logs}
|
||||||
id={this.tabId}
|
id={this.tabId}
|
||||||
isLoading={this.isLoading}
|
isLoading={this.isLoading}
|
||||||
logs={this.logs}
|
|
||||||
load={this.load}
|
load={this.load}
|
||||||
ref={this.logListElement}
|
ref={this.logListElement}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -114,7 +114,13 @@ export class Icon extends React.PureComponent<IconProps> {
|
|||||||
|
|
||||||
// render icon type
|
// render icon type
|
||||||
if (link) {
|
if (link) {
|
||||||
return <NavLink {...iconProps} to={link}/>;
|
const { className, children } = iconProps;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink className={className} to={link}>
|
||||||
|
{children}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
|
|||||||
@ -2,9 +2,15 @@ import React from "react";
|
|||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
import { autorun, observable } from "mobx";
|
import { autorun, observable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { getSearch, setSearch } from "../../navigation";
|
|
||||||
import { InputProps } from "./input";
|
import { InputProps } from "./input";
|
||||||
import { SearchInput } from "./search-input";
|
import { SearchInput } from "./search-input";
|
||||||
|
import { createPageParam } from "../../navigation";
|
||||||
|
|
||||||
|
export const searchUrlParam = createPageParam({
|
||||||
|
name: "search",
|
||||||
|
isSystem: true,
|
||||||
|
defaultValue: "",
|
||||||
|
});
|
||||||
|
|
||||||
interface Props extends InputProps {
|
interface Props extends InputProps {
|
||||||
compact?: boolean; // show only search-icon when not focused
|
compact?: boolean; // show only search-icon when not focused
|
||||||
@ -12,11 +18,11 @@ interface Props extends InputProps {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class SearchInputUrl extends React.Component<Props> {
|
export class SearchInputUrl extends React.Component<Props> {
|
||||||
@observable inputVal = ""; // fix: use empty string to avoid react warnings
|
@observable inputVal = ""; // fix: use empty string on init to avoid react warnings
|
||||||
|
|
||||||
@disposeOnUnmount
|
@disposeOnUnmount
|
||||||
updateInput = autorun(() => this.inputVal = getSearch());
|
updateInput = autorun(() => this.inputVal = searchUrlParam.get());
|
||||||
updateUrl = debounce((val: string) => setSearch(val), 250);
|
updateUrl = debounce((val: string) => searchUrlParam.set(val), 250);
|
||||||
|
|
||||||
setValue = (value: string) => {
|
setValue = (value: string) => {
|
||||||
this.inputVal = value;
|
this.inputVal = value;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { computed, observable, reaction } from "mobx";
|
import { computed, observable, reaction } from "mobx";
|
||||||
import { autobind } from "../../utils";
|
import { autobind } from "../../utils";
|
||||||
import { getSearch, setSearch } from "../../navigation";
|
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||||
|
import { searchUrlParam } from "../input/search-input-url";
|
||||||
|
|
||||||
export enum FilterType {
|
export enum FilterType {
|
||||||
SEARCH = "search",
|
SEARCH = "search",
|
||||||
@ -54,8 +54,8 @@ export class PageFiltersStore {
|
|||||||
|
|
||||||
protected syncWithGlobalSearch() {
|
protected syncWithGlobalSearch() {
|
||||||
const disposers = [
|
const disposers = [
|
||||||
reaction(() => this.getValues(FilterType.SEARCH)[0], setSearch),
|
reaction(() => this.getValues(FilterType.SEARCH)[0], search => searchUrlParam.set(search)),
|
||||||
reaction(() => getSearch(), search => {
|
reaction(() => searchUrlParam.get(), search => {
|
||||||
const filter = this.getByType(FilterType.SEARCH);
|
const filter = this.getByType(FilterType.SEARCH);
|
||||||
|
|
||||||
if (filter) {
|
if (filter) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { computed, observable, reaction } from "mobx";
|
import { computed, observable, reaction } from "mobx";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { getDetails, hideDetails } from "../../navigation";
|
import { createPageParam, navigation } from "../../navigation";
|
||||||
import { Drawer } from "../drawer";
|
import { Drawer } from "../drawer";
|
||||||
import { KubeObject } from "../../api/kube-object";
|
import { KubeObject } from "../../api/kube-object";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
@ -14,6 +14,43 @@ import { CrdResourceDetails } from "../+custom-resources";
|
|||||||
import { KubeObjectMenu } from "./kube-object-menu";
|
import { KubeObjectMenu } from "./kube-object-menu";
|
||||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||||
|
|
||||||
|
export const kubeDetailsUrlParam = createPageParam({
|
||||||
|
name: "kube-details",
|
||||||
|
isSystem: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const kubeSelectedUrlParam = createPageParam({
|
||||||
|
name: "kube-selected",
|
||||||
|
isSystem: true,
|
||||||
|
get defaultValue() {
|
||||||
|
return kubeDetailsUrlParam.get();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function showDetails(details = "", resetSelected = true) {
|
||||||
|
const detailsUrl = getDetailsUrl(details, resetSelected);
|
||||||
|
|
||||||
|
navigation.merge({ search: detailsUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideDetails() {
|
||||||
|
showDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDetailsUrl(details: string, resetSelected = false) {
|
||||||
|
const detailsUrl = kubeDetailsUrlParam.toSearchString({ value: details });
|
||||||
|
|
||||||
|
if (resetSelected) {
|
||||||
|
const params = new URLSearchParams(detailsUrl);
|
||||||
|
|
||||||
|
params.delete(kubeSelectedUrlParam.name);
|
||||||
|
|
||||||
|
return `?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return detailsUrl;
|
||||||
|
}
|
||||||
|
|
||||||
export interface KubeObjectDetailsProps<T = KubeObject> {
|
export interface KubeObjectDetailsProps<T = KubeObject> {
|
||||||
className?: string;
|
className?: string;
|
||||||
object: T;
|
object: T;
|
||||||
@ -25,7 +62,7 @@ export class KubeObjectDetails extends React.Component {
|
|||||||
@observable.ref loadingError: React.ReactNode;
|
@observable.ref loadingError: React.ReactNode;
|
||||||
|
|
||||||
@computed get path() {
|
@computed get path() {
|
||||||
return getDetails();
|
return kubeDetailsUrlParam.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get object() {
|
@computed get object() {
|
||||||
@ -70,7 +107,7 @@ export class KubeObjectDetails extends React.Component {
|
|||||||
const { object, isLoading, loadingError, isCrdInstance } = this;
|
const { object, isLoading, loadingError, isCrdInstance } = this;
|
||||||
const isOpen = !!(object || isLoading || loadingError);
|
const isOpen = !!(object || isLoading || loadingError);
|
||||||
let title = "";
|
let title = "";
|
||||||
let details: JSX.Element[];
|
let details: React.ReactNode[];
|
||||||
|
|
||||||
if (object) {
|
if (object) {
|
||||||
const { kind, getName } = object;
|
const { kind, getName } = object;
|
||||||
@ -81,7 +118,7 @@ export class KubeObjectDetails extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isCrdInstance && details.length === 0) {
|
if (isCrdInstance && details.length === 0) {
|
||||||
details.push(<CrdResourceDetails object={object} />);
|
details.push(<CrdResourceDetails object={object}/>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +127,7 @@ export class KubeObjectDetails extends React.Component {
|
|||||||
className="KubeObjectDetails flex column"
|
className="KubeObjectDetails flex column"
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
title={title}
|
title={title}
|
||||||
toolbar={<KubeObjectMenu object={object} toolbar={true} />}
|
toolbar={<KubeObjectMenu object={object} toolbar={true}/>}
|
||||||
onClose={hideDetails}
|
onClose={hideDetails}
|
||||||
>
|
>
|
||||||
{isLoading && <Spinner center/>}
|
{isLoading && <Spinner center/>}
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { computed } from "mobx";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { KubeObject } from "../../api/kube-object";
|
import { KubeObject } from "../../api/kube-object";
|
||||||
import { getSelectedDetails, showDetails } from "../../navigation";
|
|
||||||
import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout";
|
import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout";
|
||||||
import { KubeObjectStore } from "../../kube-object.store";
|
import { KubeObjectStore } from "../../kube-object.store";
|
||||||
import { KubeObjectMenu } from "./kube-object-menu";
|
import { KubeObjectMenu } from "./kube-object-menu";
|
||||||
|
import { kubeSelectedUrlParam, showDetails } from "./kube-object-details";
|
||||||
|
|
||||||
export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
|
export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
|
||||||
store: KubeObjectStore;
|
store: KubeObjectStore;
|
||||||
@ -15,14 +15,13 @@ export interface KubeObjectListLayoutProps extends ItemListLayoutProps {
|
|||||||
@observer
|
@observer
|
||||||
export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutProps> {
|
export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutProps> {
|
||||||
@computed get selectedItem() {
|
@computed get selectedItem() {
|
||||||
return this.props.store.getByPath(getSelectedDetails());
|
return this.props.store.getByPath(kubeSelectedUrlParam.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
onDetails = (item: KubeObject) => {
|
onDetails = (item: KubeObject) => {
|
||||||
if (this.props.onDetails) {
|
if (this.props.onDetails) {
|
||||||
this.props.onDetails(item);
|
this.props.onDetails(item);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
showDetails(item.selfLink);
|
showDetails(item.selfLink);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { autobind, cssNames } from "../../utils";
|
|||||||
import { KubeObject } from "../../api/kube-object";
|
import { KubeObject } from "../../api/kube-object";
|
||||||
import { editResourceTab } from "../dock/edit-resource.store";
|
import { editResourceTab } from "../dock/edit-resource.store";
|
||||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||||
import { hideDetails } from "../../navigation";
|
import { hideDetails } from "./kube-object-details";
|
||||||
import { apiManager } from "../../api/api-manager";
|
import { apiManager } from "../../api/api-manager";
|
||||||
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
|
import { kubeObjectMenuRegistry } from "../../../extensions/registries/kube-object-menu-registry";
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import React from "react";
|
|||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { IKubeMetaField, KubeObject } from "../../api/kube-object";
|
import { IKubeMetaField, KubeObject } from "../../api/kube-object";
|
||||||
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
import { DrawerItem, DrawerItemLabels } from "../drawer";
|
||||||
import { getDetailsUrl } from "../../navigation";
|
|
||||||
import { lookupApiLink } from "../../api/kube-api";
|
import { lookupApiLink } from "../../api/kube-api";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
import { KubeObjectStatusIcon } from "../kube-object-status-icon";
|
||||||
|
import { getDetailsUrl } from "./kube-object-details";
|
||||||
|
|
||||||
export interface KubeObjectMetaProps {
|
export interface KubeObjectMetaProps {
|
||||||
object: KubeObject;
|
object: KubeObject;
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import { clusterRoute, clusterURL } from "../+cluster";
|
|||||||
import { Config, configRoute, configURL } from "../+config";
|
import { Config, configRoute, configURL } from "../+config";
|
||||||
import { eventRoute, eventsURL } from "../+events";
|
import { eventRoute, eventsURL } from "../+events";
|
||||||
import { Apps, appsRoute, appsURL } from "../+apps";
|
import { Apps, appsRoute, appsURL } from "../+apps";
|
||||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||||
import { Workloads } from "../+workloads";
|
import { Workloads } from "../+workloads";
|
||||||
import { UserManagement } from "../+user-management";
|
import { UserManagement } from "../+user-management";
|
||||||
import { Storage } from "../+storage";
|
import { Storage } from "../+storage";
|
||||||
@ -75,21 +75,23 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] {
|
getTabLayoutRoutes(menu: ClusterPageMenuRegistration): TabLayoutRoute[] {
|
||||||
if (!menu.id) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const routes: TabLayoutRoute[] = [];
|
const routes: TabLayoutRoute[] = [];
|
||||||
|
|
||||||
clusterPageMenuRegistry.getSubItems(menu).forEach((subItem) => {
|
if (!menu.id) {
|
||||||
const subPage = clusterPageRegistry.getByPageMenuTarget(subItem.target);
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
clusterPageMenuRegistry.getSubItems(menu).forEach((subMenu) => {
|
||||||
|
const subPage = clusterPageRegistry.getByPageTarget(subMenu.target);
|
||||||
|
|
||||||
if (subPage) {
|
if (subPage) {
|
||||||
|
const { extensionId, id: pageId } = subPage;
|
||||||
|
|
||||||
routes.push({
|
routes.push({
|
||||||
routePath: subPage.routePath,
|
routePath: subPage.url,
|
||||||
url: getExtensionPageUrl({ extensionId: subPage.extensionId, pageId: subPage.id, params: subItem.target.params }),
|
url: getExtensionPageUrl({ extensionId, pageId, params: subMenu.target.params }),
|
||||||
title: subItem.title,
|
title: subMenu.title,
|
||||||
component: subPage.components.Page,
|
component: subPage.components.Page,
|
||||||
exact: subPage.exact
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -99,7 +101,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
|
|
||||||
renderRegisteredMenus() {
|
renderRegisteredMenus() {
|
||||||
return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => {
|
return clusterPageMenuRegistry.getRootItems().map((menuItem, index) => {
|
||||||
const registeredPage = clusterPageRegistry.getByPageMenuTarget(menuItem.target);
|
const registeredPage = clusterPageRegistry.getByPageTarget(menuItem.target);
|
||||||
const tabRoutes = this.getTabLayoutRoutes(menuItem);
|
const tabRoutes = this.getTabLayoutRoutes(menuItem);
|
||||||
const id = `registered-item-${index}`;
|
const id = `registered-item-${index}`;
|
||||||
let pageUrl: string;
|
let pageUrl: string;
|
||||||
@ -109,7 +111,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
const { extensionId, id: pageId } = registeredPage;
|
const { extensionId, id: pageId } = registeredPage;
|
||||||
|
|
||||||
pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params });
|
pageUrl = getExtensionPageUrl({ extensionId, pageId, params: menuItem.target.params });
|
||||||
isActive = isActiveRoute(registeredPage.routePath);
|
isActive = isActiveRoute(registeredPage.url);
|
||||||
} else if (tabRoutes.length > 0) {
|
} else if (tabRoutes.length > 0) {
|
||||||
pageUrl = tabRoutes[0].url;
|
pageUrl = tabRoutes[0].url;
|
||||||
isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath));
|
isActive = isActiveRoute(tabRoutes.map((tab) => tab.routePath));
|
||||||
@ -133,7 +135,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { toggle, isPinned, className } = this.props;
|
const { toggle, isPinned, className } = this.props;
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceUrlParam.toObjectParam();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={{ pinned: isPinned }}>
|
<SidebarContext.Provider value={{ pinned: isPinned }}>
|
||||||
|
|||||||
@ -1,19 +1,17 @@
|
|||||||
import "./table.scss";
|
import "./table.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { orderBy } from "lodash";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { computed, observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { autobind, cssNames, noop } from "../../utils";
|
import { autobind, cssNames, noop } from "../../utils";
|
||||||
import { TableRow, TableRowElem, TableRowProps } from "./table-row";
|
import { TableRow, TableRowElem, TableRowProps } from "./table-row";
|
||||||
import { TableHead, TableHeadElem, TableHeadProps } from "./table-head";
|
import { TableHead, TableHeadElem, TableHeadProps } from "./table-head";
|
||||||
import { TableCellElem } from "./table-cell";
|
import { TableCellElem } from "./table-cell";
|
||||||
import { VirtualList } from "../virtual-list";
|
import { VirtualList } from "../virtual-list";
|
||||||
import { navigation, setQueryParams } from "../../navigation";
|
import { createPageParam } from "../../navigation";
|
||||||
import orderBy from "lodash/orderBy";
|
|
||||||
import { ItemObject } from "../../item.store";
|
import { ItemObject } from "../../item.store";
|
||||||
|
|
||||||
// todo: refactor + decouple search from location
|
|
||||||
|
|
||||||
export type TableSortBy = string;
|
export type TableSortBy = string;
|
||||||
export type TableOrderBy = "asc" | "desc" | string;
|
export type TableOrderBy = "asc" | "desc" | string;
|
||||||
export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy };
|
export type TableSortParams = { sortBy: TableSortBy; orderBy: TableOrderBy };
|
||||||
@ -43,6 +41,16 @@ export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
|
|||||||
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>;
|
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sortByUrlParam = createPageParam({
|
||||||
|
name: "sort",
|
||||||
|
isSystem: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const orderByUrlParam = createPageParam({
|
||||||
|
name: "order",
|
||||||
|
isSystem: true,
|
||||||
|
});
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Table extends React.Component<TableProps> {
|
export class Table extends React.Component<TableProps> {
|
||||||
static defaultProps: TableProps = {
|
static defaultProps: TableProps = {
|
||||||
@ -53,18 +61,13 @@ export class Table extends React.Component<TableProps> {
|
|||||||
sortSyncWithUrl: true,
|
sortSyncWithUrl: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@observable sortParamsLocal = this.props.sortByDefault;
|
@observable sortParams: Partial<TableSortParams> = Object.assign(
|
||||||
|
this.props.sortSyncWithUrl ? {
|
||||||
@computed get sortParams(): Partial<TableSortParams> {
|
sortBy: sortByUrlParam.get(),
|
||||||
if (this.props.sortSyncWithUrl) {
|
orderBy: orderByUrlParam.get(),
|
||||||
const sortBy = navigation.searchParams.get("sortBy");
|
} : {},
|
||||||
const orderBy = navigation.searchParams.get("orderBy");
|
this.props.sortByDefault,
|
||||||
|
);
|
||||||
return { sortBy, orderBy };
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.sortParamsLocal || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHead() {
|
renderHead() {
|
||||||
const { sortable, children } = this.props;
|
const { sortable, children } = this.props;
|
||||||
@ -101,29 +104,24 @@ export class Table extends React.Component<TableProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSorted(items: any[]) {
|
getSorted(items: any[]) {
|
||||||
const { sortParams } = this;
|
const { sortBy, orderBy: order } = this.sortParams;
|
||||||
const sortingCallback = this.props.sortable[sortParams.sortBy] || noop;
|
const sortingCallback = this.props.sortable[sortBy] || noop;
|
||||||
|
|
||||||
return orderBy(
|
return orderBy(items, sortingCallback, order as any);
|
||||||
items,
|
|
||||||
sortingCallback,
|
|
||||||
sortParams.orderBy as any
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
protected onSort(params: TableSortParams) {
|
protected onSort({ sortBy, orderBy }: TableSortParams) {
|
||||||
|
this.sortParams = { sortBy, orderBy };
|
||||||
const { sortSyncWithUrl, onSort } = this.props;
|
const { sortSyncWithUrl, onSort } = this.props;
|
||||||
|
|
||||||
if (sortSyncWithUrl) {
|
if (sortSyncWithUrl) {
|
||||||
setQueryParams(params);
|
sortByUrlParam.set(sortBy);
|
||||||
}
|
orderByUrlParam.set(orderBy);
|
||||||
else {
|
|
||||||
this.sortParamsLocal = params;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onSort) {
|
if (onSort) {
|
||||||
onSort(params);
|
onSort({ sortBy, orderBy });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,136 +0,0 @@
|
|||||||
// Navigation helpers
|
|
||||||
|
|
||||||
import { matchPath, RouteProps } from "react-router";
|
|
||||||
import { reaction } from "mobx";
|
|
||||||
import { createObservableHistory } from "mobx-observable-history";
|
|
||||||
import { createBrowserHistory, LocationDescriptor } from "history";
|
|
||||||
import logger from "../main/logger";
|
|
||||||
import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route";
|
|
||||||
import { broadcastMessage, subscribeToBroadcast } from "../common/ipc";
|
|
||||||
|
|
||||||
export const history = createBrowserHistory();
|
|
||||||
export const navigation = createObservableHistory(history);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to a location. Works only in renderer.
|
|
||||||
*/
|
|
||||||
export function navigate(location: LocationDescriptor) {
|
|
||||||
const currentLocation = navigation.getPath();
|
|
||||||
|
|
||||||
navigation.push(location);
|
|
||||||
|
|
||||||
if (currentLocation === navigation.getPath()) {
|
|
||||||
navigation.goBack(); // prevent sequences of same url in history
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function matchParams<P>(route: string | string[] | RouteProps) {
|
|
||||||
return matchPath<P>(navigation.location.pathname, route);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isActiveRoute(route: string | string[] | RouteProps): boolean {
|
|
||||||
return !!matchParams(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
// common params for all pages
|
|
||||||
export interface IQueryParams {
|
|
||||||
namespaces?: string[]; // selected context namespaces
|
|
||||||
details?: string; // serialized resource details
|
|
||||||
selected?: string; // mark resource as selected
|
|
||||||
search?: string; // search-input value
|
|
||||||
sortBy?: string; // sorting params for table-list
|
|
||||||
orderBy?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getQueryString(params?: Partial<IQueryParams>, merge = true) {
|
|
||||||
const searchParams = navigation.searchParams.copyWith(params);
|
|
||||||
|
|
||||||
if (!merge) {
|
|
||||||
Array.from(searchParams.keys()).forEach(key => {
|
|
||||||
if (!(key in params)) searchParams.delete(key);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchParams.toString({ withPrefix: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setQueryParams<T>(params?: T & IQueryParams, { merge = true, replace = false } = {}) {
|
|
||||||
const newSearch = getQueryString(params, merge);
|
|
||||||
|
|
||||||
navigation.merge({ search: newSearch }, replace);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDetails() {
|
|
||||||
return navigation.searchParams.get("details");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSelectedDetails() {
|
|
||||||
return navigation.searchParams.get("selected") || getDetails();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDetailsUrl(details: string) {
|
|
||||||
if (!details) return "";
|
|
||||||
|
|
||||||
return getQueryString({
|
|
||||||
details,
|
|
||||||
selected: getSelectedDetails(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show details. Works only in renderer.
|
|
||||||
*/
|
|
||||||
export function showDetails(path: string, resetSelected = true) {
|
|
||||||
navigation.searchParams.merge({
|
|
||||||
details: path,
|
|
||||||
selected: resetSelected ? null : getSelectedDetails(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide details. Works only in renderer.
|
|
||||||
*/
|
|
||||||
export function hideDetails() {
|
|
||||||
showDetails(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSearch(text: string) {
|
|
||||||
navigation.replace({
|
|
||||||
search: getQueryString({ search: text })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSearch() {
|
|
||||||
return navigation.searchParams.get("search") || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMatchedClusterId(): string {
|
|
||||||
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
|
||||||
exact: true,
|
|
||||||
path: clusterViewRoute.path
|
|
||||||
});
|
|
||||||
|
|
||||||
return matched?.params.clusterId;
|
|
||||||
}
|
|
||||||
|
|
||||||
//-- EVENTS
|
|
||||||
|
|
||||||
if (process.isMainFrame) {
|
|
||||||
// Keep track of active cluster-id for handling IPC/menus/etc.
|
|
||||||
reaction(() => getMatchedClusterId(), clusterId => {
|
|
||||||
broadcastMessage("cluster-view:current-id", clusterId);
|
|
||||||
}, {
|
|
||||||
fireImmediately: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle navigation via IPC (e.g. from top menu)
|
|
||||||
subscribeToBroadcast("renderer:navigate", (event, location: LocationDescriptor) => {
|
|
||||||
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
|
|
||||||
navigate(location);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reload dashboard window
|
|
||||||
subscribeToBroadcast("renderer:reload", () => {
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
31
src/renderer/navigation/events.ts
Normal file
31
src/renderer/navigation/events.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import { reaction } from "mobx";
|
||||||
|
import { getMatchedClusterId, navigate } from "./helpers";
|
||||||
|
import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
|
||||||
|
export function bindEvents() {
|
||||||
|
if (!ipcRenderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.isMainFrame) {
|
||||||
|
// Keep track of active cluster-id for handling IPC/menus/etc.
|
||||||
|
reaction(() => getMatchedClusterId(), clusterId => {
|
||||||
|
broadcastMessage("cluster-view:current-id", clusterId);
|
||||||
|
}, {
|
||||||
|
fireImmediately: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle navigation via IPC (e.g. from top menu)
|
||||||
|
subscribeToBroadcast("renderer:navigate", (event, url: string) => {
|
||||||
|
logger.info(`[IPC]: ${event.type} ${JSON.stringify(url)}`, event);
|
||||||
|
navigate(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload dashboard window
|
||||||
|
subscribeToBroadcast("renderer:reload", () => {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
36
src/renderer/navigation/helpers.ts
Normal file
36
src/renderer/navigation/helpers.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { LocationDescriptor } from "history";
|
||||||
|
import { matchPath, RouteProps } from "react-router";
|
||||||
|
import { PageParam, PageSystemParamInit } from "./page-param";
|
||||||
|
import { clusterViewRoute, IClusterViewRouteParams } from "../components/cluster-manager/cluster-view.route";
|
||||||
|
import { navigation } from "./history";
|
||||||
|
|
||||||
|
export function navigate(location: LocationDescriptor) {
|
||||||
|
const currentLocation = navigation.getPath();
|
||||||
|
|
||||||
|
navigation.push(location);
|
||||||
|
|
||||||
|
if (currentLocation === navigation.getPath()) {
|
||||||
|
navigation.goBack(); // prevent sequences of same url in history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPageParam<V = string>(init: PageSystemParamInit<V>) {
|
||||||
|
return new PageParam<V>(init, navigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchRoute<P>(route: string | string[] | RouteProps) {
|
||||||
|
return matchPath<P>(navigation.location.pathname, route);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isActiveRoute(route: string | string[] | RouteProps): boolean {
|
||||||
|
return !!matchRoute(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMatchedClusterId(): string {
|
||||||
|
const matched = matchPath<IClusterViewRouteParams>(navigation.location.pathname, {
|
||||||
|
exact: true,
|
||||||
|
path: clusterViewRoute.path
|
||||||
|
});
|
||||||
|
|
||||||
|
return matched?.params.clusterId;
|
||||||
|
}
|
||||||
6
src/renderer/navigation/history.ts
Normal file
6
src/renderer/navigation/history.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import { createBrowserHistory, createMemoryHistory } from "history";
|
||||||
|
import { createObservableHistory } from "mobx-observable-history";
|
||||||
|
|
||||||
|
export const history = ipcRenderer ? createBrowserHistory() : createMemoryHistory();
|
||||||
|
export const navigation = createObservableHistory(history);
|
||||||
8
src/renderer/navigation/index.ts
Normal file
8
src/renderer/navigation/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Navigation (renderer)
|
||||||
|
|
||||||
|
import { bindEvents } from "./events";
|
||||||
|
|
||||||
|
export * from "./history";
|
||||||
|
export * from "./helpers";
|
||||||
|
|
||||||
|
bindEvents();
|
||||||
135
src/renderer/navigation/page-param.ts
Normal file
135
src/renderer/navigation/page-param.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// Manage observable URL-param from document.location.search
|
||||||
|
import { IObservableHistory } from "mobx-observable-history";
|
||||||
|
|
||||||
|
export interface PageParamInit<V = any> {
|
||||||
|
name: string;
|
||||||
|
defaultValue?: V;
|
||||||
|
defaultValueStringified?: string | string[]; // serialized version of "defaultValue"
|
||||||
|
multiValues?: boolean; // false == by default
|
||||||
|
multiValueSep?: string; // joining multiple values with separator, default: ","
|
||||||
|
skipEmpty?: boolean; // skip empty value(s), e.g. "?param=", default: true
|
||||||
|
parse?(value: string[]): V; // deserialize from URL
|
||||||
|
stringify?(value: V): string | string[]; // serialize params to URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageSystemParamInit<V = any> extends PageParamInit<V> {
|
||||||
|
isSystem?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PageParam<V = any> {
|
||||||
|
static SYSTEM_PREFIX = "lens-";
|
||||||
|
|
||||||
|
readonly name: string;
|
||||||
|
protected urlName: string;
|
||||||
|
|
||||||
|
constructor(readonly init: PageParamInit<V> | PageSystemParamInit<V>, protected history: IObservableHistory) {
|
||||||
|
const { isSystem, name } = init as PageSystemParamInit;
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
this.init.skipEmpty ??= true;
|
||||||
|
this.init.multiValueSep ??= ",";
|
||||||
|
|
||||||
|
// prefixing to avoid collisions with extensions
|
||||||
|
this.urlName = `${isSystem ? PageParam.SYSTEM_PREFIX : ""}${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty(value: V | any) {
|
||||||
|
return [value].flat().every(value => value == "" || value == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(values: string[]): V {
|
||||||
|
const { parse, multiValues } = this.init;
|
||||||
|
|
||||||
|
if (!multiValues) values.splice(1); // reduce values to single item
|
||||||
|
const parsedValues = [parse ? parse(values) : values].flat();
|
||||||
|
|
||||||
|
return multiValues ? parsedValues : parsedValues[0] as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
stringify(value: V = this.get()): string {
|
||||||
|
const { stringify, multiValues, multiValueSep, skipEmpty } = this.init;
|
||||||
|
|
||||||
|
if (skipEmpty && this.isEmpty(value)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (multiValues) {
|
||||||
|
const values = [value].flat();
|
||||||
|
const stringValues = [stringify ? stringify(value) : values.map(String)].flat();
|
||||||
|
|
||||||
|
return stringValues.join(multiValueSep);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [stringify ? stringify(value) : String(value)].flat()[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): V {
|
||||||
|
const value = this.parse(this.getRaw());
|
||||||
|
|
||||||
|
if (this.init.skipEmpty && this.isEmpty(value)) {
|
||||||
|
return this.getDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(value: V, { mergeGlobals = true, replaceHistory = false } = {}) {
|
||||||
|
const search = this.toSearchString({ mergeGlobals, value });
|
||||||
|
|
||||||
|
this.history.merge({ search }, replaceHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRaw(value: string | string[]) {
|
||||||
|
const { history, urlName } = this;
|
||||||
|
const { multiValues, multiValueSep, skipEmpty } = this.init;
|
||||||
|
const paramValue = multiValues ? [value].flat().join(multiValueSep) : String(value);
|
||||||
|
|
||||||
|
if (skipEmpty && this.isEmpty(paramValue)) {
|
||||||
|
history.searchParams.delete(urlName);
|
||||||
|
} else {
|
||||||
|
history.searchParams.set(urlName, paramValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRaw(): string[] {
|
||||||
|
const { history, urlName } = this;
|
||||||
|
const { multiValueSep } = this.init;
|
||||||
|
|
||||||
|
return history.searchParams.getAsArray(urlName, multiValueSep);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultValue() {
|
||||||
|
const { defaultValue, defaultValueStringified } = this.init;
|
||||||
|
|
||||||
|
return defaultValueStringified ? this.parse([defaultValueStringified].flat()) : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.history.searchParams.delete(this.urlName);
|
||||||
|
}
|
||||||
|
|
||||||
|
toSearchString({ withPrefix = true, mergeGlobals = true, value = this.get() } = {}): string {
|
||||||
|
const { history, urlName, init: { skipEmpty } } = this;
|
||||||
|
const searchParams = new URLSearchParams(mergeGlobals ? history.location.search : "");
|
||||||
|
|
||||||
|
searchParams.set(urlName, this.stringify(value));
|
||||||
|
|
||||||
|
if (skipEmpty) {
|
||||||
|
searchParams.forEach((value: any, paramName) => {
|
||||||
|
if (this.isEmpty(value)) searchParams.delete(paramName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.from(searchParams).length > 0) {
|
||||||
|
return `${withPrefix ? "?" : ""}${searchParams}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
toObjectParam(value = this.get()): Record<string, V> {
|
||||||
|
return {
|
||||||
|
[this.urlName]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/renderer/utils/__tests__/jsonPath.test.tsx
Normal file
41
src/renderer/utils/__tests__/jsonPath.test.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { parseJsonPath } from "../jsonPath";
|
||||||
|
|
||||||
|
describe("parseJsonPath", () => {
|
||||||
|
test("should convert \\. to use indexed notation", () => {
|
||||||
|
const res = parseJsonPath(".metadata.labels.kubesphere\\.io/alias-name");
|
||||||
|
|
||||||
|
expect(res).toBe(".metadata.labels['kubesphere.io/alias-name']");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should convert keys with escpaped charatecrs to use indexed notation", () => {
|
||||||
|
const res = parseJsonPath(".metadata.labels.kubesphere\\\"io/alias-name");
|
||||||
|
|
||||||
|
expect(res).toBe(".metadata.labels['kubesphere\"io/alias-name']");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should convert '-' to use indexed notation", () => {
|
||||||
|
const res = parseJsonPath(".metadata.labels.alias-name");
|
||||||
|
|
||||||
|
expect(res).toBe(".metadata.labels['alias-name']");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle scenario when both \\. and indexed notation are present", () => {
|
||||||
|
const rest = parseJsonPath(".metadata.labels\\.serving['some.other.item']");
|
||||||
|
|
||||||
|
expect(rest).toBe(".metadata['labels.serving']['some.other.item']");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("should not touch given jsonPath if no invalid characters present", () => {
|
||||||
|
const res = parseJsonPath(".status.conditions[?(@.type=='Ready')].status");
|
||||||
|
|
||||||
|
expect(res).toBe(".status.conditions[?(@.type=='Ready')].status");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strips '\\' away from the result", () => {
|
||||||
|
const res = parseJsonPath(".metadata.labels['serving\\.knative\\.dev/configuration']");
|
||||||
|
|
||||||
|
expect(res).toBe(".metadata.labels['serving.knative.dev/configuration']");
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
35
src/renderer/utils/jsonPath.ts
Normal file
35
src/renderer/utils/jsonPath.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Helper to convert strings used for jsonPath where \. or - is present to use indexed notation,
|
||||||
|
// for example: .metadata.labels.kubesphere\.io/alias-name -> .metadata.labels['kubesphere\.io/alias-name']
|
||||||
|
|
||||||
|
export function parseJsonPath(jsonPath: string) {
|
||||||
|
let pathExpression = jsonPath;
|
||||||
|
|
||||||
|
if (jsonPath.match(/[\\-]/g)) { // search for '\' and '-'
|
||||||
|
const [first, ...rest] = jsonPath.split(/(?<=\w)\./); // split jsonPath by '.' (\. cases are ignored)
|
||||||
|
|
||||||
|
pathExpression = `${convertToIndexNotation(first, true)}${rest.map(value => convertToIndexNotation(value)).join("")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip '\' characters from the result
|
||||||
|
return pathExpression.replace(/\\/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToIndexNotation(key: string, firstItem = false) {
|
||||||
|
if (key.match(/[\\-]/g)) { // check if found '\' and '-' in key
|
||||||
|
if (key.includes("[")) { // handle cases where key contains [...]
|
||||||
|
const keyToConvert = key.match(/^.*(?=\[)/g); // get the text from the key before '['
|
||||||
|
|
||||||
|
if (keyToConvert && keyToConvert[0].match(/[\\-]/g)) { // check if that part contains illegal characters
|
||||||
|
return key.replace(keyToConvert[0], `['${keyToConvert[0]}']`); // surround key with '[' and ']'
|
||||||
|
} else {
|
||||||
|
return `.${key}`; // otherwise return as is with leading '.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `['${key}']`;
|
||||||
|
} else { // no illegal chracters found, do not touch
|
||||||
|
const prefix = firstItem ? "" : ".";
|
||||||
|
|
||||||
|
return `${prefix}${key}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
yarn.lock
95
yarn.lock
@ -2116,14 +2116,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.7.4.tgz#607685669bb1bbde2300bc58ba43486cbbee1f0a"
|
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.7.4.tgz#607685669bb1bbde2300bc58ba43486cbbee1f0a"
|
||||||
integrity sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw==
|
integrity sha512-fdg0NO4qpuHWtZk6dASgsrBggY+8N4dWthl1bAQG9ceKUNKFjqpHaDKCAhRUI6y8vavG7hLSJ4YBwJtZyZEXqw==
|
||||||
|
|
||||||
"@types/material-ui@^0.21.7":
|
|
||||||
version "0.21.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/material-ui/-/material-ui-0.21.7.tgz#2a4ab77a56a16adef044ba607edde5214151a5d8"
|
|
||||||
integrity sha512-OxGu+Jfm3d8IVYu5w2cqosSFU+8KJYCeVjw1jLZ7DzgoE7KpSFFpbDJKWhV1FAf/HEQXzL1IpX6PmLwINlE4Xg==
|
|
||||||
dependencies:
|
|
||||||
"@types/react" "*"
|
|
||||||
"@types/react-addons-linked-state-mixin" "*"
|
|
||||||
|
|
||||||
"@types/md5-file@^4.0.2":
|
"@types/md5-file@^4.0.2":
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/md5-file/-/md5-file-4.0.2.tgz#c7241e88f4aa17218c774befb0fc34f33f21fe36"
|
resolved "https://registry.yarnpkg.com/@types/md5-file/-/md5-file-4.0.2.tgz#c7241e88f4aa17218c774befb0fc34f33f21fe36"
|
||||||
@ -2266,13 +2258,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||||
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
|
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
|
||||||
|
|
||||||
"@types/react-addons-linked-state-mixin@*":
|
|
||||||
version "0.14.21"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-addons-linked-state-mixin/-/react-addons-linked-state-mixin-0.14.21.tgz#3abf296fe09d036c233ebe55f4562f3e6233af49"
|
|
||||||
integrity sha512-3UF7Szd3JyuU+z90kqu8L4VdDWp7SUC0eRjV2QmMEliaHODGLi5XyO5ctS50K/lG6fjC0dSAPVbvnqv0nPoGMQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/react" "*"
|
|
||||||
|
|
||||||
"@types/react-beautiful-dnd@^13.0.0":
|
"@types/react-beautiful-dnd@^13.0.0":
|
||||||
version "13.0.0"
|
version "13.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
|
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
|
||||||
@ -2287,10 +2272,17 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-router-dom@^5.1.5":
|
"@types/react-dom@^17.0.0":
|
||||||
version "5.1.5"
|
version "17.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090"
|
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.0.tgz#b3b691eb956c4b3401777ee67b900cb28415d95a"
|
||||||
integrity sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw==
|
integrity sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-router-dom@^5.1.6":
|
||||||
|
version "5.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.6.tgz#07b14e7ab1893a837c8565634960dc398564b1fb"
|
||||||
|
integrity sha512-gjrxYqxz37zWEdMVvQtWPFMFj1dRDb4TGOcgyOfSXTrEXdF92L00WE3C471O3TV/RF1oskcStkXsOU0Ete4s/g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/history" "*"
|
"@types/history" "*"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
@ -2327,7 +2319,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react@*", "@types/react@^16.9.35":
|
"@types/react@*":
|
||||||
version "16.9.35"
|
version "16.9.35"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
|
||||||
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
|
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
|
||||||
@ -2335,6 +2327,14 @@
|
|||||||
"@types/prop-types" "*"
|
"@types/prop-types" "*"
|
||||||
csstype "^2.2.0"
|
csstype "^2.2.0"
|
||||||
|
|
||||||
|
"@types/react@^17.0.0":
|
||||||
|
version "17.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.0.tgz#5af3eb7fad2807092f0046a1302b7823e27919b8"
|
||||||
|
integrity sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==
|
||||||
|
dependencies:
|
||||||
|
"@types/prop-types" "*"
|
||||||
|
csstype "^3.0.2"
|
||||||
|
|
||||||
"@types/relateurl@*":
|
"@types/relateurl@*":
|
||||||
version "0.2.28"
|
version "0.2.28"
|
||||||
resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6"
|
resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6"
|
||||||
@ -5038,6 +5038,11 @@ csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.5, csstype@^2.6.7:
|
|||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
|
||||||
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
|
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
|
||||||
|
|
||||||
|
csstype@^3.0.2:
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8"
|
||||||
|
integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==
|
||||||
|
|
||||||
currently-unhandled@^0.4.1:
|
currently-unhandled@^0.4.1:
|
||||||
version "0.4.1"
|
version "0.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
|
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
|
||||||
@ -11209,6 +11214,13 @@ p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-try "^2.0.0"
|
p-try "^2.0.0"
|
||||||
|
|
||||||
|
p-limit@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
|
||||||
|
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
|
||||||
|
dependencies:
|
||||||
|
yocto-queue "^0.1.0"
|
||||||
|
|
||||||
p-locate@^2.0.0:
|
p-locate@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
|
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
|
||||||
@ -12123,15 +12135,14 @@ react-beautiful-dnd@^13.0.0:
|
|||||||
redux "^4.0.4"
|
redux "^4.0.4"
|
||||||
use-memo-one "^1.1.1"
|
use-memo-one "^1.1.1"
|
||||||
|
|
||||||
react-dom@^16.13.1:
|
react-dom@^17.0.1:
|
||||||
version "16.13.1"
|
version "17.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
|
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
|
||||||
integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
|
integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.2"
|
scheduler "^0.20.1"
|
||||||
scheduler "^0.19.1"
|
|
||||||
|
|
||||||
react-input-autosize@^2.2.2:
|
react-input-autosize@^2.2.2:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
@ -12232,15 +12243,6 @@ react-zlib-js@^1.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.4.tgz#dd2b9fbf56d5ab224fa7a99affbbedeba9aa3dc7"
|
resolved "https://registry.yarnpkg.com/react-zlib-js/-/react-zlib-js-1.0.4.tgz#dd2b9fbf56d5ab224fa7a99affbbedeba9aa3dc7"
|
||||||
integrity sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A==
|
integrity sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A==
|
||||||
|
|
||||||
react@^16.14.0:
|
|
||||||
version "16.14.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d"
|
|
||||||
integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==
|
|
||||||
dependencies:
|
|
||||||
loose-envify "^1.1.0"
|
|
||||||
object-assign "^4.1.1"
|
|
||||||
prop-types "^15.6.2"
|
|
||||||
|
|
||||||
react@^16.8.0:
|
react@^16.8.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
||||||
@ -12250,6 +12252,14 @@ react@^16.8.0:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
|
react@^17.0.1:
|
||||||
|
version "17.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
||||||
|
integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.1.0"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
read-cmd-shim@^1.0.1, read-cmd-shim@^1.0.5:
|
read-cmd-shim@^1.0.1, read-cmd-shim@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16"
|
resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16"
|
||||||
@ -12949,10 +12959,10 @@ saxes@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xmlchars "^2.2.0"
|
xmlchars "^2.2.0"
|
||||||
|
|
||||||
scheduler@^0.19.1:
|
scheduler@^0.20.1:
|
||||||
version "0.19.1"
|
version "0.20.1"
|
||||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
|
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c"
|
||||||
integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==
|
integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
@ -14545,7 +14555,7 @@ typeface-roboto@^0.0.75:
|
|||||||
resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b"
|
resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-0.0.75.tgz#98d5ba35ec234bbc7172374c8297277099cc712b"
|
||||||
integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==
|
integrity sha512-VrR/IiH00Z1tFP4vDGfwZ1esNqTiDMchBEXYY9kilT6wRGgFoCAlgkEUMHb1E3mB0FsfZhv756IF0+R+SFPfdg==
|
||||||
|
|
||||||
typescript@^4.0.2:
|
typescript@4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
|
||||||
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
|
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
|
||||||
@ -15572,6 +15582,11 @@ yn@3.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||||
|
|
||||||
|
yocto-queue@^0.1.0:
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
zip-stream@^1.2.0:
|
zip-stream@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04"
|
resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user