mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge remote-tracking branch 'origin/master' into webpack5-upgrade-attempt
This commit is contained in:
commit
3782204bc4
@ -21,6 +21,42 @@ The categories provided by Lens itself have the following names:
|
|||||||
|
|
||||||
To register a category, call the `Main.Catalog.catalogCategories.add()` and `Renderer.Catalog.catalogCategories.add()` with instances of your class.
|
To register a category, call the `Main.Catalog.catalogCategories.add()` and `Renderer.Catalog.catalogCategories.add()` with instances of your class.
|
||||||
|
|
||||||
|
### Custom Category Views
|
||||||
|
|
||||||
|
By default when a specific category is selected in the catalog page a list of entities of the group and kind that the category has registered.
|
||||||
|
It is possible to register custom views for specific categories by registering them on your `Renderer.LensExtension` class.
|
||||||
|
|
||||||
|
A registration takes the form of a [Common.Types.CustomCategoryViewRegistration](../api/interfaces/Common.Types.CustomCategoryViewRegistration.md)
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Renderer, Common } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
function MyKubernetesClusterView({
|
||||||
|
category,
|
||||||
|
}: Common.Types.CustomCategoryViewProps) {
|
||||||
|
return <div>My view: {category.getId()}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class extends Renderer.LensExtension {
|
||||||
|
customCategoryViews = [
|
||||||
|
{
|
||||||
|
group: "entity.k8slens.dev",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
priority: 10,
|
||||||
|
components: {
|
||||||
|
View: MyKubernetesClusterView,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Will register a new view for the KubernetesCluster category, and because the priority is < 50 it will be displayed above the default list view.
|
||||||
|
|
||||||
|
The default list view has a priority of 50 and and custom views with priority (defaulting to 50) >= 50 will be displayed afterwards.
|
||||||
|
|
||||||
## Entities
|
## Entities
|
||||||
|
|
||||||
An entity is the data within the catalog.
|
An entity is the data within the catalog.
|
||||||
|
|||||||
@ -195,8 +195,8 @@
|
|||||||
"@hapi/call": "^8.0.1",
|
"@hapi/call": "^8.0.1",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.16.1",
|
"@kubernetes/client-node": "^0.16.1",
|
||||||
"@ogre-tools/injectable": "3.1.1",
|
"@ogre-tools/injectable": "3.2.0",
|
||||||
"@ogre-tools/injectable-react": "3.1.1",
|
"@ogre-tools/injectable-react": "3.2.0",
|
||||||
"@sentry/electron": "^2.5.4",
|
"@sentry/electron": "^2.5.4",
|
||||||
"@sentry/integrations": "^6.15.0",
|
"@sentry/integrations": "^6.15.0",
|
||||||
"@types/circular-dependency-plugin": "5.0.4",
|
"@types/circular-dependency-plugin": "5.0.4",
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { autoBind, formatDuration } from "../../utils";
|
import { formatDuration } from "../../utils";
|
||||||
import capitalize from "lodash/capitalize";
|
import capitalize from "lodash/capitalize";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
import { helmChartStore } from "../../../renderer/components/+apps-helm-charts/helm-chart.store";
|
import { helmChartStore } from "../../../renderer/components/+apps-helm-charts/helm-chart.store";
|
||||||
@ -80,9 +80,9 @@ interface EndpointQuery {
|
|||||||
const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
const endpoint = buildURLPositional<EndpointParams, EndpointQuery>("/v2/releases/:namespace?/:name?/:route?");
|
||||||
|
|
||||||
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
|
||||||
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
|
const releases = await apiBase.get<HelmReleaseDto[]>(endpoint({ namespace }));
|
||||||
|
|
||||||
return releases.map(HelmRelease.create);
|
return releases.map(toHelmRelease);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
|
||||||
@ -152,7 +152,7 @@ export async function rollbackRelease(name: string, namespace: string, revision:
|
|||||||
return apiBase.put(path, { data });
|
return apiBase.put(path, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HelmRelease {
|
interface HelmReleaseDto {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
name: string;
|
name: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
@ -162,27 +162,30 @@ export interface HelmRelease {
|
|||||||
revision: string;
|
revision: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HelmRelease implements ItemObject {
|
export interface HelmRelease extends HelmReleaseDto, ItemObject {
|
||||||
constructor(data: any) {
|
getNs: () => string
|
||||||
Object.assign(this, data);
|
getChart: (withVersion?: boolean) => string
|
||||||
autoBind(this);
|
getRevision: () => number
|
||||||
}
|
getStatus: () => string
|
||||||
|
getVersion: () => string
|
||||||
|
getUpdated: (humanize?: boolean, compact?: boolean) => string | number
|
||||||
|
getRepo: () => Promise<string>
|
||||||
|
}
|
||||||
|
|
||||||
static create(data: any) {
|
const toHelmRelease = (release: HelmReleaseDto) : HelmRelease => ({
|
||||||
return new HelmRelease(data);
|
...release,
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
getId() {
|
||||||
return this.namespace + this.name;
|
return this.namespace + this.name;
|
||||||
}
|
},
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
},
|
||||||
|
|
||||||
getNs() {
|
getNs() {
|
||||||
return this.namespace;
|
return this.namespace;
|
||||||
}
|
},
|
||||||
|
|
||||||
getChart(withVersion = false) {
|
getChart(withVersion = false) {
|
||||||
let chart = this.chart;
|
let chart = this.chart;
|
||||||
@ -194,24 +197,24 @@ export class HelmRelease implements ItemObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return chart;
|
return chart;
|
||||||
}
|
},
|
||||||
|
|
||||||
getRevision() {
|
getRevision() {
|
||||||
return parseInt(this.revision, 10);
|
return parseInt(this.revision, 10);
|
||||||
}
|
},
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return capitalize(this.status);
|
return capitalize(this.status);
|
||||||
}
|
},
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||||
|
|
||||||
return versions?.[0] ?? "";
|
return versions?.[0] ?? "";
|
||||||
}
|
},
|
||||||
|
|
||||||
getUpdated(humanize = true, compact = true) {
|
getUpdated(humanize = true, compact = true) {
|
||||||
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
|
||||||
const updatedDate = new Date(updated).getTime();
|
const updatedDate = new Date(updated).getTime();
|
||||||
const diff = Date.now() - updatedDate;
|
const diff = Date.now() - updatedDate;
|
||||||
|
|
||||||
@ -220,7 +223,7 @@ export class HelmRelease implements ItemObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return diff;
|
return diff;
|
||||||
}
|
},
|
||||||
|
|
||||||
// Helm does not store from what repository the release is installed,
|
// Helm does not store from what repository the release is installed,
|
||||||
// so we have to try to guess it by searching charts
|
// so we have to try to guess it by searching charts
|
||||||
@ -228,8 +231,10 @@ export class HelmRelease implements ItemObject {
|
|||||||
const chartName = this.getChart();
|
const chartName = this.getChart();
|
||||||
const version = this.getVersion();
|
const version = this.getVersion();
|
||||||
const versions = await helmChartStore.getVersions(chartName);
|
const versions = await helmChartStore.getVersions(chartName);
|
||||||
const chartVersion = versions.find(chartVersion => chartVersion.version === version);
|
const chartVersion = versions.find(
|
||||||
|
(chartVersion) => chartVersion.version === version,
|
||||||
|
);
|
||||||
|
|
||||||
return chartVersion ? chartVersion.repo : "";
|
return chartVersion ? chartVersion.repo : "";
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
|
|||||||
@ -34,6 +34,9 @@ export const extensionRoute: RouteProps = {
|
|||||||
path: `${preferencesRoute.path}/extensions`,
|
path: `${preferencesRoute.path}/extensions`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const terminalRoute: RouteProps = {
|
||||||
|
path: `${preferencesRoute.path}/terminal`,
|
||||||
|
};
|
||||||
export const preferencesURL = buildURL(preferencesRoute.path);
|
export const preferencesURL = buildURL(preferencesRoute.path);
|
||||||
export const appURL = buildURL(appRoute.path);
|
export const appURL = buildURL(appRoute.path);
|
||||||
export const proxyURL = buildURL(proxyRoute.path);
|
export const proxyURL = buildURL(proxyRoute.path);
|
||||||
@ -41,3 +44,4 @@ export const kubernetesURL = buildURL(kubernetesRoute.path);
|
|||||||
export const editorURL = buildURL(editorRoute.path);
|
export const editorURL = buildURL(editorRoute.path);
|
||||||
export const telemetryURL = buildURL(telemetryRoute.path);
|
export const telemetryURL = buildURL(telemetryRoute.path);
|
||||||
export const extensionURL = buildURL(extensionRoute.path);
|
export const extensionURL = buildURL(extensionRoute.path);
|
||||||
|
export const terminalURL = buildURL(terminalRoute.path);
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import { getAppVersion, ObservableToggleSet } from "../utils";
|
|||||||
import type { editor } from "monaco-editor";
|
import type { editor } from "monaco-editor";
|
||||||
import merge from "lodash/merge";
|
import merge from "lodash/merge";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import { defaultTheme } from "../vars";
|
import { defaultTheme, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
|
||||||
|
|
||||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
@ -18,19 +18,29 @@ export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
|||||||
|
|
||||||
export interface KubeconfigSyncValue {
|
export interface KubeconfigSyncValue {
|
||||||
}
|
}
|
||||||
|
export interface TerminalConfig {
|
||||||
|
fontSize: number;
|
||||||
|
fontFamily: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultTerminalConfig: TerminalConfig = {
|
||||||
|
fontSize: defaultFontSize,
|
||||||
|
fontFamily: defaultTerminalFontFamily,
|
||||||
|
};
|
||||||
|
|
||||||
export type EditorConfiguration = Pick<editor.IStandaloneEditorConstructionOptions,
|
export type EditorConfiguration = Pick<editor.IStandaloneEditorConstructionOptions,
|
||||||
"minimap" | "tabSize" | "lineNumbers">;
|
"minimap" | "tabSize" | "lineNumbers" | "fontSize" | "fontFamily">;
|
||||||
|
|
||||||
export const defaultEditorConfig: EditorConfiguration = {
|
export const defaultEditorConfig: EditorConfiguration = {
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
lineNumbers: "on",
|
lineNumbers: "on",
|
||||||
|
fontSize: defaultFontSize,
|
||||||
|
fontFamily: defaultEditorFontFamily,
|
||||||
minimap: {
|
minimap: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
side: "right",
|
side: "right",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PreferenceDescription<T, R = T> {
|
interface PreferenceDescription<T, R = T> {
|
||||||
fromStore(val: T | undefined): R;
|
fromStore(val: T | undefined): R;
|
||||||
toStore(val: R): T | undefined;
|
toStore(val: R): T | undefined;
|
||||||
@ -273,6 +283,15 @@ const editorConfiguration: PreferenceDescription<EditorConfiguration, EditorConf
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const terminalConfig: PreferenceDescription<TerminalConfig, TerminalConfig> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return merge(defaultTerminalConfig, val);
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const updateChannels = new Map([
|
const updateChannels = new Map([
|
||||||
["latest", {
|
["latest", {
|
||||||
label: "Stable",
|
label: "Stable",
|
||||||
@ -358,6 +377,7 @@ export const DESCRIPTORS = {
|
|||||||
syncKubeconfigEntries,
|
syncKubeconfigEntries,
|
||||||
editorConfiguration,
|
editorConfiguration,
|
||||||
terminalCopyOnSelect,
|
terminalCopyOnSelect,
|
||||||
|
terminalConfig,
|
||||||
updateChannel,
|
updateChannel,
|
||||||
extensionRegistryUrl,
|
extensionRegistryUrl,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { getAppVersion } from "../utils/app-version";
|
|||||||
import { kubeConfigDefaultPath } from "../kube-helpers";
|
import { kubeConfigDefaultPath } from "../kube-helpers";
|
||||||
import { appEventBus } from "../app-event-bus/event-bus";
|
import { appEventBus } from "../app-event-bus/event-bus";
|
||||||
import { ObservableToggleSet, toJS } from "../../renderer/utils";
|
import { ObservableToggleSet, toJS } from "../../renderer/utils";
|
||||||
import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
|
import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel, TerminalConfig } from "./preferences-helpers";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
|
|
||||||
export interface UserStoreModel {
|
export interface UserStoreModel {
|
||||||
@ -57,6 +57,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
@observable downloadBinariesPath?: string;
|
@observable downloadBinariesPath?: string;
|
||||||
@observable kubectlBinariesPath?: string;
|
@observable kubectlBinariesPath?: string;
|
||||||
@observable terminalCopyOnSelect: boolean;
|
@observable terminalCopyOnSelect: boolean;
|
||||||
|
@observable terminalConfig: TerminalConfig;
|
||||||
@observable updateChannel?: string;
|
@observable updateChannel?: string;
|
||||||
@observable extensionRegistryUrl: ExtensionRegistry;
|
@observable extensionRegistryUrl: ExtensionRegistry;
|
||||||
|
|
||||||
@ -185,6 +186,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
||||||
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
|
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
|
||||||
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
|
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
|
||||||
|
this.terminalConfig = DESCRIPTORS.terminalConfig.fromStore(preferences?.terminalConfig);
|
||||||
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
|
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
|
||||||
this.extensionRegistryUrl = DESCRIPTORS.extensionRegistryUrl.fromStore(preferences?.extensionRegistryUrl);
|
this.extensionRegistryUrl = DESCRIPTORS.extensionRegistryUrl.fromStore(preferences?.extensionRegistryUrl);
|
||||||
}
|
}
|
||||||
@ -210,6 +212,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
||||||
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
||||||
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
|
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
|
||||||
|
terminalConfig: DESCRIPTORS.terminalConfig.toStore(this.terminalConfig),
|
||||||
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
|
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
|
||||||
extensionRegistryUrl: DESCRIPTORS.extensionRegistryUrl.toStore(this.extensionRegistryUrl),
|
extensionRegistryUrl: DESCRIPTORS.extensionRegistryUrl.toStore(this.extensionRegistryUrl),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -17,3 +17,11 @@ export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
|
|||||||
|
|
||||||
return map.get(key);
|
return map.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `getOrInsert` but specifically for when `V` is `Map<any, any>` so that
|
||||||
|
* the typings are inferred.
|
||||||
|
*/
|
||||||
|
export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map<MK, MV> {
|
||||||
|
return getOrInsert(map, key, new Map<MK, MV>());
|
||||||
|
}
|
||||||
|
|||||||
@ -26,6 +26,9 @@ export const productName = packageInfo.productName;
|
|||||||
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
||||||
export const publicPath = "/build/" as string;
|
export const publicPath = "/build/" as string;
|
||||||
export const defaultTheme = "lens-dark" as string;
|
export const defaultTheme = "lens-dark" as string;
|
||||||
|
export const defaultFontSize = 12;
|
||||||
|
export const defaultTerminalFontFamily = "RobotoMono";
|
||||||
|
export const defaultEditorFontFamily = "RobotoMono";
|
||||||
|
|
||||||
// Webpack build paths
|
// Webpack build paths
|
||||||
export const contextDir = process.cwd();
|
export const contextDir = process.cwd();
|
||||||
|
|||||||
@ -11,3 +11,4 @@ export type { PageRegistration, RegisteredPage, PageParams, PageComponentProps,
|
|||||||
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry";
|
export type { ClusterPageMenuRegistration, ClusterPageMenuComponents } from "../registries/page-menu-registry";
|
||||||
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
export type { StatusBarRegistration } from "../registries/status-bar-registry";
|
||||||
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler";
|
export type { ProtocolHandlerRegistration, RouteParams as ProtocolRouteParams, RouteHandler as ProtocolRouteHandler } from "../registries/protocol-handler";
|
||||||
|
export type { CustomCategoryViewProps, CustomCategoryViewComponents, CustomCategoryViewRegistration } from "../../renderer/components/+catalog/custom-views";
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/
|
|||||||
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
||||||
import type { AppPreferenceRegistration } from "../renderer/components/+preferences/app-preferences/app-preference-registration";
|
import type { AppPreferenceRegistration } from "../renderer/components/+preferences/app-preferences/app-preference-registration";
|
||||||
import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns";
|
import type { AdditionalCategoryColumnRegistration } from "../renderer/components/+catalog/custom-category-columns";
|
||||||
|
import type { CustomCategoryViewRegistration } from "../renderer/components/+catalog/custom-views";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -35,6 +36,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
topBarItems: TopBarRegistration[] = [];
|
topBarItems: TopBarRegistration[] = [];
|
||||||
additionalCategoryColumns: AdditionalCategoryColumnRegistration[] = [];
|
additionalCategoryColumns: AdditionalCategoryColumnRegistration[] = [];
|
||||||
|
customCategoryViews: CustomCategoryViewRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
|
|||||||
@ -14,11 +14,10 @@ export class PortForwardRoute {
|
|||||||
const { namespace, resourceType, resourceName } = params;
|
const { namespace, resourceType, resourceName } = params;
|
||||||
const port = Number(query.get("port"));
|
const port = Number(query.get("port"));
|
||||||
const forwardPort = Number(query.get("forwardPort"));
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
const protocol = query.get("protocol");
|
|
||||||
|
|
||||||
const portForward = PortForward.getPortforward({
|
const portForward = PortForward.getPortforward({
|
||||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
namespace, port, forwardPort, protocol,
|
namespace, port, forwardPort,
|
||||||
});
|
});
|
||||||
|
|
||||||
respondJson(response, { port: portForward?.forwardPort ?? null });
|
respondJson(response, { port: portForward?.forwardPort ?? null });
|
||||||
|
|||||||
@ -16,7 +16,6 @@ export interface PortForwardArgs {
|
|||||||
name: string;
|
name: string;
|
||||||
port: number;
|
port: number;
|
||||||
forwardPort: number;
|
forwardPort: number;
|
||||||
protocol?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
@ -32,8 +31,7 @@ export class PortForward {
|
|||||||
pf.kind == forward.kind &&
|
pf.kind == forward.kind &&
|
||||||
pf.name == forward.name &&
|
pf.name == forward.name &&
|
||||||
pf.namespace == forward.namespace &&
|
pf.namespace == forward.namespace &&
|
||||||
pf.port == forward.port &&
|
pf.port == forward.port
|
||||||
(!forward.protocol || pf.protocol == forward.protocol)
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,7 +42,6 @@ export class PortForward {
|
|||||||
public name: string;
|
public name: string;
|
||||||
public port: number;
|
public port: number;
|
||||||
public forwardPort: number;
|
public forwardPort: number;
|
||||||
public protocol: string;
|
|
||||||
|
|
||||||
constructor(private dependencies: Dependencies, public pathToKubeConfig: string, args: PortForwardArgs) {
|
constructor(private dependencies: Dependencies, public pathToKubeConfig: string, args: PortForwardArgs) {
|
||||||
this.clusterId = args.clusterId;
|
this.clusterId = args.clusterId;
|
||||||
@ -53,7 +50,6 @@ export class PortForward {
|
|||||||
this.name = args.name;
|
this.name = args.name;
|
||||||
this.port = args.port;
|
this.port = args.port;
|
||||||
this.forwardPort = args.forwardPort;
|
this.forwardPort = args.forwardPort;
|
||||||
this.protocol = args.protocol ?? "http";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
|
|||||||
@ -18,7 +18,6 @@ export const routePortForward =
|
|||||||
const { namespace, resourceType, resourceName } = params;
|
const { namespace, resourceType, resourceName } = params;
|
||||||
const port = Number(query.get("port"));
|
const port = Number(query.get("port"));
|
||||||
const forwardPort = Number(query.get("forwardPort"));
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
const protocol = query.get("protocol");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let portForward = PortForward.getPortforward({
|
let portForward = PortForward.getPortforward({
|
||||||
@ -28,7 +27,6 @@ export const routePortForward =
|
|||||||
namespace,
|
namespace,
|
||||||
port,
|
port,
|
||||||
forwardPort,
|
forwardPort,
|
||||||
protocol,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!portForward) {
|
if (!portForward) {
|
||||||
@ -46,7 +44,6 @@ export const routePortForward =
|
|||||||
name: resourceName,
|
name: resourceName,
|
||||||
port,
|
port,
|
||||||
forwardPort: thePort,
|
forwardPort: thePort,
|
||||||
protocol,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const started = await portForward.start();
|
const started = await portForward.start();
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import {
|
||||||
|
createRelease,
|
||||||
|
IReleaseCreatePayload,
|
||||||
|
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
import releasesInjectable from "../releases.injectable";
|
||||||
|
|
||||||
|
const createReleaseInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const releases = di.inject(releasesInjectable);
|
||||||
|
|
||||||
|
return async (payload: IReleaseCreatePayload) => {
|
||||||
|
const release = await createRelease(payload);
|
||||||
|
|
||||||
|
releases.invalidate();
|
||||||
|
|
||||||
|
return release;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createReleaseInjectable;
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import {
|
||||||
|
deleteRelease,
|
||||||
|
HelmRelease,
|
||||||
|
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
import releasesInjectable from "../releases.injectable";
|
||||||
|
|
||||||
|
const deleteReleaseInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const releases = di.inject(releasesInjectable);
|
||||||
|
|
||||||
|
return async (release: HelmRelease) => {
|
||||||
|
await deleteRelease(release.getName(), release.getNs());
|
||||||
|
|
||||||
|
releases.invalidate();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default deleteReleaseInjectable;
|
||||||
@ -1,321 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import "./release-details.scss";
|
|
||||||
|
|
||||||
import React, { Component } from "react";
|
|
||||||
import groupBy from "lodash/groupBy";
|
|
||||||
import isEqual from "lodash/isEqual";
|
|
||||||
import { makeObservable, observable, reaction } from "mobx";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import kebabCase from "lodash/kebabCase";
|
|
||||||
import { getRelease, getReleaseValues, HelmRelease, IReleaseDetails } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
|
||||||
import { HelmReleaseMenu } from "./release-menu";
|
|
||||||
import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
|
|
||||||
import { Badge } from "../badge";
|
|
||||||
import { cssNames, stopPropagation } from "../../utils";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
|
||||||
import { Spinner } from "../spinner";
|
|
||||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
|
||||||
import { Button } from "../button";
|
|
||||||
import type { ReleaseStore } from "./release.store";
|
|
||||||
import { Notifications } from "../notifications";
|
|
||||||
import { ThemeStore } from "../../theme.store";
|
|
||||||
import { apiManager } from "../../../common/k8s-api/api-manager";
|
|
||||||
import { SubTitle } from "../layout/sub-title";
|
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
|
||||||
import { Secret } from "../../../common/k8s-api/endpoints";
|
|
||||||
import { getDetailsUrl } from "../kube-detail-params";
|
|
||||||
import { Checkbox } from "../checkbox";
|
|
||||||
import { MonacoEditor } from "../monaco-editor";
|
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
|
||||||
import releaseStoreInjectable from "./release-store.injectable";
|
|
||||||
import createUpgradeChartTabInjectable
|
|
||||||
from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
release: HelmRelease;
|
|
||||||
hideDetails(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
releaseStore: ReleaseStore
|
|
||||||
createUpgradeChartTab: (release: HelmRelease) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
@observer
|
|
||||||
class NonInjectedReleaseDetails extends Component<Props & Dependencies> {
|
|
||||||
@observable details: IReleaseDetails | null = null;
|
|
||||||
@observable values = "";
|
|
||||||
@observable valuesLoading = false;
|
|
||||||
@observable showOnlyUserSuppliedValues = true;
|
|
||||||
@observable saving = false;
|
|
||||||
@observable releaseSecret: Secret;
|
|
||||||
@observable error?: string = undefined;
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
disposeOnUnmount(this, [
|
|
||||||
reaction(() => this.props.release, release => {
|
|
||||||
if (!release) return;
|
|
||||||
this.loadDetails();
|
|
||||||
this.loadValues();
|
|
||||||
this.releaseSecret = null;
|
|
||||||
}),
|
|
||||||
reaction(() => secretsStore.getItems(), () => {
|
|
||||||
if (!this.props.release) return;
|
|
||||||
const { getReleaseSecret } = this.props.releaseStore;
|
|
||||||
const { release } = this.props;
|
|
||||||
const secret = getReleaseSecret(release);
|
|
||||||
|
|
||||||
if (this.releaseSecret) {
|
|
||||||
if (isEqual(this.releaseSecret.getLabels(), secret.getLabels())) return;
|
|
||||||
this.loadDetails();
|
|
||||||
}
|
|
||||||
this.releaseSecret = secret;
|
|
||||||
}),
|
|
||||||
reaction(() => this.showOnlyUserSuppliedValues, () => {
|
|
||||||
this.loadValues();
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: Props & Dependencies) {
|
|
||||||
super(props);
|
|
||||||
makeObservable(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadDetails() {
|
|
||||||
const { release } = this.props;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.details = null;
|
|
||||||
this.details = await getRelease(release.getName(), release.getNs());
|
|
||||||
} catch (error) {
|
|
||||||
this.error = `Failed to get release details: ${error}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadValues() {
|
|
||||||
const { release } = this.props;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.valuesLoading = true;
|
|
||||||
this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.showOnlyUserSuppliedValues)) ?? "";
|
|
||||||
} catch (error) {
|
|
||||||
Notifications.error(`Failed to load values for ${release.getName()}: ${error}`);
|
|
||||||
this.values = "";
|
|
||||||
} finally {
|
|
||||||
this.valuesLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValues = async () => {
|
|
||||||
const { release } = this.props;
|
|
||||||
const name = release.getName();
|
|
||||||
const namespace = release.getNs();
|
|
||||||
const data = {
|
|
||||||
chart: release.getChart(),
|
|
||||||
repo: await release.getRepo(),
|
|
||||||
version: release.getVersion(),
|
|
||||||
values: this.values,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.saving = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.props.releaseStore.update(name, namespace, data);
|
|
||||||
Notifications.ok(
|
|
||||||
<p>Release <b>{name}</b> successfully updated!</p>,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Notifications.error(err);
|
|
||||||
}
|
|
||||||
this.saving = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
upgradeVersion = () => {
|
|
||||||
const { release, hideDetails } = this.props;
|
|
||||||
|
|
||||||
this.props.createUpgradeChartTab(release);
|
|
||||||
hideDetails();
|
|
||||||
};
|
|
||||||
|
|
||||||
renderValues() {
|
|
||||||
const { values, valuesLoading, saving } = this;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="values">
|
|
||||||
<DrawerTitle title="Values"/>
|
|
||||||
<div className="flex column gaps">
|
|
||||||
<Checkbox
|
|
||||||
label="User-supplied values only"
|
|
||||||
value={this.showOnlyUserSuppliedValues}
|
|
||||||
onChange={value => this.showOnlyUserSuppliedValues = value}
|
|
||||||
disabled={valuesLoading}
|
|
||||||
/>
|
|
||||||
<MonacoEditor
|
|
||||||
readOnly={valuesLoading}
|
|
||||||
className={cssNames({ loading: valuesLoading })}
|
|
||||||
style={{ minHeight: 300 }}
|
|
||||||
value={values}
|
|
||||||
onChange={text => this.values = text}
|
|
||||||
>
|
|
||||||
{valuesLoading && <Spinner center/>}
|
|
||||||
</MonacoEditor>
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
label="Save"
|
|
||||||
waiting={saving}
|
|
||||||
disabled={valuesLoading}
|
|
||||||
onClick={this.updateValues}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderNotes() {
|
|
||||||
if (!this.details.info?.notes) return null;
|
|
||||||
const { notes } = this.details.info;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="notes">
|
|
||||||
{notes}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderResources() {
|
|
||||||
const { resources } = this.details;
|
|
||||||
|
|
||||||
if (!resources) return null;
|
|
||||||
const groups = groupBy(resources, item => item.kind);
|
|
||||||
const tables = Object.entries(groups).map(([kind, items]) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={kind}>
|
|
||||||
<SubTitle title={kind}/>
|
|
||||||
<Table scrollable={false}>
|
|
||||||
<TableHead sticky={false}>
|
|
||||||
<TableCell className="name">Name</TableCell>
|
|
||||||
{items[0].getNs() && <TableCell className="namespace">Namespace</TableCell>}
|
|
||||||
<TableCell className="age">Age</TableCell>
|
|
||||||
</TableHead>
|
|
||||||
{items.map(item => {
|
|
||||||
const name = item.getName();
|
|
||||||
const namespace = item.getNs();
|
|
||||||
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == item.apiVersion);
|
|
||||||
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={item.getId()}>
|
|
||||||
<TableCell className="name">
|
|
||||||
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
|
|
||||||
</TableCell>
|
|
||||||
{namespace && <TableCell className="namespace">{namespace}</TableCell>}
|
|
||||||
<TableCell className="age">{item.getAge()}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Table>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="resources">
|
|
||||||
{tables}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderContent() {
|
|
||||||
const { release } = this.props;
|
|
||||||
|
|
||||||
if (!release) return null;
|
|
||||||
|
|
||||||
if (this.error) {
|
|
||||||
return (
|
|
||||||
<div className="loading-error">
|
|
||||||
{this.error}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.details) {
|
|
||||||
return <Spinner center/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DrawerItem name="Chart" className="chart">
|
|
||||||
<div className="flex gaps align-center">
|
|
||||||
<span>{release.getChart()}</span>
|
|
||||||
<Button
|
|
||||||
primary
|
|
||||||
label="Upgrade"
|
|
||||||
className="box right upgrade"
|
|
||||||
onClick={this.upgradeVersion}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DrawerItem>
|
|
||||||
<DrawerItem name="Updated">
|
|
||||||
{release.getUpdated()} ago ({release.updated})
|
|
||||||
</DrawerItem>
|
|
||||||
<DrawerItem name="Namespace">
|
|
||||||
{release.getNs()}
|
|
||||||
</DrawerItem>
|
|
||||||
<DrawerItem name="Version" onClick={stopPropagation}>
|
|
||||||
<div className="version flex gaps align-center">
|
|
||||||
<span>
|
|
||||||
{release.getVersion()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</DrawerItem>
|
|
||||||
<DrawerItem name="Status" className="status" labelsOnly>
|
|
||||||
<Badge
|
|
||||||
label={release.getStatus()}
|
|
||||||
className={kebabCase(release.getStatus())}
|
|
||||||
/>
|
|
||||||
</DrawerItem>
|
|
||||||
{this.renderValues()}
|
|
||||||
<DrawerTitle title="Notes"/>
|
|
||||||
{this.renderNotes()}
|
|
||||||
<DrawerTitle title="Resources"/>
|
|
||||||
{this.renderResources()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { release, hideDetails } = this.props;
|
|
||||||
const title = release ? `Release: ${release.getName()}` : "";
|
|
||||||
const toolbar = <HelmReleaseMenu release={release} toolbar hideDetails={hideDetails}/>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
className={cssNames("ReleaseDetails", ThemeStore.getInstance().activeTheme.type)}
|
|
||||||
usePortal={true}
|
|
||||||
open={!!release}
|
|
||||||
title={title}
|
|
||||||
onClose={hideDetails}
|
|
||||||
toolbar={toolbar}
|
|
||||||
>
|
|
||||||
{this.renderContent()}
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ReleaseDetails = withInjectables<Dependencies, Props>(
|
|
||||||
NonInjectedReleaseDetails,
|
|
||||||
|
|
||||||
{
|
|
||||||
getProps: (di, props) => ({
|
|
||||||
releaseStore: di.inject(releaseStoreInjectable),
|
|
||||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
|
||||||
...props,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { getRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||||
|
import releaseInjectable from "./release.injectable";
|
||||||
|
|
||||||
|
const releaseDetailsInjectable = getInjectable({
|
||||||
|
instantiate: (di) =>
|
||||||
|
asyncComputed(async () => {
|
||||||
|
const release = di.inject(releaseInjectable).value.get();
|
||||||
|
|
||||||
|
return await getRelease(release.name, release.namespace);
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default releaseDetailsInjectable;
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import "release.mixins";
|
@import "../release.mixins";
|
||||||
|
|
||||||
.ReleaseDetails {
|
.ReleaseDetails {
|
||||||
.DrawerItem {
|
.DrawerItem {
|
||||||
@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./release-details.scss";
|
||||||
|
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import groupBy from "lodash/groupBy";
|
||||||
|
import { computed, makeObservable, observable } from "mobx";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import kebabCase from "lodash/kebabCase";
|
||||||
|
import type { HelmRelease, IReleaseDetails, IReleaseUpdateDetails, IReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
import { HelmReleaseMenu } from "../release-menu";
|
||||||
|
import { Drawer, DrawerItem, DrawerTitle } from "../../drawer";
|
||||||
|
import { Badge } from "../../badge";
|
||||||
|
import { cssNames, stopPropagation } from "../../../utils";
|
||||||
|
import { Observer, observer } from "mobx-react";
|
||||||
|
import { Spinner } from "../../spinner";
|
||||||
|
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||||
|
import { Button } from "../../button";
|
||||||
|
import { Notifications } from "../../notifications";
|
||||||
|
import { ThemeStore } from "../../../theme.store";
|
||||||
|
import { apiManager } from "../../../../common/k8s-api/api-manager";
|
||||||
|
import { SubTitle } from "../../layout/sub-title";
|
||||||
|
import { getDetailsUrl } from "../../kube-detail-params";
|
||||||
|
import { Checkbox } from "../../checkbox";
|
||||||
|
import { MonacoEditor } from "../../monaco-editor";
|
||||||
|
import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import createUpgradeChartTabInjectable from "../../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable";
|
||||||
|
import updateReleaseInjectable from "../update-release/update-release.injectable";
|
||||||
|
import releaseInjectable from "./release.injectable";
|
||||||
|
import releaseDetailsInjectable from "./release-details.injectable";
|
||||||
|
import releaseValuesInjectable from "./release-values.injectable";
|
||||||
|
import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
hideDetails(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
release: IAsyncComputed<HelmRelease>
|
||||||
|
releaseDetails: IAsyncComputed<IReleaseDetails>
|
||||||
|
releaseValues: IAsyncComputed<string>
|
||||||
|
updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>
|
||||||
|
createUpgradeChartTab: (release: HelmRelease) => void
|
||||||
|
userSuppliedValuesAreShown: { toggle: () => void, value: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class NonInjectedReleaseDetails extends Component<Props & Dependencies> {
|
||||||
|
@observable saving = false;
|
||||||
|
|
||||||
|
private nonSavedValues: string;
|
||||||
|
|
||||||
|
constructor(props: Props & Dependencies) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get release() {
|
||||||
|
return this.props.release.value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get details() {
|
||||||
|
return this.props.releaseDetails.value.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValues = async () => {
|
||||||
|
const name = this.release.getName();
|
||||||
|
const namespace = this.release.getNs();
|
||||||
|
const data = {
|
||||||
|
chart: this.release.getChart(),
|
||||||
|
repo: await this.release.getRepo(),
|
||||||
|
version: this.release.getVersion(),
|
||||||
|
values: this.nonSavedValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.props.updateRelease(name, namespace, data);
|
||||||
|
Notifications.ok(
|
||||||
|
<p>Release <b>{name}</b> successfully updated!</p>,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.props.releaseValues.invalidate();
|
||||||
|
} catch (err) {
|
||||||
|
Notifications.error(err);
|
||||||
|
}
|
||||||
|
this.saving = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
upgradeVersion = () => {
|
||||||
|
const { hideDetails } = this.props;
|
||||||
|
|
||||||
|
this.props.createUpgradeChartTab(this.release);
|
||||||
|
hideDetails();
|
||||||
|
};
|
||||||
|
|
||||||
|
renderValues() {
|
||||||
|
return (
|
||||||
|
<Observer>
|
||||||
|
{() => {
|
||||||
|
const { saving } = this;
|
||||||
|
|
||||||
|
const releaseValuesArePending =
|
||||||
|
this.props.releaseValues.pending.get();
|
||||||
|
|
||||||
|
this.nonSavedValues = this.props.releaseValues.value.get();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="values">
|
||||||
|
<DrawerTitle title="Values" />
|
||||||
|
<div className="flex column gaps">
|
||||||
|
<Checkbox
|
||||||
|
label="User-supplied values only"
|
||||||
|
value={this.props.userSuppliedValuesAreShown.value}
|
||||||
|
onChange={this.props.userSuppliedValuesAreShown.toggle}
|
||||||
|
disabled={releaseValuesArePending}
|
||||||
|
/>
|
||||||
|
<MonacoEditor
|
||||||
|
style={{ minHeight: 300 }}
|
||||||
|
value={this.nonSavedValues}
|
||||||
|
onChange={(text) => (this.nonSavedValues = text)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
label="Save"
|
||||||
|
waiting={saving}
|
||||||
|
disabled={releaseValuesArePending}
|
||||||
|
onClick={this.updateValues}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Observer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNotes() {
|
||||||
|
if (!this.details.info?.notes) return null;
|
||||||
|
const { notes } = this.details.info;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notes">
|
||||||
|
{notes}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderResources() {
|
||||||
|
const { resources } = this.details;
|
||||||
|
|
||||||
|
if (!resources) return null;
|
||||||
|
const groups = groupBy(resources, item => item.kind);
|
||||||
|
const tables = Object.entries(groups).map(([kind, items]) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={kind}>
|
||||||
|
<SubTitle title={kind}/>
|
||||||
|
<Table scrollable={false}>
|
||||||
|
<TableHead sticky={false}>
|
||||||
|
<TableCell className="name">Name</TableCell>
|
||||||
|
{items[0].getNs() && <TableCell className="namespace">Namespace</TableCell>}
|
||||||
|
<TableCell className="age">Age</TableCell>
|
||||||
|
</TableHead>
|
||||||
|
{items.map(item => {
|
||||||
|
const name = item.getName();
|
||||||
|
const namespace = item.getNs();
|
||||||
|
const api = apiManager.getApi(api => api.kind === kind && api.apiVersionWithGroup == item.apiVersion);
|
||||||
|
const detailsUrl = api ? getDetailsUrl(api.getUrl({ name, namespace })) : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={item.getId()}>
|
||||||
|
<TableCell className="name">
|
||||||
|
{detailsUrl ? <Link to={detailsUrl}>{name}</Link> : name}
|
||||||
|
</TableCell>
|
||||||
|
{namespace && <TableCell className="namespace">{namespace}</TableCell>}
|
||||||
|
<TableCell className="age">{item.getAge()}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="resources">
|
||||||
|
{tables}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
if (!this.release) return null;
|
||||||
|
|
||||||
|
if (!this.details) {
|
||||||
|
return <Spinner center/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<DrawerItem name="Chart" className="chart">
|
||||||
|
<div className="flex gaps align-center">
|
||||||
|
<span>{this.release.getChart()}</span>
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
label="Upgrade"
|
||||||
|
className="box right upgrade"
|
||||||
|
onClick={this.upgradeVersion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItem name="Updated">
|
||||||
|
{this.release.getUpdated()} ago ({this.release.updated})
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItem name="Namespace">
|
||||||
|
{this.release.getNs()}
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItem name="Version" onClick={stopPropagation}>
|
||||||
|
<div className="version flex gaps align-center">
|
||||||
|
<span>
|
||||||
|
{this.release.getVersion()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DrawerItem>
|
||||||
|
<DrawerItem name="Status" className="status" labelsOnly>
|
||||||
|
<Badge
|
||||||
|
label={this.release.getStatus()}
|
||||||
|
className={kebabCase(this.release.getStatus())}
|
||||||
|
/>
|
||||||
|
</DrawerItem>
|
||||||
|
{this.renderValues()}
|
||||||
|
<DrawerTitle title="Notes"/>
|
||||||
|
{this.renderNotes()}
|
||||||
|
<DrawerTitle title="Resources"/>
|
||||||
|
{this.renderResources()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { hideDetails } = this.props;
|
||||||
|
const title = this.release ? `Release: ${this.release.getName()}` : "";
|
||||||
|
const toolbar = <HelmReleaseMenu release={this.release} toolbar hideDetails={hideDetails}/>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
className={cssNames("ReleaseDetails", ThemeStore.getInstance().activeTheme.type)}
|
||||||
|
usePortal={true}
|
||||||
|
open={!!this.release}
|
||||||
|
title={title}
|
||||||
|
onClose={hideDetails}
|
||||||
|
toolbar={toolbar}
|
||||||
|
>
|
||||||
|
{this.renderContent()}
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReleaseDetails = withInjectables<Dependencies, Props>(
|
||||||
|
NonInjectedReleaseDetails,
|
||||||
|
|
||||||
|
{
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
release: di.inject(releaseInjectable),
|
||||||
|
releaseDetails: di.inject(releaseDetailsInjectable),
|
||||||
|
releaseValues: di.inject(releaseValuesInjectable),
|
||||||
|
|
||||||
|
userSuppliedValuesAreShown: di.inject(userSuppliedValuesAreShownInjectable),
|
||||||
|
|
||||||
|
updateRelease: di.inject(updateReleaseInjectable),
|
||||||
|
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import { matchPath } from "react-router";
|
||||||
|
import observableHistoryInjectable from "../../../navigation/observable-history.injectable";
|
||||||
|
import { releaseRoute, ReleaseRouteParams } from "../../../../common/routes";
|
||||||
|
|
||||||
|
const releaseRouteParametersInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const observableHistory = di.inject(observableHistoryInjectable);
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const releasePathParameters = matchPath<ReleaseRouteParams>(observableHistory.location.pathname, {
|
||||||
|
path: releaseRoute.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!releasePathParameters) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return releasePathParameters.params;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default releaseRouteParametersInjectable;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||||
|
import releaseInjectable from "./release.injectable";
|
||||||
|
import { Notifications } from "../../notifications";
|
||||||
|
import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable";
|
||||||
|
|
||||||
|
const releaseValuesInjectable = getInjectable({
|
||||||
|
instantiate: (di) =>
|
||||||
|
asyncComputed(async () => {
|
||||||
|
const release = di.inject(releaseInjectable).value.get();
|
||||||
|
const userSuppliedValuesAreShown = di.inject(userSuppliedValuesAreShownInjectable).value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await getReleaseValues(release.getName(), release.getNs(), !userSuppliedValuesAreShown) ?? "";
|
||||||
|
} catch (error) {
|
||||||
|
Notifications.error(`Failed to load values for ${release.getName()}: ${error}`);
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default releaseValuesInjectable;
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { matches } from "lodash/fp";
|
||||||
|
import releasesInjectable from "../releases.injectable";
|
||||||
|
import releaseRouteParametersInjectable from "./release-route-parameters.injectable";
|
||||||
|
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||||
|
|
||||||
|
const releaseInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const releases = di.inject(releasesInjectable);
|
||||||
|
const releaseRouteParameters = di.inject(releaseRouteParametersInjectable);
|
||||||
|
|
||||||
|
return asyncComputed(async () => {
|
||||||
|
const { name, namespace } = releaseRouteParameters.get();
|
||||||
|
|
||||||
|
if (!name || !namespace) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases.value.get().find(matches({ name, namespace }));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default releaseInjectable;
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
|
const userSuppliedValuesAreShownInjectable = getInjectable({
|
||||||
|
instantiate: () => {
|
||||||
|
const state = observable.box(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get value() {
|
||||||
|
return state.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle: () => {
|
||||||
|
state.set(!state.get());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default userSuppliedValuesAreShownInjectable;
|
||||||
|
|
||||||
@ -6,16 +6,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import type { ReleaseStore } from "./release.store";
|
|
||||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||||
import { MenuItem } from "../menu";
|
import { MenuItem } from "../menu";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import releaseStoreInjectable from "./release-store.injectable";
|
import createUpgradeChartTabInjectable from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable";
|
||||||
import createUpgradeChartTabInjectable
|
import releaseRollbackDialogModelInjectable from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable";
|
||||||
from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable";
|
import deleteReleaseInjectable from "./delete-release/delete-release.injectable";
|
||||||
import releaseRollbackDialogModelInjectable
|
|
||||||
from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable";
|
|
||||||
|
|
||||||
interface Props extends MenuActionsProps {
|
interface Props extends MenuActionsProps {
|
||||||
release: HelmRelease;
|
release: HelmRelease;
|
||||||
@ -23,14 +20,14 @@ interface Props extends MenuActionsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
releaseStore: ReleaseStore
|
deleteRelease: (release: HelmRelease) => Promise<any>
|
||||||
createUpgradeChartTab: (release: HelmRelease) => void
|
createUpgradeChartTab: (release: HelmRelease) => void
|
||||||
openRollbackDialog: (release: HelmRelease) => void
|
openRollbackDialog: (release: HelmRelease) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
class NonInjectedHelmReleaseMenu extends React.Component<Props & Dependencies> {
|
class NonInjectedHelmReleaseMenu extends React.Component<Props & Dependencies> {
|
||||||
remove = () => {
|
remove = () => {
|
||||||
return this.props.releaseStore.remove(this.props.release);
|
return this.props.deleteRelease(this.props.release);
|
||||||
};
|
};
|
||||||
|
|
||||||
upgrade = () => {
|
upgrade = () => {
|
||||||
@ -87,7 +84,7 @@ export const HelmReleaseMenu = withInjectables<Dependencies, Props>(
|
|||||||
|
|
||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
releaseStore: di.inject(releaseStoreInjectable),
|
deleteRelease: di.inject(deleteReleaseInjectable),
|
||||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||||
openRollbackDialog: di.inject(releaseRollbackDialogModelInjectable).open,
|
openRollbackDialog: di.inject(releaseRollbackDialogModelInjectable).open,
|
||||||
|
|
||||||
|
|||||||
@ -15,16 +15,16 @@ import { Select, SelectOption } from "../select";
|
|||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import orderBy from "lodash/orderBy";
|
import orderBy from "lodash/orderBy";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import releaseStoreInjectable from "./release-store.injectable";
|
|
||||||
import releaseRollbackDialogModelInjectable
|
import releaseRollbackDialogModelInjectable
|
||||||
from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable";
|
from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable";
|
||||||
import type { ReleaseRollbackDialogModel } from "./release-rollback-dialog-model/release-rollback-dialog-model";
|
import type { ReleaseRollbackDialogModel } from "./release-rollback-dialog-model/release-rollback-dialog-model";
|
||||||
|
import rollbackReleaseInjectable from "./rollback-release/rollback-release.injectable";
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
rollbackRelease: (releaseName: string, namespace: string, revisionNumber: number) => Promise<any>
|
rollbackRelease: (releaseName: string, namespace: string, revisionNumber: number) => Promise<void>
|
||||||
model: ReleaseRollbackDialogModel
|
model: ReleaseRollbackDialogModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ export const ReleaseRollbackDialog = withInjectables<Dependencies, Props>(
|
|||||||
|
|
||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
rollbackRelease: di.inject(releaseStoreInjectable).rollback,
|
rollbackRelease: di.inject(rollbackReleaseInjectable),
|
||||||
model: di.inject(releaseRollbackDialogModelInjectable),
|
model: di.inject(releaseRollbackDialogModelInjectable),
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import { ReleaseStore } from "./release.store";
|
|
||||||
import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable";
|
|
||||||
|
|
||||||
const releaseStoreInjectable = getInjectable({
|
|
||||||
instantiate: (di) => new ReleaseStore({
|
|
||||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
|
||||||
}),
|
|
||||||
|
|
||||||
lifecycle: lifecycleEnum.singleton,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default releaseStoreInjectable;
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import isEqual from "lodash/isEqual";
|
|
||||||
import { action, observable, reaction, when, makeObservable } from "mobx";
|
|
||||||
import { autoBind } from "../../utils";
|
|
||||||
import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
|
||||||
import { ItemStore } from "../../../common/item.store";
|
|
||||||
import type { Secret } from "../../../common/k8s-api/endpoints";
|
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
|
||||||
import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store";
|
|
||||||
import { Notifications } from "../notifications";
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
namespaceStore: NamespaceStore
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReleaseStore extends ItemStore<HelmRelease> {
|
|
||||||
releaseSecrets = observable.map<string, Secret>();
|
|
||||||
|
|
||||||
constructor(private dependencies: Dependencies ) {
|
|
||||||
super();
|
|
||||||
makeObservable(this);
|
|
||||||
autoBind(this);
|
|
||||||
|
|
||||||
when(() => secretsStore.isLoaded, () => {
|
|
||||||
this.releaseSecrets.replace(this.getReleaseSecrets());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watchAssociatedSecrets(): (() => void) {
|
|
||||||
return reaction(() => secretsStore.getItems(), () => {
|
|
||||||
if (this.isLoading) return;
|
|
||||||
const newSecrets = this.getReleaseSecrets();
|
|
||||||
const amountChanged = newSecrets.length !== this.releaseSecrets.size;
|
|
||||||
const labelsChanged = newSecrets.some(([id, secret]) => (
|
|
||||||
!isEqual(secret.getLabels(), this.releaseSecrets.get(id)?.getLabels())
|
|
||||||
));
|
|
||||||
|
|
||||||
if (amountChanged || labelsChanged) {
|
|
||||||
this.loadFromContextNamespaces();
|
|
||||||
}
|
|
||||||
this.releaseSecrets.replace(newSecrets);
|
|
||||||
}, {
|
|
||||||
fireImmediately: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watchSelectedNamespaces(): (() => void) {
|
|
||||||
return reaction(() => this.dependencies.namespaceStore.context.contextNamespaces, namespaces => {
|
|
||||||
this.loadAll(namespaces);
|
|
||||||
}, {
|
|
||||||
fireImmediately: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getReleaseSecrets() {
|
|
||||||
return secretsStore
|
|
||||||
.getByLabel({ owner: "helm" })
|
|
||||||
.map(s => [s.getId(), s] as const);
|
|
||||||
}
|
|
||||||
|
|
||||||
getReleaseSecret(release: HelmRelease) {
|
|
||||||
return secretsStore.getByLabel({
|
|
||||||
owner: "helm",
|
|
||||||
name: release.getName(),
|
|
||||||
})
|
|
||||||
.find(secret => secret.getNs() == release.getNs());
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async loadAll(namespaces: string[]) {
|
|
||||||
this.isLoading = true;
|
|
||||||
this.isLoaded = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const items = await this.loadItems(namespaces);
|
|
||||||
|
|
||||||
this.items.replace(this.sortItems(items));
|
|
||||||
this.isLoaded = true;
|
|
||||||
this.failedLoading = false;
|
|
||||||
} catch (error) {
|
|
||||||
this.failedLoading = true;
|
|
||||||
console.warn("Loading Helm Chart releases has failed", error);
|
|
||||||
|
|
||||||
if (error.error) {
|
|
||||||
Notifications.error(error.error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadFromContextNamespaces(): Promise<void> {
|
|
||||||
return this.loadAll(this.dependencies.namespaceStore.context.contextNamespaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadItems(namespaces: string[]) {
|
|
||||||
const isLoadingAll = this.dependencies.namespaceStore.context.allNamespaces?.length > 1
|
|
||||||
&& this.dependencies.namespaceStore.context.cluster.accessibleNamespaces.length === 0
|
|
||||||
&& this.dependencies.namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
|
|
||||||
|
|
||||||
if (isLoadingAll) {
|
|
||||||
return listReleases();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise // load resources per namespace
|
|
||||||
.all(namespaces.map(namespace => listReleases(namespace)))
|
|
||||||
.then(items => items.flat());
|
|
||||||
}
|
|
||||||
|
|
||||||
create = async (payload: IReleaseCreatePayload) => {
|
|
||||||
const response = await createRelease(payload);
|
|
||||||
|
|
||||||
if (this.isLoaded) this.loadFromContextNamespaces();
|
|
||||||
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
|
|
||||||
const response = await updateRelease(name, namespace, payload);
|
|
||||||
|
|
||||||
if (this.isLoaded) this.loadFromContextNamespaces();
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
rollback = async (name: string, namespace: string, revision: number) => {
|
|
||||||
const response = await rollbackRelease(name, namespace, revision);
|
|
||||||
|
|
||||||
if (this.isLoaded) this.loadFromContextNamespaces();
|
|
||||||
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
async remove(release: HelmRelease) {
|
|
||||||
return super.removeItem(release, () => deleteRelease(release.getName(), release.getNs()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeSelectedItems() {
|
|
||||||
return Promise.all(this.selectedItems.map(this.remove));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { asyncComputed } from "@ogre-tools/injectable-react";
|
||||||
|
import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable";
|
||||||
|
import { listReleases } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
|
||||||
|
const releasesInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const namespaceStore = di.inject(namespaceStoreInjectable);
|
||||||
|
|
||||||
|
// TODO: Inject clusterContext directly instead of accessing dependency of a dependency
|
||||||
|
const clusterContext = namespaceStore.context;
|
||||||
|
|
||||||
|
return asyncComputed(async () => {
|
||||||
|
const contextNamespaces = namespaceStore.contextNamespaces || [];
|
||||||
|
|
||||||
|
const isLoadingAll =
|
||||||
|
clusterContext.allNamespaces?.length > 1 &&
|
||||||
|
clusterContext.cluster.accessibleNamespaces.length === 0 &&
|
||||||
|
clusterContext.allNamespaces.every((namespace) =>
|
||||||
|
contextNamespaces.includes(namespace),
|
||||||
|
);
|
||||||
|
|
||||||
|
const releaseArrays = await (isLoadingAll ? listReleases() : Promise.all(
|
||||||
|
contextNamespaces.map((namespace) =>
|
||||||
|
listReleases(namespace),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
return releaseArrays.flat();
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default releasesInjectable;
|
||||||
@ -3,26 +3,30 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import "../item-object-list/item-list-layout.scss";
|
||||||
import "./releases.scss";
|
import "./releases.scss";
|
||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import kebabCase from "lodash/kebabCase";
|
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
|
||||||
import type { RouteComponentProps } from "react-router";
|
import type { RouteComponentProps } from "react-router";
|
||||||
import type { ReleaseStore } from "./release.store";
|
|
||||||
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
import { ReleaseDetails } from "./release-details";
|
|
||||||
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
|
||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
|
||||||
import { HelmReleaseMenu } from "./release-menu";
|
|
||||||
import { secretsStore } from "../+config-secrets/secrets.store";
|
|
||||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
|
||||||
import type { ReleaseRouteParams } from "../../../common/routes";
|
import type { ReleaseRouteParams } from "../../../common/routes";
|
||||||
import { releaseURL } from "../../../common/routes";
|
import { releaseURL } from "../../../common/routes";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import releaseStoreInjectable from "./release-store.injectable";
|
|
||||||
import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable";
|
import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable";
|
||||||
|
import { ItemListLayout } from "../item-object-list";
|
||||||
|
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||||
|
import { kebabCase } from "lodash/fp";
|
||||||
|
import { HelmReleaseMenu } from "./release-menu";
|
||||||
|
import type { ItemStore } from "../../../common/item.store";
|
||||||
|
import { ReleaseRollbackDialog } from "./release-rollback-dialog";
|
||||||
|
import { ReleaseDetails } from "./release-details/release-details";
|
||||||
|
import removableReleasesInjectable from "./removable-releases.injectable";
|
||||||
|
import type { RemovableHelmRelease } from "./removable-releases";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
import releasesInjectable from "./releases.injectable";
|
||||||
|
import { Spinner } from "../spinner";
|
||||||
|
|
||||||
enum columnId {
|
enum columnId {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -39,7 +43,8 @@ interface Props extends RouteComponentProps<ReleaseRouteParams> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
releaseStore: ReleaseStore
|
releases: IComputedValue<RemovableHelmRelease[]>
|
||||||
|
releasesArePending: IComputedValue<boolean>
|
||||||
selectNamespace: (namespace: string) => void
|
selectNamespace: (namespace: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,27 +56,10 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
|
|||||||
if (namespace) {
|
if (namespace) {
|
||||||
this.props.selectNamespace(namespace);
|
this.props.selectNamespace(namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
disposeOnUnmount(this, [
|
|
||||||
this.props.releaseStore.watchAssociatedSecrets(),
|
|
||||||
this.props.releaseStore.watchSelectedNamespaces(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectedRelease() {
|
|
||||||
const { match: { params: { name, namespace }}} = this.props;
|
|
||||||
|
|
||||||
return this.props.releaseStore.items.find(release => {
|
|
||||||
return release.getName() == name && release.getNs() == namespace;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onDetails = (item: HelmRelease) => {
|
onDetails = (item: HelmRelease) => {
|
||||||
if (item === this.selectedRelease) {
|
this.showDetails(item);
|
||||||
this.hideDetails();
|
|
||||||
} else {
|
|
||||||
this.showDetails(item);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
showDetails = (item: HelmRelease) => {
|
showDetails = (item: HelmRelease) => {
|
||||||
@ -101,14 +89,57 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.props.releasesArePending.get()) {
|
||||||
|
// TODO: Make Spinner "center" work properly
|
||||||
|
return <div className="flex center" style={{ height: "100%" }}><Spinner /></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const releases = this.props.releases;
|
||||||
|
|
||||||
|
// TODO: Implement ItemListLayout without stateful stores
|
||||||
|
const legacyReleaseStore = {
|
||||||
|
get items() {
|
||||||
|
return releases.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
loadAll: () => Promise.resolve(),
|
||||||
|
isLoaded: true,
|
||||||
|
failedLoading: false,
|
||||||
|
|
||||||
|
getTotalCount: () => releases.get().length,
|
||||||
|
|
||||||
|
toggleSelection: (item) => {
|
||||||
|
item.toggle();
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelectedAll: () =>
|
||||||
|
releases.get().every((release) => release.isSelected),
|
||||||
|
|
||||||
|
toggleSelectionAll: () => {
|
||||||
|
releases.get().forEach((release) => release.toggle());
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelected: (item) => item.isSelected,
|
||||||
|
|
||||||
|
get selectedItems() {
|
||||||
|
return releases.get().filter((release) => release.isSelected);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSelectedItems() {
|
||||||
|
return Promise.all(
|
||||||
|
releases.get().filter((release) => release.isSelected).map((release) => release.delete()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} as ItemStore<RemovableHelmRelease>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ItemListLayout
|
<ItemListLayout
|
||||||
|
store={legacyReleaseStore}
|
||||||
|
preloadStores={false}
|
||||||
isConfigurable
|
isConfigurable
|
||||||
tableId="helm_releases"
|
tableId="helm_releases"
|
||||||
className="HelmReleases"
|
className="HelmReleases"
|
||||||
store={this.props.releaseStore}
|
|
||||||
dependentStores={[secretsStore]}
|
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[columnId.name]: release => release.getName(),
|
[columnId.name]: release => release.getName(),
|
||||||
[columnId.namespace]: release => release.getNs(),
|
[columnId.namespace]: release => release.getNs(),
|
||||||
@ -167,13 +198,13 @@ class NonInjectedHelmReleases extends Component<Dependencies & Props> {
|
|||||||
customizeRemoveDialog={selectedItems => ({
|
customizeRemoveDialog={selectedItems => ({
|
||||||
message: this.renderRemoveDialogMessage(selectedItems),
|
message: this.renderRemoveDialogMessage(selectedItems),
|
||||||
})}
|
})}
|
||||||
detailsItem={this.selectedRelease}
|
|
||||||
onDetails={this.onDetails}
|
onDetails={this.onDetails}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ReleaseDetails
|
<ReleaseDetails
|
||||||
release={this.selectedRelease}
|
|
||||||
hideDetails={this.hideDetails}
|
hideDetails={this.hideDetails}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ReleaseRollbackDialog/>
|
<ReleaseRollbackDialog/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -185,7 +216,8 @@ export const HelmReleases = withInjectables<Dependencies, Props>(
|
|||||||
|
|
||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
releaseStore: di.inject(releaseStoreInjectable),
|
releases: di.inject(removableReleasesInjectable),
|
||||||
|
releasesArePending: di.inject(releasesInjectable).pending,
|
||||||
selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces,
|
selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces,
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import releasesInjectable from "./releases.injectable";
|
||||||
|
import deleteReleaseInjectable from "./delete-release/delete-release.injectable";
|
||||||
|
import { removableReleases } from "./removable-releases";
|
||||||
|
|
||||||
|
const removableReleasesInjectable = getInjectable({
|
||||||
|
instantiate: (di) =>
|
||||||
|
removableReleases({
|
||||||
|
releases: di.inject(releasesInjectable),
|
||||||
|
deleteRelease: di.inject(deleteReleaseInjectable),
|
||||||
|
releaseSelectionStatus: observable.map<string, boolean>(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default removableReleasesInjectable;
|
||||||
48
src/renderer/components/+apps-releases/removable-releases.ts
Normal file
48
src/renderer/components/+apps-releases/removable-releases.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
||||||
|
import { computed, ObservableMap } from "mobx";
|
||||||
|
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
releases: IAsyncComputed<HelmRelease[]>;
|
||||||
|
releaseSelectionStatus: ObservableMap<string, boolean>;
|
||||||
|
deleteRelease: (release: HelmRelease) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemovableHelmRelease extends HelmRelease {
|
||||||
|
toggle: () => void;
|
||||||
|
isSelected: boolean;
|
||||||
|
delete: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removableReleases = ({
|
||||||
|
releases,
|
||||||
|
releaseSelectionStatus,
|
||||||
|
deleteRelease,
|
||||||
|
}: Dependencies) => {
|
||||||
|
const isSelected = (release: HelmRelease) =>
|
||||||
|
releaseSelectionStatus.get(release.getId()) || false;
|
||||||
|
|
||||||
|
return computed(() =>
|
||||||
|
releases.value.get().map(
|
||||||
|
(release): RemovableHelmRelease => ({
|
||||||
|
...release,
|
||||||
|
|
||||||
|
toggle: () => {
|
||||||
|
releaseSelectionStatus.set(release.getId(), !isSelected(release));
|
||||||
|
},
|
||||||
|
|
||||||
|
get isSelected() {
|
||||||
|
return isSelected(release);
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async () => {
|
||||||
|
await deleteRelease(release);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { rollbackRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
import releasesInjectable from "../releases.injectable";
|
||||||
|
|
||||||
|
const rollbackReleaseInjectable = getInjectable({
|
||||||
|
instantiate: (di) => {
|
||||||
|
const releases = di.inject(releasesInjectable);
|
||||||
|
|
||||||
|
return async (name: string, namespace: string, revision: number) => {
|
||||||
|
await rollbackRelease(name, namespace, revision);
|
||||||
|
|
||||||
|
releases.invalidate();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rollbackReleaseInjectable;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { updateRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
|
|
||||||
|
const updateReleaseInjectable = getInjectable({
|
||||||
|
instantiate: () => updateRelease,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updateReleaseInjectable;
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import type React from "react";
|
||||||
|
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||||
|
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
|
import type { CustomCategoryViewRegistration } from "../custom-views";
|
||||||
|
import customCategoryViewsInjectable from "../custom-views.injectable";
|
||||||
|
|
||||||
|
describe("Custom Category Views", () => {
|
||||||
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = getDiForUnitTesting();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should order items correctly over all extensions", () => {
|
||||||
|
const component1 = (): React.ReactElement => null;
|
||||||
|
const component2 = (): React.ReactElement => null;
|
||||||
|
|
||||||
|
di.override(rendererExtensionsInjectable, () => computed(() => [
|
||||||
|
{
|
||||||
|
customCategoryViews: [
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
View: component1,
|
||||||
|
},
|
||||||
|
group: "foo",
|
||||||
|
kind: "bar",
|
||||||
|
priority: 100,
|
||||||
|
} as CustomCategoryViewRegistration,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customCategoryViews: [
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
View: component2,
|
||||||
|
},
|
||||||
|
group: "foo",
|
||||||
|
kind: "bar",
|
||||||
|
priority: 95,
|
||||||
|
} as CustomCategoryViewRegistration,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as LensRendererExtension[]));
|
||||||
|
|
||||||
|
const customCategoryViews = di.inject(customCategoryViewsInjectable);
|
||||||
|
const { after } = customCategoryViews.get().get("foo").get("bar");
|
||||||
|
|
||||||
|
expect(after[0].View).toBe(component2);
|
||||||
|
expect(after[1].View).toBe(component1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should put put priority < 50 items in before", () => {
|
||||||
|
const component1 = (): React.ReactElement => null;
|
||||||
|
const component2 = (): React.ReactElement => null;
|
||||||
|
|
||||||
|
di.override(rendererExtensionsInjectable, () => computed(() => [
|
||||||
|
{
|
||||||
|
customCategoryViews: [
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
View: component1,
|
||||||
|
},
|
||||||
|
group: "foo",
|
||||||
|
kind: "bar",
|
||||||
|
priority: 40,
|
||||||
|
} as CustomCategoryViewRegistration,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customCategoryViews: [
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
View: component2,
|
||||||
|
},
|
||||||
|
group: "foo",
|
||||||
|
kind: "bar",
|
||||||
|
priority: 95,
|
||||||
|
} as CustomCategoryViewRegistration,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as LensRendererExtension[]));
|
||||||
|
|
||||||
|
const customCategoryViews = di.inject(customCategoryViewsInjectable);
|
||||||
|
const { before } = customCategoryViews.get().get("foo").get("bar");
|
||||||
|
|
||||||
|
expect(before[0].View).toBe(component1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -8,7 +8,7 @@ import styles from "./catalog.module.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { ItemListLayout } from "../item-object-list";
|
import { ItemListLayout } from "../item-object-list";
|
||||||
import { action, makeObservable, observable, reaction, runInAction, when } from "mobx";
|
import { action, IComputedValue, makeObservable, observable, reaction, runInAction, when } from "mobx";
|
||||||
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
|
import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { MenuItem, MenuActions } from "../menu";
|
import { MenuItem, MenuActions } from "../menu";
|
||||||
@ -33,6 +33,9 @@ import catalogPreviousActiveTabStorageInjectable from "./catalog-previous-active
|
|||||||
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable";
|
||||||
import type { GetCategoryColumnsParams, CategoryColumns } from "./get-category-columns.injectable";
|
import type { GetCategoryColumnsParams, CategoryColumns } from "./get-category-columns.injectable";
|
||||||
import getCategoryColumnsInjectable from "./get-category-columns.injectable";
|
import getCategoryColumnsInjectable from "./get-category-columns.injectable";
|
||||||
|
import type { RegisteredCustomCategoryViewDecl } from "./custom-views.injectable";
|
||||||
|
import customCategoryViewsInjectable from "./custom-views.injectable";
|
||||||
|
import type { CustomCategoryViewComponents } from "./custom-views";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<CatalogViewRouteParam> {}
|
interface Props extends RouteComponentProps<CatalogViewRouteParam> {}
|
||||||
|
|
||||||
@ -40,6 +43,7 @@ interface Dependencies {
|
|||||||
catalogPreviousActiveTabStorage: { set: (value: string ) => void };
|
catalogPreviousActiveTabStorage: { set: (value: string ) => void };
|
||||||
catalogEntityStore: CatalogEntityStore;
|
catalogEntityStore: CatalogEntityStore;
|
||||||
getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
getCategoryColumns: (params: GetCategoryColumnsParams) => CategoryColumns;
|
||||||
|
customCategoryViews: IComputedValue<Map<string, Map<string, RegisteredCustomCategoryViewDecl>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -213,16 +217,44 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderViews = () => {
|
||||||
|
const { catalogEntityStore, customCategoryViews } = this.props;
|
||||||
|
const { activeCategory } = catalogEntityStore;
|
||||||
|
|
||||||
|
if (!activeCategory) {
|
||||||
|
return this.renderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
const customViews = customCategoryViews.get()
|
||||||
|
.get(activeCategory.spec.group)
|
||||||
|
?.get(activeCategory.spec.names.kind);
|
||||||
|
const renderView = ({ View }: CustomCategoryViewComponents, index: number) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
category={activeCategory}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{customViews?.before.map(renderView)}
|
||||||
|
{this.renderList()}
|
||||||
|
{customViews?.after.map(renderView)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderList() {
|
renderList() {
|
||||||
const { activeCategory } = this.props.catalogEntityStore;
|
const { catalogEntityStore, getCategoryColumns } = this.props;
|
||||||
const tableId = activeCategory ? `catalog-items-${activeCategory.metadata.name.replace(" ", "")}` : "catalog-items";
|
const { activeCategory } = catalogEntityStore;
|
||||||
|
const tableId = activeCategory
|
||||||
|
? `catalog-items-${activeCategory.metadata.name.replace(" ", "")}`
|
||||||
|
: "catalog-items";
|
||||||
|
|
||||||
if (this.activeTab === undefined) {
|
if (this.activeTab === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sortingCallbacks, searchFilters, renderTableContents, renderTableHeader } = this.props.getCategoryColumns({ activeCategory });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemListLayout
|
<ItemListLayout
|
||||||
className={styles.Catalog}
|
className={styles.Catalog}
|
||||||
@ -230,14 +262,11 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
|||||||
renderHeaderTitle={activeCategory?.metadata.name ?? "Browse All"}
|
renderHeaderTitle={activeCategory?.metadata.name ?? "Browse All"}
|
||||||
isSelectable={false}
|
isSelectable={false}
|
||||||
isConfigurable={true}
|
isConfigurable={true}
|
||||||
store={this.props.catalogEntityStore}
|
store={catalogEntityStore}
|
||||||
sortingCallbacks={sortingCallbacks}
|
|
||||||
searchFilters={searchFilters}
|
|
||||||
renderTableHeader={renderTableHeader}
|
|
||||||
customizeTableRowProps={entity => ({
|
customizeTableRowProps={entity => ({
|
||||||
disabled: !entity.isEnabled(),
|
disabled: !entity.isEnabled(),
|
||||||
})}
|
})}
|
||||||
renderTableContents={renderTableContents}
|
{...getCategoryColumns({ activeCategory })}
|
||||||
onDetails={this.onDetails}
|
onDetails={this.onDetails}
|
||||||
renderItemMenu={this.renderItemMenu}
|
renderItemMenu={this.renderItemMenu}
|
||||||
/>
|
/>
|
||||||
@ -254,7 +283,7 @@ class NonInjectedCatalog extends React.Component<Props & Dependencies> {
|
|||||||
return (
|
return (
|
||||||
<MainLayout sidebar={this.renderNavigation()}>
|
<MainLayout sidebar={this.renderNavigation()}>
|
||||||
<div className="p-6 h-full">
|
<div className="p-6 h-full">
|
||||||
{this.renderList()}
|
{this.renderViews()}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
selectedEntity
|
selectedEntity
|
||||||
@ -281,6 +310,7 @@ export const Catalog = withInjectables<Dependencies, Props>( NonInjectedCatalog,
|
|||||||
catalogEntityStore: di.inject(catalogEntityStoreInjectable),
|
catalogEntityStore: di.inject(catalogEntityStoreInjectable),
|
||||||
catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable),
|
catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable),
|
||||||
getCategoryColumns: di.inject(getCategoryColumnsInjectable),
|
getCategoryColumns: di.inject(getCategoryColumnsInjectable),
|
||||||
|
customCategoryViews: di.inject(customCategoryViewsInjectable),
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|||||||
import { computed, IComputedValue } from "mobx";
|
import { computed, IComputedValue } from "mobx";
|
||||||
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||||
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
||||||
import { getOrInsert } from "../../utils";
|
import { getOrInsert, getOrInsertMap } from "../../utils";
|
||||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
@ -19,7 +19,7 @@ function getAdditionCategoryColumns({ extensions }: Dependencies): IComputedValu
|
|||||||
|
|
||||||
for (const ext of extensions.get()) {
|
for (const ext of extensions.get()) {
|
||||||
for (const { renderCell, titleProps, priority = 50, searchFilter, sortCallback, ...registration } of ext.additionalCategoryColumns) {
|
for (const { renderCell, titleProps, priority = 50, searchFilter, sortCallback, ...registration } of ext.additionalCategoryColumns) {
|
||||||
const byGroup = getOrInsert(res, registration.group, new Map<string, RegisteredAdditionalCategoryColumn[]>());
|
const byGroup = getOrInsertMap(res, registration.group);
|
||||||
const byKind = getOrInsert(byGroup, registration.kind, []);
|
const byKind = getOrInsert(byGroup, registration.kind, []);
|
||||||
const id = `${ext.name}:${registration.id}`;
|
const id = `${ext.name}:${registration.id}`;
|
||||||
|
|
||||||
|
|||||||
58
src/renderer/components/+catalog/custom-views.injectable.ts
Normal file
58
src/renderer/components/+catalog/custom-views.injectable.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { orderBy } from "lodash";
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
|
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
|
||||||
|
import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { getOrInsert, getOrInsertMap } from "../../utils";
|
||||||
|
import type { CustomCategoryViewComponents } from "./custom-views";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensions: IComputedValue<LensRendererExtension[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisteredCustomCategoryViewDecl {
|
||||||
|
/**
|
||||||
|
* The asc sorted list of items with priority set to < 50
|
||||||
|
*/
|
||||||
|
before: CustomCategoryViewComponents[];
|
||||||
|
/**
|
||||||
|
* The asc sorted list of items with priority not set or set to >= 50
|
||||||
|
*/
|
||||||
|
after: CustomCategoryViewComponents[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCustomCategoryViews({ extensions }: Dependencies): IComputedValue<Map<string, Map<string, RegisteredCustomCategoryViewDecl>>> {
|
||||||
|
return computed(() => {
|
||||||
|
const res = new Map<string, Map<string, RegisteredCustomCategoryViewDecl>>();
|
||||||
|
const registrations = extensions.get()
|
||||||
|
.flatMap(ext => ext.customCategoryViews)
|
||||||
|
.map(({ priority = 50, ...rest }) => ({ priority, ...rest }));
|
||||||
|
const sortedRegistrations = orderBy(registrations, "priority", "asc");
|
||||||
|
|
||||||
|
for (const { priority, group, kind, components } of sortedRegistrations) {
|
||||||
|
const byGroup = getOrInsertMap(res, group);
|
||||||
|
const { before, after } = getOrInsert(byGroup, kind, { before: [], after: [] });
|
||||||
|
|
||||||
|
if (priority < 50) {
|
||||||
|
before.push(components);
|
||||||
|
} else {
|
||||||
|
after.push(components);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const customCategoryViewsInjectable = getInjectable({
|
||||||
|
instantiate: (di) => getCustomCategoryViews({
|
||||||
|
extensions: di.inject(rendererExtensionsInjectable),
|
||||||
|
}),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default customCategoryViewsInjectable;
|
||||||
57
src/renderer/components/+catalog/custom-views.ts
Normal file
57
src/renderer/components/+catalog/custom-views.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from "react";
|
||||||
|
import type { CatalogCategory } from "../../api/catalog-entity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The props for CustomCategoryViewComponents.View
|
||||||
|
*/
|
||||||
|
export interface CustomCategoryViewProps {
|
||||||
|
/**
|
||||||
|
* The category instance itself
|
||||||
|
*/
|
||||||
|
category: CatalogCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The components for the category view.
|
||||||
|
*/
|
||||||
|
export interface CustomCategoryViewComponents {
|
||||||
|
View: React.ComponentType<CustomCategoryViewProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the type used to declare additional views for a specific category
|
||||||
|
*/
|
||||||
|
export interface CustomCategoryViewRegistration {
|
||||||
|
/**
|
||||||
|
* The catalog entity kind that is declared by the category for this registration
|
||||||
|
*
|
||||||
|
* e.g.
|
||||||
|
* - `"KubernetesCluster"`
|
||||||
|
*/
|
||||||
|
kind: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The catalog entity group that is declared by the category for this registration
|
||||||
|
*
|
||||||
|
* e.g.
|
||||||
|
* - `"entity.k8slens.dev"`
|
||||||
|
*/
|
||||||
|
group: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting order value. Used to determine the total order of the views.
|
||||||
|
*
|
||||||
|
* @default 50
|
||||||
|
*/
|
||||||
|
priority?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The components for this registration
|
||||||
|
*/
|
||||||
|
components: CustomCategoryViewComponents;
|
||||||
|
}
|
||||||
@ -10,7 +10,6 @@ import { Select, SelectOption } from "../select";
|
|||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { isWindows } from "../../../common/vars";
|
|
||||||
import { Switch } from "../switch";
|
import { Switch } from "../switch";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
|
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
|
||||||
@ -36,16 +35,7 @@ interface Dependencies {
|
|||||||
|
|
||||||
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems }) => {
|
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems }) => {
|
||||||
const userStore = UserStore.getInstance();
|
const userStore = UserStore.getInstance();
|
||||||
const defaultShell = process.env.SHELL
|
|
||||||
|| process.env.PTYSHELL
|
|
||||||
|| (
|
|
||||||
isWindows
|
|
||||||
? "powershell.exe"
|
|
||||||
: "System default shell"
|
|
||||||
);
|
|
||||||
|
|
||||||
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
|
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
|
||||||
const [shell, setShell] = React.useState(userStore.shell || "");
|
|
||||||
const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application");
|
const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application");
|
||||||
const themeStore = ThemeStore.getInstance();
|
const themeStore = ThemeStore.getInstance();
|
||||||
|
|
||||||
@ -62,43 +52,7 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems })
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr />
|
<hr/>
|
||||||
|
|
||||||
<section id="terminalTheme">
|
|
||||||
<SubTitle title="Terminal theme" />
|
|
||||||
<Select
|
|
||||||
themeName="lens"
|
|
||||||
options={[
|
|
||||||
{ label: "Match theme", value: "" },
|
|
||||||
...themeStore.themeOptions,
|
|
||||||
]}
|
|
||||||
value={userStore.terminalTheme}
|
|
||||||
onChange={({ value }) => userStore.terminalTheme = value}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="shell">
|
|
||||||
<SubTitle title="Terminal Shell Path" />
|
|
||||||
<Input
|
|
||||||
theme="round-black"
|
|
||||||
placeholder={defaultShell}
|
|
||||||
value={shell}
|
|
||||||
onChange={setShell}
|
|
||||||
onBlur={() => userStore.shell = shell}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="terminalSelection">
|
|
||||||
<SubTitle title="Terminal copy & paste" />
|
|
||||||
<Switch
|
|
||||||
checked={userStore.terminalCopyOnSelect}
|
|
||||||
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
|
|
||||||
>
|
|
||||||
Copy on select and paste on right-click
|
|
||||||
</Switch>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<section id="extensionRegistryUrl">
|
<section id="extensionRegistryUrl">
|
||||||
<SubTitle title="Extension Install Registry" />
|
<SubTitle title="Extension Install Registry" />
|
||||||
|
|||||||
@ -69,6 +69,27 @@ export const Editor = observer(() => {
|
|||||||
onChange={value => editorConfiguration.tabSize = Number(value)}
|
onChange={value => editorConfiguration.tabSize = Number(value)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<SubTitle title="Font size"/>
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
type="number"
|
||||||
|
min={10}
|
||||||
|
validators={InputValidators.isNumber}
|
||||||
|
value={editorConfiguration.fontSize.toString()}
|
||||||
|
onChange={value => editorConfiguration.fontSize = Number(value)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<SubTitle title="Font family"/>
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
type="text"
|
||||||
|
validators={InputValidators.isNumber}
|
||||||
|
value={editorConfiguration.fontFamily}
|
||||||
|
onChange={value => editorConfiguration.fontFamily = value}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,6 +22,8 @@ import {
|
|||||||
editorRoute,
|
editorRoute,
|
||||||
telemetryRoute,
|
telemetryRoute,
|
||||||
telemetryURL,
|
telemetryURL,
|
||||||
|
terminalRoute,
|
||||||
|
terminalURL,
|
||||||
} from "../../../common/routes";
|
} from "../../../common/routes";
|
||||||
import { navigateWithoutHistoryChange, navigation } from "../../navigation";
|
import { navigateWithoutHistoryChange, navigation } from "../../navigation";
|
||||||
import { SettingLayout } from "../layout/setting-layout";
|
import { SettingLayout } from "../layout/setting-layout";
|
||||||
@ -29,6 +31,7 @@ import { Tab, Tabs } from "../tabs";
|
|||||||
import { Application } from "./application";
|
import { Application } from "./application";
|
||||||
import { Kubernetes } from "./kubernetes";
|
import { Kubernetes } from "./kubernetes";
|
||||||
import { Editor } from "./editor";
|
import { Editor } from "./editor";
|
||||||
|
import { Terminal } from "./terminal";
|
||||||
import { LensProxy } from "./proxy";
|
import { LensProxy } from "./proxy";
|
||||||
import { Telemetry } from "./telemetry";
|
import { Telemetry } from "./telemetry";
|
||||||
import { Extensions } from "./extensions";
|
import { Extensions } from "./extensions";
|
||||||
@ -56,6 +59,7 @@ const NonInjectedPreferences: React.FC<Dependencies> = ({ appPreferenceItems })
|
|||||||
<Tab value={proxyURL()} label="Proxy" data-testid="proxy-tab" active={isActive(proxyRoute)}/>
|
<Tab value={proxyURL()} label="Proxy" data-testid="proxy-tab" active={isActive(proxyRoute)}/>
|
||||||
<Tab value={kubernetesURL()} label="Kubernetes" data-testid="kubernetes-tab" active={isActive(kubernetesRoute)}/>
|
<Tab value={kubernetesURL()} label="Kubernetes" data-testid="kubernetes-tab" active={isActive(kubernetesRoute)}/>
|
||||||
<Tab value={editorURL()} label="Editor" data-testid="editor-tab" active={isActive(editorRoute)}/>
|
<Tab value={editorURL()} label="Editor" data-testid="editor-tab" active={isActive(editorRoute)}/>
|
||||||
|
<Tab value={terminalURL()} label="Terminal" data-testid="terminal-tab" active={isActive(terminalRoute)}/>
|
||||||
{(telemetryExtensions.length > 0 || !!sentryDsn) &&
|
{(telemetryExtensions.length > 0 || !!sentryDsn) &&
|
||||||
<Tab value={telemetryURL()} label="Telemetry" data-testid="telemetry-tab" active={isActive(telemetryRoute)}/>
|
<Tab value={telemetryURL()} label="Telemetry" data-testid="telemetry-tab" active={isActive(telemetryRoute)}/>
|
||||||
}
|
}
|
||||||
@ -77,6 +81,7 @@ const NonInjectedPreferences: React.FC<Dependencies> = ({ appPreferenceItems })
|
|||||||
<Route path={proxyURL()} component={LensProxy}/>
|
<Route path={proxyURL()} component={LensProxy}/>
|
||||||
<Route path={kubernetesURL()} component={Kubernetes}/>
|
<Route path={kubernetesURL()} component={Kubernetes}/>
|
||||||
<Route path={editorURL()} component={Editor}/>
|
<Route path={editorURL()} component={Editor}/>
|
||||||
|
<Route path={terminalURL()} component={Terminal}/>
|
||||||
<Route path={telemetryURL()} component={Telemetry}/>
|
<Route path={telemetryURL()} component={Telemetry}/>
|
||||||
<Route path={extensionURL()} component={Extensions}/>
|
<Route path={extensionURL()} component={Extensions}/>
|
||||||
<Redirect exact from={`${preferencesURL()}/`} to={appURL()}/>
|
<Redirect exact from={`${preferencesURL()}/`} to={appURL()}/>
|
||||||
|
|||||||
82
src/renderer/components/+preferences/terminal.tsx
Normal file
82
src/renderer/components/+preferences/terminal.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { UserStore } from "../../../common/user-store";
|
||||||
|
import { SubTitle } from "../layout/sub-title";
|
||||||
|
import { Input, InputValidators } from "../input";
|
||||||
|
import { isWindows } from "../../../common/vars";
|
||||||
|
import { Switch } from "../switch";
|
||||||
|
import { Select } from "../select";
|
||||||
|
import { ThemeStore } from "../../theme.store";
|
||||||
|
|
||||||
|
export const Terminal = observer(() => {
|
||||||
|
const userStore = UserStore.getInstance();
|
||||||
|
const themeStore = ThemeStore.getInstance();
|
||||||
|
const defaultShell = process.env.SHELL
|
||||||
|
|| process.env.PTYSHELL
|
||||||
|
|| (
|
||||||
|
isWindows
|
||||||
|
? "powershell.exe"
|
||||||
|
: "System default shell"
|
||||||
|
);
|
||||||
|
|
||||||
|
return (<div>
|
||||||
|
<section id="shell">
|
||||||
|
<SubTitle title="Terminal Shell Path"/>
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
placeholder={defaultShell}
|
||||||
|
value={userStore.shell}
|
||||||
|
onChange={(value) => userStore.shell = value}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="terminalSelection">
|
||||||
|
<SubTitle title="Terminal copy & paste" />
|
||||||
|
<Switch
|
||||||
|
checked={userStore.terminalCopyOnSelect}
|
||||||
|
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
|
||||||
|
>
|
||||||
|
Copy on select and paste on right-click
|
||||||
|
</Switch>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="terminalTheme">
|
||||||
|
<SubTitle title="Terminal theme" />
|
||||||
|
<Select
|
||||||
|
themeName="lens"
|
||||||
|
options={[
|
||||||
|
{ label: "Match theme", value: "" },
|
||||||
|
...themeStore.themeOptions,
|
||||||
|
]}
|
||||||
|
value={userStore.terminalTheme}
|
||||||
|
onChange={({ value }) => userStore.terminalTheme = value}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<SubTitle title="Font size"/>
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
type="number"
|
||||||
|
min={10}
|
||||||
|
validators={InputValidators.isNumber}
|
||||||
|
value={userStore.terminalConfig.fontSize.toString()}
|
||||||
|
onChange={(value) => userStore.terminalConfig.fontSize=Number(value)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<SubTitle title="Font family"/>
|
||||||
|
<Input
|
||||||
|
theme="round-black"
|
||||||
|
type="text"
|
||||||
|
value={userStore.terminalConfig.fontFamily}
|
||||||
|
onChange={(value) => userStore.terminalConfig.fontFamily=value}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>);
|
||||||
|
});
|
||||||
@ -27,10 +27,10 @@ import type {
|
|||||||
IReleaseCreatePayload,
|
IReleaseCreatePayload,
|
||||||
IReleaseUpdateDetails,
|
IReleaseUpdateDetails,
|
||||||
} from "../../../common/k8s-api/endpoints/helm-releases.api";
|
} from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
import releaseStoreInjectable from "../+apps-releases/release-store.injectable";
|
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import installChartStoreInjectable from "./install-chart-store/install-chart-store.injectable";
|
import installChartStoreInjectable from "./install-chart-store/install-chart-store.injectable";
|
||||||
import dockStoreInjectable from "./dock-store/dock-store.injectable";
|
import dockStoreInjectable from "./dock-store/dock-store.injectable";
|
||||||
|
import createReleaseInjectable from "../+apps-releases/create-release/create-release.injectable";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tab: DockTab;
|
tab: DockTab;
|
||||||
@ -222,7 +222,7 @@ export const InstallChart = withInjectables<Dependencies, Props>(
|
|||||||
|
|
||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
createRelease: di.inject(releaseStoreInjectable).create,
|
createRelease: di.inject(createReleaseInjectable),
|
||||||
installChartStore: di.inject(installChartStoreInjectable),
|
installChartStore: di.inject(installChartStoreInjectable),
|
||||||
dockStore: di.inject(dockStoreInjectable),
|
dockStore: di.inject(dockStoreInjectable),
|
||||||
...props,
|
...props,
|
||||||
|
|||||||
@ -11,24 +11,28 @@ import type { DockStore, TabId } from "../dock-store/dock.store";
|
|||||||
import { TerminalApi, TerminalChannels } from "../../../api/terminal-api";
|
import { TerminalApi, TerminalChannels } from "../../../api/terminal-api";
|
||||||
import { ThemeStore } from "../../../theme.store";
|
import { ThemeStore } from "../../../theme.store";
|
||||||
import { disposer } from "../../../utils";
|
import { disposer } from "../../../utils";
|
||||||
import { isMac } from "../../../../common/vars";
|
import { isMac, defaultTerminalFontFamily } from "../../../../common/vars";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
import { UserStore } from "../../../../common/user-store";
|
import { UserStore } from "../../../../common/user-store";
|
||||||
import { clipboard } from "electron";
|
import { clipboard } from "electron";
|
||||||
import logger from "../../../../common/logger";
|
import logger from "../../../../common/logger";
|
||||||
|
import type { TerminalConfig } from "../../../../common/user-store/preferences-helpers";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
dockStore: DockStore
|
dockStore: DockStore
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Terminal {
|
export class Terminal {
|
||||||
|
|
||||||
|
private terminalConfig: TerminalConfig = UserStore.getInstance().terminalConfig;
|
||||||
|
|
||||||
public static get spawningPool() {
|
public static get spawningPool() {
|
||||||
return document.getElementById("terminal-init");
|
return document.getElementById("terminal-init");
|
||||||
}
|
}
|
||||||
|
|
||||||
static async preloadFonts() {
|
static async preloadFonts() {
|
||||||
const fontPath = require("../../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
|
const fontPath = require("../../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
|
||||||
const fontFace = new FontFace("RobotoMono", `url(${fontPath})`);
|
const fontFace = new FontFace(defaultTerminalFontFamily, `url(${fontPath})`);
|
||||||
|
|
||||||
await fontFace.load();
|
await fontFace.load();
|
||||||
document.fonts.add(fontFace);
|
document.fonts.add(fontFace);
|
||||||
@ -37,8 +41,8 @@ export class Terminal {
|
|||||||
private xterm: XTerm | null = new XTerm({
|
private xterm: XTerm | null = new XTerm({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "bar",
|
cursorStyle: "bar",
|
||||||
fontSize: 13,
|
fontSize: this.terminalConfig.fontSize,
|
||||||
fontFamily: "RobotoMono",
|
fontFamily: this.terminalConfig.fontFamily,
|
||||||
});
|
});
|
||||||
private readonly fitAddon = new FitAddon();
|
private readonly fitAddon = new FitAddon();
|
||||||
private scrollPos = 0;
|
private scrollPos = 0;
|
||||||
@ -97,6 +101,12 @@ export class Terminal {
|
|||||||
}, {
|
}, {
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
}),
|
}),
|
||||||
|
reaction(() => UserStore.getInstance().terminalConfig.fontSize, this.setFontSize, {
|
||||||
|
fireImmediately: true,
|
||||||
|
}),
|
||||||
|
reaction(() => UserStore.getInstance().terminalConfig.fontFamily, this.setFontFamily, {
|
||||||
|
fireImmediately: true,
|
||||||
|
}),
|
||||||
dependencies.dockStore.onResize(this.onResize),
|
dependencies.dockStore.onResize(this.onResize),
|
||||||
() => onDataHandler.dispose(),
|
() => onDataHandler.dispose(),
|
||||||
() => this.fitAddon.dispose(),
|
() => this.fitAddon.dispose(),
|
||||||
@ -192,6 +202,14 @@ export class Terminal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setFontSize = (size: number) => {
|
||||||
|
this.xterm.options.fontSize = size;
|
||||||
|
};
|
||||||
|
|
||||||
|
setFontFamily = (family: string) => {
|
||||||
|
this.xterm.options.fontFamily = family;
|
||||||
|
};
|
||||||
|
|
||||||
keyHandler = (evt: KeyboardEvent): boolean => {
|
keyHandler = (evt: KeyboardEvent): boolean => {
|
||||||
const { code, ctrlKey, metaKey } = evt;
|
const { code, ctrlKey, metaKey } = evt;
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { UpgradeChartStore } from "./upgrade-chart.store";
|
import { UpgradeChartStore } from "./upgrade-chart.store";
|
||||||
import releaseStoreInjectable from "../../+apps-releases/release-store.injectable";
|
|
||||||
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
import dockStoreInjectable from "../dock-store/dock-store.injectable";
|
||||||
import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable";
|
import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable";
|
||||||
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||||
|
import releasesInjectable from "../../+apps-releases/releases.injectable";
|
||||||
|
|
||||||
const upgradeChartStoreInjectable = getInjectable({
|
const upgradeChartStoreInjectable = getInjectable({
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
@ -16,7 +16,7 @@ const upgradeChartStoreInjectable = getInjectable({
|
|||||||
const valuesStore = createDockTabStore<string>();
|
const valuesStore = createDockTabStore<string>();
|
||||||
|
|
||||||
return new UpgradeChartStore({
|
return new UpgradeChartStore({
|
||||||
releaseStore: di.inject(releaseStoreInjectable),
|
releases: di.inject(releasesInjectable),
|
||||||
dockStore: di.inject(dockStoreInjectable),
|
dockStore: di.inject(dockStoreInjectable),
|
||||||
createStorage: di.inject(createStorageInjectable),
|
createStorage: di.inject(createStorageInjectable),
|
||||||
valuesStore,
|
valuesStore,
|
||||||
|
|||||||
@ -3,12 +3,22 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, autorun, computed, IReactionDisposer, reaction, makeObservable } from "mobx";
|
import {
|
||||||
|
action,
|
||||||
|
autorun,
|
||||||
|
computed,
|
||||||
|
IReactionDisposer,
|
||||||
|
reaction,
|
||||||
|
makeObservable,
|
||||||
|
} from "mobx";
|
||||||
import { DockStore, DockTab, TabId, TabKind } from "../dock-store/dock.store";
|
import { DockStore, DockTab, TabId, TabKind } from "../dock-store/dock.store";
|
||||||
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
|
import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store";
|
||||||
import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
import {
|
||||||
import type { ReleaseStore } from "../../+apps-releases/release.store";
|
getReleaseValues,
|
||||||
|
HelmRelease,
|
||||||
|
} from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
import { iter, StorageHelper } from "../../../utils";
|
import { iter, StorageHelper } from "../../../utils";
|
||||||
|
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
||||||
|
|
||||||
export interface IChartUpgradeData {
|
export interface IChartUpgradeData {
|
||||||
releaseName: string;
|
releaseName: string;
|
||||||
@ -16,7 +26,7 @@ export interface IChartUpgradeData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
releaseStore: ReleaseStore
|
releases: IAsyncComputed<HelmRelease[]>
|
||||||
valuesStore: DockTabStore<string>
|
valuesStore: DockTabStore<string>
|
||||||
dockStore: DockStore
|
dockStore: DockStore
|
||||||
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
|
createStorage: <T>(storageKey: string, options: DockTabStorageState<T>) => StorageHelper<DockTabStorageState<T>>
|
||||||
@ -60,14 +70,14 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dispose = reaction(() => {
|
const dispose = reaction(() => {
|
||||||
const release = this.dependencies.releaseStore.getByName(releaseName);
|
const release = this.dependencies.releases.value.get().find(release => release.getName() === releaseName);
|
||||||
|
|
||||||
return release?.getRevision(); // watch changes only by revision
|
return release?.getRevision(); // watch changes only by revision
|
||||||
},
|
},
|
||||||
release => {
|
release => {
|
||||||
const releaseTab = this.getTabByRelease(releaseName);
|
const releaseTab = this.getTabByRelease(releaseName);
|
||||||
|
|
||||||
if (!this.dependencies.releaseStore.isLoaded || !releaseTab) {
|
if (!releaseTab) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +101,7 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
|||||||
isLoading(tabId = this.dependencies.dockStore.selectedTabId) {
|
isLoading(tabId = this.dependencies.dockStore.selectedTabId) {
|
||||||
const values = this.values.getData(tabId);
|
const values = this.values.getData(tabId);
|
||||||
|
|
||||||
return !this.dependencies.releaseStore.isLoaded || values === undefined;
|
return values === undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -99,7 +109,6 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
|||||||
const values = this.values.getData(tabId);
|
const values = this.values.getData(tabId);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
!this.dependencies.releaseStore.isLoaded && this.dependencies.releaseStore.loadFromContextNamespaces(),
|
|
||||||
!values && this.loadValues(tabId),
|
!values && this.loadValues(tabId),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,15 +13,22 @@ import type { DockTab } from "./dock-store/dock.store";
|
|||||||
import { InfoPanel } from "./info-panel";
|
import { InfoPanel } from "./info-panel";
|
||||||
import type { UpgradeChartStore } from "./upgrade-chart-store/upgrade-chart.store";
|
import type { UpgradeChartStore } from "./upgrade-chart-store/upgrade-chart.store";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import type { ReleaseStore } from "../+apps-releases/release.store";
|
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { EditorPanel } from "./editor-panel";
|
import { EditorPanel } from "./editor-panel";
|
||||||
import { helmChartStore, IChartVersion } from "../+apps-helm-charts/helm-chart.store";
|
import {
|
||||||
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
|
helmChartStore,
|
||||||
|
IChartVersion,
|
||||||
|
} from "../+apps-helm-charts/helm-chart.store";
|
||||||
|
import type {
|
||||||
|
HelmRelease,
|
||||||
|
IReleaseUpdateDetails,
|
||||||
|
IReleaseUpdatePayload,
|
||||||
|
} from "../../../common/k8s-api/endpoints/helm-releases.api";
|
||||||
import { Select, SelectOption } from "../select";
|
import { Select, SelectOption } from "../select";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { IAsyncComputed, withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import releaseStoreInjectable from "../+apps-releases/release-store.injectable";
|
|
||||||
import upgradeChartStoreInjectable from "./upgrade-chart-store/upgrade-chart-store.injectable";
|
import upgradeChartStoreInjectable from "./upgrade-chart-store/upgrade-chart-store.injectable";
|
||||||
|
import updateReleaseInjectable from "../+apps-releases/update-release/update-release.injectable";
|
||||||
|
import releasesInjectable from "../+apps-releases/releases.injectable";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -29,8 +36,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
releaseStore: ReleaseStore
|
releases: IAsyncComputed<HelmRelease[]>
|
||||||
upgradeChartStore: UpgradeChartStore
|
upgradeChartStore: UpgradeChartStore
|
||||||
|
updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -61,7 +69,7 @@ export class NonInjectedUpgradeChart extends React.Component<Props & Dependencie
|
|||||||
|
|
||||||
if (!tabData) return null;
|
if (!tabData) return null;
|
||||||
|
|
||||||
return this.props.releaseStore.getByName(tabData.releaseName);
|
return this.props.releases.value.get().find(release => release.getName() === tabData.releaseName);
|
||||||
}
|
}
|
||||||
|
|
||||||
get value() {
|
get value() {
|
||||||
@ -95,7 +103,7 @@ export class NonInjectedUpgradeChart extends React.Component<Props & Dependencie
|
|||||||
const releaseName = this.release.getName();
|
const releaseName = this.release.getName();
|
||||||
const releaseNs = this.release.getNs();
|
const releaseNs = this.release.getNs();
|
||||||
|
|
||||||
await this.props.releaseStore.update(releaseName, releaseNs, {
|
await this.props.updateRelease(releaseName, releaseNs, {
|
||||||
chart: this.release.getChart(),
|
chart: this.release.getChart(),
|
||||||
values: this.value,
|
values: this.value,
|
||||||
repo, version,
|
repo, version,
|
||||||
@ -167,7 +175,8 @@ export const UpgradeChart = withInjectables<Dependencies, Props>(
|
|||||||
|
|
||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
releaseStore: di.inject(releaseStoreInjectable),
|
releases: di.inject(releasesInjectable),
|
||||||
|
updateRelease: di.inject(updateReleaseInjectable),
|
||||||
upgradeChartStore: di.inject(upgradeChartStoreInjectable),
|
upgradeChartStore: di.inject(upgradeChartStoreInjectable),
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -11,12 +11,13 @@ import { createPortal } from "react-dom";
|
|||||||
import { cssNames, noop, StorageHelper } from "../../utils";
|
import { cssNames, noop, StorageHelper } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Animate, AnimateName } from "../animate";
|
import { Animate, AnimateName } from "../animate";
|
||||||
import { history } from "../../navigation";
|
|
||||||
import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor";
|
import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor";
|
||||||
import drawerStorageInjectable, {
|
import drawerStorageInjectable, {
|
||||||
defaultDrawerWidth,
|
defaultDrawerWidth,
|
||||||
} from "./drawer-storage/drawer-storage.injectable";
|
} from "./drawer-storage/drawer-storage.injectable";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import historyInjectable from "../../navigation/history.injectable";
|
||||||
|
import type { History } from "history";
|
||||||
|
|
||||||
export type DrawerPosition = "top" | "left" | "right" | "bottom";
|
export type DrawerPosition = "top" | "left" | "right" | "bottom";
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ resizingAnchorProps.set("top", [ResizeDirection.VERTICAL, ResizeSide.TRAILING, R
|
|||||||
resizingAnchorProps.set("bottom", [ResizeDirection.VERTICAL, ResizeSide.LEADING, ResizeGrowthDirection.BOTTOM_TO_TOP]);
|
resizingAnchorProps.set("bottom", [ResizeDirection.VERTICAL, ResizeSide.LEADING, ResizeGrowthDirection.BOTTOM_TO_TOP]);
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
|
history: History
|
||||||
drawerStorage: StorageHelper<{ width: number }>;
|
drawerStorage: StorageHelper<{ width: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +72,7 @@ class NonInjectedDrawer extends React.Component<DrawerProps & Dependencies, Stat
|
|||||||
private scrollElem: HTMLElement;
|
private scrollElem: HTMLElement;
|
||||||
private scrollPos = new Map<string, number>();
|
private scrollPos = new Map<string, number>();
|
||||||
|
|
||||||
private stopListenLocation = history.listen(() => {
|
private stopListenLocation = this.props.history.listen(() => {
|
||||||
this.restoreScrollPos();
|
this.restoreScrollPos();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -111,14 +113,14 @@ class NonInjectedDrawer extends React.Component<DrawerProps & Dependencies, Stat
|
|||||||
|
|
||||||
saveScrollPos = () => {
|
saveScrollPos = () => {
|
||||||
if (!this.scrollElem) return;
|
if (!this.scrollElem) return;
|
||||||
const key = history.location.key;
|
const key = this.props.history.location.key;
|
||||||
|
|
||||||
this.scrollPos.set(key, this.scrollElem.scrollTop);
|
this.scrollPos.set(key, this.scrollElem.scrollTop);
|
||||||
};
|
};
|
||||||
|
|
||||||
restoreScrollPos = () => {
|
restoreScrollPos = () => {
|
||||||
if (!this.scrollElem) return;
|
if (!this.scrollElem) return;
|
||||||
const key = history.location.key;
|
const key = this.props.history.location.key;
|
||||||
|
|
||||||
this.scrollElem.scrollTop = this.scrollPos.get(key) || 0;
|
this.scrollElem.scrollTop = this.scrollPos.get(key) || 0;
|
||||||
};
|
};
|
||||||
@ -232,6 +234,7 @@ export const Drawer = withInjectables<Dependencies, DrawerProps>(
|
|||||||
|
|
||||||
{
|
{
|
||||||
getProps: (di, props) => ({
|
getProps: (di, props) => ({
|
||||||
|
history: di.inject(historyInjectable),
|
||||||
drawerStorage: di.inject(drawerStorageInjectable),
|
drawerStorage: di.inject(drawerStorageInjectable),
|
||||||
...props,
|
...props,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import React from "react";
|
|||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Redirect, Route, Router, Switch } from "react-router";
|
import { Redirect, Route, Router, Switch } from "react-router";
|
||||||
import { history } from "../../navigation";
|
|
||||||
import { UserManagement } from "../../components/+user-management/user-management";
|
import { UserManagement } from "../../components/+user-management/user-management";
|
||||||
import { ConfirmDialog } from "../../components/confirm-dialog";
|
import { ConfirmDialog } from "../../components/confirm-dialog";
|
||||||
import { ClusterOverview } from "../../components/+cluster/cluster-overview";
|
import { ClusterOverview } from "../../components/+cluster/cluster-overview";
|
||||||
@ -41,17 +40,18 @@ import { PortForwardDialog } from "../../port-forward";
|
|||||||
import { DeleteClusterDialog } from "../../components/delete-cluster-dialog";
|
import { DeleteClusterDialog } from "../../components/delete-cluster-dialog";
|
||||||
import type { NamespaceStore } from "../../components/+namespaces/namespace-store/namespace.store";
|
import type { NamespaceStore } from "../../components/+namespaces/namespace-store/namespace.store";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import namespaceStoreInjectable
|
import namespaceStoreInjectable from "../../components/+namespaces/namespace-store/namespace-store.injectable";
|
||||||
from "../../components/+namespaces/namespace-store/namespace-store.injectable";
|
|
||||||
import type { ClusterId } from "../../../common/cluster-types";
|
import type { ClusterId } from "../../../common/cluster-types";
|
||||||
import hostedClusterInjectable
|
import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster/hosted-cluster.injectable";
|
||||||
from "../../../common/cluster-store/hosted-cluster/hosted-cluster.injectable";
|
|
||||||
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
import type { Disposer } from "../../../common/utils";
|
import type { Disposer } from "../../../common/utils";
|
||||||
import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable";
|
import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable";
|
||||||
|
import historyInjectable from "../../navigation/history.injectable";
|
||||||
|
import type { History } from "history";
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
|
history: History,
|
||||||
namespaceStore: NamespaceStore
|
namespaceStore: NamespaceStore
|
||||||
hostedClusterId: ClusterId
|
hostedClusterId: ClusterId
|
||||||
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
subscribeStores: (stores: KubeObjectStore<KubeObject>[]) => Disposer
|
||||||
@ -135,10 +135,11 @@ class NonInjectedClusterFrame extends React.Component<Dependencies> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Router history={history}>
|
<Router history={this.props.history}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MainLayout sidebar={<Sidebar />} footer={<Dock />}>
|
<MainLayout sidebar={<Sidebar />} footer={<Dock />}>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|
||||||
<Route component={ClusterOverview} {...routes.clusterRoute}/>
|
<Route component={ClusterOverview} {...routes.clusterRoute}/>
|
||||||
<Route component={Nodes} {...routes.nodesRoute}/>
|
<Route component={Nodes} {...routes.nodesRoute}/>
|
||||||
<Route component={Workloads} {...routes.workloadsRoute}/>
|
<Route component={Workloads} {...routes.workloadsRoute}/>
|
||||||
@ -181,6 +182,7 @@ class NonInjectedClusterFrame extends React.Component<Dependencies> {
|
|||||||
|
|
||||||
export const ClusterFrame = withInjectables<Dependencies>(NonInjectedClusterFrame, {
|
export const ClusterFrame = withInjectables<Dependencies>(NonInjectedClusterFrame, {
|
||||||
getProps: di => ({
|
getProps: di => ({
|
||||||
|
history: di.inject(historyInjectable),
|
||||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||||
hostedClusterId: di.inject(hostedClusterInjectable).id,
|
hostedClusterId: di.inject(hostedClusterInjectable).id,
|
||||||
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores,
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { injectSystemCAs } from "../../../common/system-ca";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Route, Router, Switch } from "react-router";
|
import { Route, Router, Switch } from "react-router";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { history } from "../../navigation";
|
|
||||||
import { ClusterManager } from "../../components/cluster-manager";
|
import { ClusterManager } from "../../components/cluster-manager";
|
||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
import { Notifications } from "../../components/notifications";
|
import { Notifications } from "../../components/notifications";
|
||||||
@ -16,14 +15,21 @@ import { CommandContainer } from "../../components/command-palette/command-conta
|
|||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
||||||
import { ClusterFrameHandler } from "../../components/cluster-manager/lens-views";
|
import { ClusterFrameHandler } from "../../components/cluster-manager/lens-views";
|
||||||
|
import historyInjectable from "../../navigation/history.injectable";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import type { History } from "history";
|
||||||
|
|
||||||
injectSystemCAs();
|
injectSystemCAs();
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
history: History
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class RootFrame extends React.Component {
|
class NonInjectedRootFrame extends React.Component<Dependencies> {
|
||||||
static displayName = "RootFrame";
|
static displayName = "RootFrame";
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: Dependencies) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
ClusterFrameHandler.createInstance();
|
ClusterFrameHandler.createInstance();
|
||||||
@ -35,7 +41,7 @@ export class RootFrame extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Router history={history}>
|
<Router history={this.props.history}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route component={ClusterManager} />
|
<Route component={ClusterManager} />
|
||||||
@ -48,3 +54,7 @@ export class RootFrame extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const RootFrame = withInjectables(NonInjectedRootFrame, {
|
||||||
|
getProps: (di) => ({ history: di.inject(historyInjectable) }),
|
||||||
|
});
|
||||||
|
|||||||
13
src/renderer/navigation/history.injectable.ts
Normal file
13
src/renderer/navigation/history.injectable.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { history } from "./history";
|
||||||
|
|
||||||
|
const historyInjectable = getInjectable({
|
||||||
|
instantiate: () => history,
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default historyInjectable;
|
||||||
@ -14,8 +14,14 @@ export const searchParamsOptions: ObservableSearchParamsOptions = {
|
|||||||
joinArraysWith: ",", // param values splitter, applicable only with {joinArrays:true}
|
joinArraysWith: ",", // param values splitter, applicable only with {joinArrays:true}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated: Switch to using di.inject(historyInjectable)
|
||||||
|
*/
|
||||||
export const history = ipcRenderer ? createBrowserHistory() : createMemoryHistory();
|
export const history = ipcRenderer ? createBrowserHistory() : createMemoryHistory();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated: Switch to using di.inject(observableHistoryInjectable)
|
||||||
|
*/
|
||||||
export const navigation = createObservableHistory(history, {
|
export const navigation = createObservableHistory(history, {
|
||||||
searchParams: searchParamsOptions,
|
searchParams: searchParamsOptions,
|
||||||
});
|
});
|
||||||
|
|||||||
15
src/renderer/navigation/observable-history.injectable.ts
Normal file
15
src/renderer/navigation/observable-history.injectable.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import { navigation as observableHistory } from "./history";
|
||||||
|
|
||||||
|
const observableHistoryInjectable = getInjectable({
|
||||||
|
instantiate: () => observableHistory,
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default observableHistoryInjectable;
|
||||||
@ -273,11 +273,9 @@ export class PortForwardStore extends ItemStore<PortForwardItem> {
|
|||||||
let response: PortForwardResult;
|
let response: PortForwardResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const protocol = pf.protocol ?? "http";
|
|
||||||
|
|
||||||
response = await apiBase.post<PortForwardResult>(
|
response = await apiBase.post<PortForwardResult>(
|
||||||
`/pods/port-forward/${pf.namespace}/${pf.kind}/${pf.name}`,
|
`/pods/port-forward/${pf.namespace}/${pf.kind}/${pf.name}`,
|
||||||
{ query: { port, forwardPort, protocol }},
|
{ query: { port, forwardPort }},
|
||||||
);
|
);
|
||||||
|
|
||||||
// expecting the received port to be the specified port, unless the specified port is 0, which indicates any available port is suitable
|
// expecting the received port to be the specified port, unless the specified port is 0, which indicates any available port is suitable
|
||||||
@ -355,11 +353,11 @@ function portForwardsEqual(portForward: ForwardedPort) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getActivePortForward(portForward: ForwardedPort): Promise<ForwardedPort> {
|
async function getActivePortForward(portForward: ForwardedPort): Promise<ForwardedPort> {
|
||||||
const { port, forwardPort, protocol } = portForward;
|
const { port, forwardPort } = portForward;
|
||||||
let response: PortForwardResult;
|
let response: PortForwardResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await apiBase.get<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort, protocol }});
|
response = await apiBase.get<PortForwardResult>(`/pods/port-forward/${portForward.namespace}/${portForward.kind}/${portForward.name}`, { query: { port, forwardPort }});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`[PORT-FORWARD-STORE] Error getting active port-forward: ${error}`, portForward);
|
logger.warn(`[PORT-FORWARD-STORE] Error getting active port-forward: ${error}`, portForward);
|
||||||
}
|
}
|
||||||
|
|||||||
18
yarn.lock
18
yarn.lock
@ -984,19 +984,19 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
|
|
||||||
"@ogre-tools/injectable-react@3.1.1":
|
"@ogre-tools/injectable-react@3.2.0":
|
||||||
version "3.1.1"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-3.1.1.tgz#d95ecec518ba798c36fa3a6f651fa52748e72b00"
|
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-3.2.0.tgz#468542e846952deb8e7a4f6757da4813ee8f11fa"
|
||||||
integrity sha512-Fhb/51NzrLzkA3G5zCpNOshvm0el1gROWGHkBqq1d/8PEekcEijIL8HZ6B/ylCWjQTJ1MaYViJdzs2iNP1oQxw==
|
integrity sha512-VU5l0uKe86psVzEPbXl1TLlflnoL+uSeOaOCy/mAGzau4nqRb+eA4RzYgzUs/D9tDYzJ7Es+LZWD9uSyXmpyXg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ogre-tools/fp" "^3.0.0"
|
"@ogre-tools/fp" "^3.0.0"
|
||||||
"@ogre-tools/injectable" "^3.1.1"
|
"@ogre-tools/injectable" "^3.2.0"
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
|
|
||||||
"@ogre-tools/injectable@3.1.1", "@ogre-tools/injectable@^3.1.1":
|
"@ogre-tools/injectable@3.2.0", "@ogre-tools/injectable@^3.2.0":
|
||||||
version "3.1.1"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-3.1.1.tgz#2f293a90e4d3f730ebab2689fd609edc24ffc563"
|
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-3.2.0.tgz#7d8f653cb3a2c0253a29422bcffd5123308600a9"
|
||||||
integrity sha512-X7cDU2Mkcl2bP8JtR9l/Hx31jmKYEuCVJGjZIYxWlE1Nvd3HGq98oTV5uEGNP6+GjLHhXjzoscT9SKKzexyQWg==
|
integrity sha512-aRlRdvLefJMBvFu1tRlTGNgpMbqE250lwMXT8Y6/ruC88rCL+TygCWcsJELad1OgqX1cfHkCgCYeQeohV+G3Zg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ogre-tools/fp" "^3.0.0"
|
"@ogre-tools/fp" "^3.0.0"
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user