mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Cherry-pick from 4.2.3 (#2628)
* Fix: logs data disapearing causing crashes (#2566) Signed-off-by: Sebastian Malton <sebastian@malton.name> * Refactor helm-chart.api and improve kube validation and error handling (#2265) Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix: HPA's not sortable by age (#2565) Signed-off-by: Sebastian Malton <sebastian@malton.name> * Conditionally render status icon for kube meta (#2298) Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix custom resource loading spinner appears above extensions' cluster menus (#2344) Signed-off-by: Sebastian Malton <sebastian@malton.name> * Lens should point to the release docs (#2268) Signed-off-by: Sebastian Malton <sebastian@malton.name> * Refactor the Extensions settings page (#2221) Signed-off-by: Sebastian Malton <sebastian@malton.name> * try and get jest to not core dump Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
7fde8125eb
commit
1f854d0a0f
42 files changed
+1777
-1067
No files matched your search
@@ -1,14 +1,13 @@
|
||||
import "./helm-chart-details.scss";
|
||||
|
||||
import React, { Component } from "react";
|
||||
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
||||
import { getChartDetails, HelmChart } from "../../api/endpoints/helm-charts.api";
|
||||
import { observable, autorun } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Drawer, DrawerItem } from "../drawer";
|
||||
import { autobind, stopPropagation } from "../../utils";
|
||||
import { MarkdownViewer } from "../markdown-viewer";
|
||||
import { Spinner } from "../spinner";
|
||||
import { CancelablePromise } from "../../utils/cancelableFetch";
|
||||
import { Button } from "../button";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { createInstallChartTab } from "../dock/install-chart.store";
|
||||
@@ -26,35 +25,37 @@ export class HelmChartDetails extends Component<Props> {
|
||||
@observable readme: string = null;
|
||||
@observable error: string = null;
|
||||
|
||||
private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>;
|
||||
private abortController?: AbortController;
|
||||
|
||||
componentWillUnmount() {
|
||||
this.chartPromise?.cancel();
|
||||
this.abortController?.abort();
|
||||
}
|
||||
|
||||
chartUpdater = autorun(() => {
|
||||
this.selectedChart = null;
|
||||
const { chart: { name, repo, version } } = this.props;
|
||||
|
||||
helmChartsApi.get(repo, name, version).then(result => {
|
||||
this.readme = result.readme;
|
||||
this.chartVersions = result.versions;
|
||||
this.selectedChart = result.versions[0];
|
||||
},
|
||||
error => {
|
||||
this.error = error;
|
||||
});
|
||||
getChartDetails(repo, name, { version })
|
||||
.then(result => {
|
||||
this.readme = result.readme;
|
||||
this.chartVersions = result.versions;
|
||||
this.selectedChart = result.versions[0];
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error;
|
||||
});
|
||||
});
|
||||
|
||||
@autobind()
|
||||
async onVersionChange({ value: version }: SelectOption) {
|
||||
async onVersionChange({ value: version }: SelectOption<string>) {
|
||||
this.selectedChart = this.chartVersions.find(chart => chart.version === version);
|
||||
this.readme = null;
|
||||
|
||||
try {
|
||||
this.chartPromise?.cancel();
|
||||
this.abortController?.abort();
|
||||
this.abortController = new AbortController();
|
||||
const { chart: { name, repo } } = this.props;
|
||||
const { readme } = await (this.chartPromise = helmChartsApi.get(repo, name, version));
|
||||
const { readme } = await getChartDetails(repo, name, { version, reqInit: { signal: this.abortController.signal }});
|
||||
|
||||
this.readme = readme;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import semver from "semver";
|
||||
import { observable } from "mobx";
|
||||
import { autobind } from "../../utils";
|
||||
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
||||
import { getChartDetails, HelmChart, listCharts } from "../../api/endpoints/helm-charts.api";
|
||||
import { ItemStore } from "../../item.store";
|
||||
import flatten from "lodash/flatten";
|
||||
|
||||
@@ -16,7 +16,7 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
|
||||
async loadAll() {
|
||||
try {
|
||||
const res = await this.loadItems(() => helmChartsApi.list());
|
||||
const res = await this.loadItems(() => listCharts());
|
||||
|
||||
this.failedLoading = false;
|
||||
|
||||
@@ -48,13 +48,13 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
return versions;
|
||||
}
|
||||
|
||||
const loadVersions = (repo: string) => {
|
||||
return helmChartsApi.get(repo, chartName).then(({ versions }) => {
|
||||
return versions.map(chart => ({
|
||||
repo,
|
||||
version: chart.getVersion()
|
||||
}));
|
||||
});
|
||||
const loadVersions = async (repo: string) => {
|
||||
const { versions } = await getChartDetails(repo, chartName);
|
||||
|
||||
return versions.map(chart => ({
|
||||
repo,
|
||||
version: chart.getVersion()
|
||||
}));
|
||||
};
|
||||
|
||||
if (!this.isLoaded) {
|
||||
|
||||
@@ -47,7 +47,8 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
||||
[columnId.namespace]: (item: HorizontalPodAutoscaler) => item.getNs(),
|
||||
[columnId.minPods]: (item: HorizontalPodAutoscaler) => item.getMinPods(),
|
||||
[columnId.maxPods]: (item: HorizontalPodAutoscaler) => item.getMaxPods(),
|
||||
[columnId.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas()
|
||||
[columnId.replicas]: (item: HorizontalPodAutoscaler) => item.getReplicas(),
|
||||
[columnId.age]: (item: HorizontalPodAutoscaler) => item.getTimeDiffFromNow(),
|
||||
}}
|
||||
searchFilters={[
|
||||
(item: HorizontalPodAutoscaler) => item.getSearchFields()
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, waitFor } from "@testing-library/react";
|
||||
import fse from "fs-extra";
|
||||
import React from "react";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
import { ExtensionDiscovery } from "../../../../extensions/extension-discovery";
|
||||
import { ExtensionLoader } from "../../../../extensions/extension-loader";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { ExtensionInstallationStateStore } from "../extension-install.store";
|
||||
import { Extensions } from "../extensions";
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
jest.setTimeout(30000);
|
||||
jest.mock("fs-extra");
|
||||
jest.mock("../../notifications");
|
||||
|
||||
jest.mock("../../../../common/utils", () => ({
|
||||
...jest.requireActual("../../../../common/utils"),
|
||||
@@ -20,37 +22,30 @@ jest.mock("../../../../common/utils", () => ({
|
||||
extractTar: jest.fn(() => Promise.resolve())
|
||||
}));
|
||||
|
||||
jest.mock("../../notifications", () => ({
|
||||
ok: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn()
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: (): void => void 0,
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock("electron", () => {
|
||||
return {
|
||||
app: {
|
||||
getVersion: () => "99.99.99",
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: (): void => void 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe("Extensions", () => {
|
||||
beforeEach(async () => {
|
||||
mockFs({
|
||||
"tmp": {}
|
||||
});
|
||||
|
||||
ExtensionInstallationStateStore.reset();
|
||||
UserStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
|
||||
await UserStore.createInstance().load();
|
||||
await ThemeStore.createInstance().init();
|
||||
|
||||
ExtensionLoader.resetInstance();
|
||||
ExtensionDiscovery.resetInstance();
|
||||
Extensions.installStates.clear();
|
||||
|
||||
ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve());
|
||||
|
||||
ExtensionLoader.resetInstance();
|
||||
ExtensionLoader.createInstance().addExtension({
|
||||
id: "extensionId",
|
||||
manifest: {
|
||||
@@ -64,49 +59,38 @@ describe("Extensions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||
ExtensionDiscovery.getInstance().isLoaded = true;
|
||||
render(<><Extensions /><ConfirmDialog/></>);
|
||||
|
||||
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
|
||||
expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(screen.getByText("Uninstall"));
|
||||
|
||||
// Approve confirm dialog
|
||||
fireEvent.click(screen.getByText("Yes"));
|
||||
|
||||
expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
|
||||
expect(screen.getByText("Disable").closest("button")).toBeDisabled();
|
||||
expect(screen.getByText("Uninstall").closest("button")).toBeDisabled();
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("displays error notification on uninstall error", () => {
|
||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||
ExtensionDiscovery.getInstance().isLoaded = true;
|
||||
(ExtensionDiscovery.getInstance().uninstallExtension as any).mockImplementationOnce(() =>
|
||||
Promise.reject()
|
||||
);
|
||||
render(<><Extensions /><ConfirmDialog/></>);
|
||||
|
||||
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
|
||||
expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
|
||||
const res = render(<><Extensions /><ConfirmDialog /></>);
|
||||
|
||||
fireEvent.click(screen.getByText("Uninstall"));
|
||||
expect(res.getByText("Disable").closest("button")).not.toBeDisabled();
|
||||
expect(res.getByText("Uninstall").closest("button")).not.toBeDisabled();
|
||||
|
||||
fireEvent.click(res.getByText("Uninstall"));
|
||||
|
||||
// Approve confirm dialog
|
||||
fireEvent.click(screen.getByText("Yes"));
|
||||
fireEvent.click(res.getByText("Yes"));
|
||||
|
||||
waitFor(() => {
|
||||
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
|
||||
expect(screen.getByText("Uninstall").closest("button")).not.toBeDisabled();
|
||||
expect(Notifications.error).toHaveBeenCalledTimes(1);
|
||||
await waitFor(() => {
|
||||
expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
|
||||
expect(res.getByText("Disable").closest("button")).toBeDisabled();
|
||||
expect(res.getByText("Uninstall").closest("button")).toBeDisabled();
|
||||
}, {
|
||||
timeout: 30000,
|
||||
});
|
||||
});
|
||||
|
||||
it("disables install button while installing", () => {
|
||||
render(<Extensions />);
|
||||
it("disables install button while installing", async () => {
|
||||
const res = render(<Extensions />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("Path or URL to an extension package", {
|
||||
(fse.unlink as jest.MockedFunction<typeof fse.unlink>).mockReturnValue(Promise.resolve() as any);
|
||||
|
||||
fireEvent.change(res.getByPlaceholderText("Path or URL to an extension package", {
|
||||
exact: false
|
||||
}), {
|
||||
target: {
|
||||
@@ -114,13 +98,8 @@ describe("Extensions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Install"));
|
||||
|
||||
waitFor(() => {
|
||||
expect(screen.getByText("Install").closest("button")).toBeDisabled();
|
||||
expect(fse.move).toHaveBeenCalledWith("");
|
||||
expect(Notifications.error).not.toHaveBeenCalled();
|
||||
});
|
||||
fireEvent.click(res.getByText("Install"));
|
||||
expect(res.getByText("Install").closest("button")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("displays spinner while extensions are loading", () => {
|
||||
@@ -128,8 +107,11 @@ describe("Extensions", () => {
|
||||
const { container } = render(<Extensions />);
|
||||
|
||||
expect(container.querySelector(".Spinner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not display the spinner while extensions are not loading", async () => {
|
||||
ExtensionDiscovery.getInstance().isLoaded = true;
|
||||
const { container } = render(<Extensions />);
|
||||
|
||||
waitFor(() =>
|
||||
expect(container.querySelector(".Spinner")).not.toBeInTheDocument()
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { action, computed, observable } from "mobx";
|
||||
import logger from "../../../main/logger";
|
||||
import { disposer, ExtendableDisposer } from "../../utils";
|
||||
import * as uuid from "uuid";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
export enum ExtensionInstallationState {
|
||||
INSTALLING = "installing",
|
||||
UNINSTALLING = "uninstalling",
|
||||
IDLE = "idle",
|
||||
}
|
||||
|
||||
const Prefix = "[ExtensionInstallationStore]";
|
||||
|
||||
export class ExtensionInstallationStateStore {
|
||||
private static InstallingFromMainChannel = "extension-installation-state-store:install";
|
||||
private static ClearInstallingFromMainChannel = "extension-installation-state-store:clear-install";
|
||||
private static PreInstallIds = observable.set<string>();
|
||||
private static UninstallingExtensions = observable.set<string>();
|
||||
private static InstallingExtensions = observable.set<string>();
|
||||
|
||||
static bindIpcListeners() {
|
||||
ipcRenderer
|
||||
.on(ExtensionInstallationStateStore.InstallingFromMainChannel, (event, extId) => {
|
||||
ExtensionInstallationStateStore.setInstalling(extId);
|
||||
})
|
||||
.on(ExtensionInstallationStateStore.ClearInstallingFromMainChannel, (event, extId) => {
|
||||
ExtensionInstallationStateStore.clearInstalling(extId);
|
||||
});
|
||||
}
|
||||
|
||||
@action static reset() {
|
||||
logger.warn(`${Prefix}: resetting, may throw errors`);
|
||||
ExtensionInstallationStateStore.InstallingExtensions.clear();
|
||||
ExtensionInstallationStateStore.UninstallingExtensions.clear();
|
||||
ExtensionInstallationStateStore.PreInstallIds.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Strictly transitions an extension from not installing to installing
|
||||
* @param extId the ID of the extension
|
||||
* @throws if state is not IDLE
|
||||
*/
|
||||
@action static setInstalling(extId: string): void {
|
||||
logger.debug(`${Prefix}: trying to set ${extId} as installing`);
|
||||
|
||||
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
|
||||
|
||||
if (curState !== ExtensionInstallationState.IDLE) {
|
||||
throw new Error(`${Prefix}: cannot set ${extId} as installing. Is currently ${curState}.`);
|
||||
}
|
||||
|
||||
ExtensionInstallationStateStore.InstallingExtensions.add(extId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts that an extension is being installed by the main process
|
||||
* @param extId the ID of the extension
|
||||
*/
|
||||
static setInstallingFromMain(extId: string): void {
|
||||
broadcastMessage(ExtensionInstallationStateStore.InstallingFromMainChannel, extId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts that an extension is no longer being installed by the main process
|
||||
* @param extId the ID of the extension
|
||||
*/
|
||||
static clearInstallingFromMain(extId: string): void {
|
||||
broadcastMessage(ExtensionInstallationStateStore.ClearInstallingFromMainChannel, extId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the start of a pre-install phase of an extension installation. The
|
||||
* part of the installation before the tarball has been unpacked and the ID
|
||||
* determined.
|
||||
* @returns a disposer which should be called to mark the end of the install phase
|
||||
*/
|
||||
@action static startPreInstall(): ExtendableDisposer {
|
||||
const preInstallStepId = uuid.v4();
|
||||
|
||||
logger.debug(`${Prefix}: starting a new preinstall phase: ${preInstallStepId}`);
|
||||
ExtensionInstallationStateStore.PreInstallIds.add(preInstallStepId);
|
||||
|
||||
return disposer(() => {
|
||||
ExtensionInstallationStateStore.PreInstallIds.delete(preInstallStepId);
|
||||
logger.debug(`${Prefix}: ending a preinstall phase: ${preInstallStepId}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Strictly transitions an extension from not uninstalling to uninstalling
|
||||
* @param extId the ID of the extension
|
||||
* @throws if state is not IDLE
|
||||
*/
|
||||
@action static setUninstalling(extId: string): void {
|
||||
logger.debug(`${Prefix}: trying to set ${extId} as uninstalling`);
|
||||
|
||||
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
|
||||
|
||||
if (curState !== ExtensionInstallationState.IDLE) {
|
||||
throw new Error(`${Prefix}: cannot set ${extId} as uninstalling. Is currently ${curState}.`);
|
||||
}
|
||||
|
||||
ExtensionInstallationStateStore.UninstallingExtensions.add(extId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strictly clears the INSTALLING state of an extension
|
||||
* @param extId The ID of the extension
|
||||
* @throws if state is not INSTALLING
|
||||
*/
|
||||
@action static clearInstalling(extId: string): void {
|
||||
logger.debug(`${Prefix}: trying to clear ${extId} as installing`);
|
||||
|
||||
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
|
||||
|
||||
switch (curState) {
|
||||
case ExtensionInstallationState.INSTALLING:
|
||||
return void ExtensionInstallationStateStore.InstallingExtensions.delete(extId);
|
||||
default:
|
||||
throw new Error(`${Prefix}: cannot clear INSTALLING state for ${extId}, it is currently ${curState}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strictly clears the UNINSTALLING state of an extension
|
||||
* @param extId The ID of the extension
|
||||
* @throws if state is not UNINSTALLING
|
||||
*/
|
||||
@action static clearUninstalling(extId: string): void {
|
||||
logger.debug(`${Prefix}: trying to clear ${extId} as uninstalling`);
|
||||
|
||||
const curState = ExtensionInstallationStateStore.getInstallationState(extId);
|
||||
|
||||
switch (curState) {
|
||||
case ExtensionInstallationState.UNINSTALLING:
|
||||
return void ExtensionInstallationStateStore.UninstallingExtensions.delete(extId);
|
||||
default:
|
||||
throw new Error(`${Prefix}: cannot clear UNINSTALLING state for ${extId}, it is currently ${curState}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of the extension. IDLE is default value.
|
||||
* @param extId The ID of the extension
|
||||
*/
|
||||
static getInstallationState(extId: string): ExtensionInstallationState {
|
||||
if (ExtensionInstallationStateStore.InstallingExtensions.has(extId)) {
|
||||
return ExtensionInstallationState.INSTALLING;
|
||||
}
|
||||
|
||||
if (ExtensionInstallationStateStore.UninstallingExtensions.has(extId)) {
|
||||
return ExtensionInstallationState.UNINSTALLING;
|
||||
}
|
||||
|
||||
return ExtensionInstallationState.IDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the extension is currently INSTALLING
|
||||
* @param extId The ID of the extension
|
||||
*/
|
||||
static isExtensionInstalling(extId: string): boolean {
|
||||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.INSTALLING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the extension is currently UNINSTALLING
|
||||
* @param extId The ID of the extension
|
||||
*/
|
||||
static isExtensionUninstalling(extId: string): boolean {
|
||||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.UNINSTALLING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the extension is currently IDLE
|
||||
* @param extId The ID of the extension
|
||||
*/
|
||||
static isExtensionIdle(extId: string): boolean {
|
||||
return ExtensionInstallationStateStore.getInstallationState(extId) === ExtensionInstallationState.IDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current number of extensions installing
|
||||
*/
|
||||
@computed static get installing(): number {
|
||||
return ExtensionInstallationStateStore.InstallingExtensions.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is at least one extension currently installing
|
||||
*/
|
||||
@computed static get anyInstalling(): boolean {
|
||||
return ExtensionInstallationStateStore.installing > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current number of extensions preinstalling
|
||||
*/
|
||||
@computed static get preinstalling(): number {
|
||||
return ExtensionInstallationStateStore.PreInstallIds.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is at least one extension currently downloading
|
||||
*/
|
||||
@computed static get anyPreinstalling(): boolean {
|
||||
return ExtensionInstallationStateStore.preinstalling > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is at least one installing or preinstalling step taking place
|
||||
*/
|
||||
@computed static get anyPreInstallingOrInstalling(): boolean {
|
||||
return ExtensionInstallationStateStore.anyInstalling || ExtensionInstallationStateStore.anyPreinstalling;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
||||
import "./button.scss";
|
||||
import React, { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
|
||||
|
||||
@@ -26,29 +26,22 @@ export class Button extends React.PureComponent<ButtonProps, {}> {
|
||||
|
||||
render() {
|
||||
const {
|
||||
className, waiting, label, primary, accent, plain, hidden, active, big,
|
||||
round, outlined, tooltip, light, children, ...props
|
||||
waiting, label, primary, accent, plain, hidden, active, big,
|
||||
round, outlined, tooltip, light, children, ...btnProps
|
||||
} = this.props;
|
||||
const btnProps: Partial<ButtonProps> = props;
|
||||
|
||||
if (hidden) return null;
|
||||
|
||||
btnProps.className = cssNames("Button", className, {
|
||||
btnProps.className = cssNames("Button", btnProps.className, {
|
||||
waiting, primary, accent, plain, active, big, round, outlined, light,
|
||||
});
|
||||
|
||||
const btnContent: ReactNode = (
|
||||
<>
|
||||
{label}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
// render as link
|
||||
if (this.props.href) {
|
||||
return (
|
||||
<a {...btnProps} ref={e => this.link = e}>
|
||||
{btnContent}
|
||||
{label}
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -56,7 +49,8 @@ export class Button extends React.PureComponent<ButtonProps, {}> {
|
||||
// render as button
|
||||
return (
|
||||
<button type="button" {...btnProps} ref={e => this.button = e}>
|
||||
{btnContent}
|
||||
{label}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,18 @@ import { Icon } from "../icon";
|
||||
export interface ConfirmDialogProps extends Partial<DialogProps> {
|
||||
}
|
||||
|
||||
export interface ConfirmDialogParams {
|
||||
ok?: () => void;
|
||||
export interface ConfirmDialogParams extends ConfirmDialogBooleanParams {
|
||||
ok?: () => any | Promise<any>;
|
||||
cancel?: () => any | Promise<any>;
|
||||
}
|
||||
|
||||
export interface ConfirmDialogBooleanParams {
|
||||
labelOk?: ReactNode;
|
||||
labelCancel?: ReactNode;
|
||||
message?: ReactNode;
|
||||
message: ReactNode;
|
||||
icon?: ReactNode;
|
||||
okButtonProps?: Partial<ButtonProps>
|
||||
cancelButtonProps?: Partial<ButtonProps>
|
||||
okButtonProps?: Partial<ButtonProps>;
|
||||
cancelButtonProps?: Partial<ButtonProps>;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -33,19 +37,26 @@ export class ConfirmDialog extends React.Component<ConfirmDialogProps> {
|
||||
ConfirmDialog.params = params;
|
||||
}
|
||||
|
||||
static close() {
|
||||
ConfirmDialog.isOpen = false;
|
||||
static confirm(params: ConfirmDialogBooleanParams): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
ConfirmDialog.open({
|
||||
ok: () => resolve(true),
|
||||
cancel: () => resolve(false),
|
||||
...params,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public defaultParams: ConfirmDialogParams = {
|
||||
static defaultParams: Partial<ConfirmDialogParams> = {
|
||||
ok: noop,
|
||||
cancel: noop,
|
||||
labelOk: "Ok",
|
||||
labelCancel: "Cancel",
|
||||
icon: <Icon big material="warning"/>,
|
||||
};
|
||||
|
||||
get params(): ConfirmDialogParams {
|
||||
return Object.assign({}, this.defaultParams, ConfirmDialog.params);
|
||||
return Object.assign({}, ConfirmDialog.defaultParams, ConfirmDialog.params);
|
||||
}
|
||||
|
||||
ok = async () => {
|
||||
@@ -54,16 +65,21 @@ export class ConfirmDialog extends React.Component<ConfirmDialogProps> {
|
||||
await Promise.resolve(this.params.ok()).catch(noop);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
ConfirmDialog.isOpen = false;
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.isSaving = false;
|
||||
};
|
||||
|
||||
close = () => {
|
||||
ConfirmDialog.close();
|
||||
close = async () => {
|
||||
try {
|
||||
await Promise.resolve(this.params.cancel()).catch(noop);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
ConfirmDialog.isOpen = false;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { action, autorun } from "mobx";
|
||||
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
||||
import { getChartDetails, getChartValues, HelmChart } from "../../api/endpoints/helm-charts.api";
|
||||
import { IReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
|
||||
import { Notifications } from "../notifications";
|
||||
|
||||
@@ -54,7 +54,7 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
const { repo, name, version } = this.getData(tabId);
|
||||
|
||||
this.versions.clearData(tabId); // reset
|
||||
const charts = await helmChartsApi.get(repo, name, version);
|
||||
const charts = await getChartDetails(repo, name, { version });
|
||||
const versions = charts.versions.map(chartVersion => chartVersion.version);
|
||||
|
||||
this.versions.setData(tabId, versions);
|
||||
@@ -64,7 +64,7 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
async loadValues(tabId: TabId, attempt = 0): Promise<void> {
|
||||
const data = this.getData(tabId);
|
||||
const { repo, name, version } = data;
|
||||
const values = await helmChartsApi.getValues(repo, name, version);
|
||||
const values = await getChartValues(repo, name, version);
|
||||
|
||||
if (values) {
|
||||
this.setData(tabId, { ...data, values });
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Icon } from "../icon";
|
||||
import { LogTabData } from "./log-tab.store";
|
||||
|
||||
interface Props {
|
||||
tabData: LogTabData
|
||||
tabData?: LogTabData
|
||||
logs: string[]
|
||||
save: (data: Partial<LogTabData>) => void
|
||||
reload: () => void
|
||||
@@ -19,6 +19,11 @@ interface Props {
|
||||
|
||||
export const LogControls = observer((props: Props) => {
|
||||
const { tabData, save, reload, logs } = props;
|
||||
|
||||
if (!tabData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { showTimestamps, previous } = tabData;
|
||||
const since = logs.length ? logStore.getTimestamps(logs[0]) : null;
|
||||
const pod = new Pod(tabData.selectedPod);
|
||||
|
||||
@@ -26,23 +26,14 @@ export class Logs extends React.Component<Props> {
|
||||
|
||||
componentDidMount() {
|
||||
disposeOnUnmount(this,
|
||||
reaction(() => this.props.tab.id, this.reload, { fireImmediately: true })
|
||||
reaction(() => this.props.tab.id, this.reload, { fireImmediately: true }),
|
||||
);
|
||||
}
|
||||
|
||||
get tabData() {
|
||||
return logTabStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
}
|
||||
|
||||
@autobind()
|
||||
save(data: Partial<LogTabData>) {
|
||||
logTabStore.setData(this.tabId, { ...this.tabData, ...data });
|
||||
}
|
||||
|
||||
load = async () => {
|
||||
this.isLoading = true;
|
||||
await logStore.load(this.tabId);
|
||||
@@ -82,15 +73,19 @@ export class Logs extends React.Component<Props> {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
renderResourceSelector() {
|
||||
renderResourceSelector(data?: LogTabData) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const logs = logStore.logs;
|
||||
const searchLogs = this.tabData.showTimestamps ? logs : logStore.logsWithoutTimestamps;
|
||||
const searchLogs = data.showTimestamps ? logs : logStore.logsWithoutTimestamps;
|
||||
const controls = (
|
||||
<div className="flex gaps">
|
||||
<LogResourceSelector
|
||||
tabId={this.tabId}
|
||||
tabData={this.tabData}
|
||||
save={this.save}
|
||||
tabData={data}
|
||||
save={newData => logTabStore.setData(this.tabId, { ...data, ...newData })}
|
||||
reload={this.reload}
|
||||
/>
|
||||
<LogSearch
|
||||
@@ -115,10 +110,15 @@ export class Logs extends React.Component<Props> {
|
||||
|
||||
render() {
|
||||
const logs = logStore.logs;
|
||||
const data = logTabStore.getData(this.tabId);
|
||||
|
||||
if (!data) {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="PodLogs flex column">
|
||||
{this.renderResourceSelector()}
|
||||
{this.renderResourceSelector(data)}
|
||||
<LogList
|
||||
logs={logs}
|
||||
id={this.tabId}
|
||||
@@ -128,8 +128,8 @@ export class Logs extends React.Component<Props> {
|
||||
/>
|
||||
<LogControls
|
||||
logs={logs}
|
||||
tabData={this.tabData}
|
||||
save={this.save}
|
||||
tabData={data}
|
||||
save={newData => logTabStore.setData(this.tabId, { ...data, ...newData })}
|
||||
reload={this.reload}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -315,6 +315,7 @@ export class Input extends React.Component<InputProps, State> {
|
||||
rows: multiLine ? (rows || 1) : null,
|
||||
ref: this.bindRef,
|
||||
spellCheck: "false",
|
||||
disabled,
|
||||
});
|
||||
const showErrors = errors.length > 0 && !valid && dirty;
|
||||
const errorsInfo = (
|
||||
|
||||
@@ -47,6 +47,14 @@ export const isUrl: InputValidator = {
|
||||
},
|
||||
};
|
||||
|
||||
export const isExtensionNameInstallRegex = /^(?<name>(@[-\w]+\/)?[-\w]+)(@(?<version>\d\.\d\.\d(-\w+)?))?$/gi;
|
||||
|
||||
export const isExtensionNameInstall: InputValidator = {
|
||||
condition: ({ type }) => type === "text",
|
||||
message: () => "Not an extension name with optional version",
|
||||
validate: value => value.match(isExtensionNameInstallRegex) !== null,
|
||||
};
|
||||
|
||||
export const isPath: InputValidator = {
|
||||
condition: ({ type }) => type === "text",
|
||||
message: () => `This field must be a valid path`,
|
||||
|
||||
@@ -2,105 +2,106 @@ import "./kube-object-status-icon.scss";
|
||||
|
||||
import React from "react";
|
||||
import { Icon } from "../icon";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { cssNames, formatDuration } from "../../utils";
|
||||
import { KubeObjectStatusRegistration, kubeObjectStatusRegistry } from "../../../extensions/registries/kube-object-status-registry";
|
||||
import { KubeObjectStatus, KubeObjectStatusLevel } from "../../..//extensions/renderer-api/k8s-api";
|
||||
import { computed } from "mobx";
|
||||
import { KubeObject, KubeObjectStatus, KubeObjectStatusLevel } from "../../..//extensions/renderer-api/k8s-api";
|
||||
import { kubeObjectStatusRegistry } from "../../../extensions/registries";
|
||||
|
||||
function statusClassName(level: number): string {
|
||||
switch (level) {
|
||||
case KubeObjectStatusLevel.INFO:
|
||||
return "info";
|
||||
case KubeObjectStatusLevel.WARNING:
|
||||
return "warning";
|
||||
case KubeObjectStatusLevel.CRITICAL:
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
function statusTitle(level: KubeObjectStatusLevel): string {
|
||||
switch (level) {
|
||||
case KubeObjectStatusLevel.INFO:
|
||||
return "Info";
|
||||
case KubeObjectStatusLevel.WARNING:
|
||||
return "Warning";
|
||||
case KubeObjectStatusLevel.CRITICAL:
|
||||
return "Critical";
|
||||
}
|
||||
}
|
||||
|
||||
function getAge(timestamp: string) {
|
||||
return timestamp
|
||||
? formatDuration(Date.now() - new Date(timestamp).getTime(), true)
|
||||
: "";
|
||||
}
|
||||
|
||||
interface SplitStatusesByLevel {
|
||||
maxLevel: string,
|
||||
criticals: KubeObjectStatus[];
|
||||
warnings: KubeObjectStatus[];
|
||||
infos: KubeObjectStatus[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This fuction returns the class level for corresponding to the highest status level
|
||||
* and the statuses split by their levels.
|
||||
* @param src a list of status items
|
||||
*/
|
||||
function splitByLevel(src: KubeObjectStatus[]): SplitStatusesByLevel {
|
||||
const parts = new Map(Object.values(KubeObjectStatusLevel).map(v => [v, []]));
|
||||
|
||||
src.forEach(status => parts.get(status.level).push(status));
|
||||
|
||||
const criticals = parts.get(KubeObjectStatusLevel.CRITICAL);
|
||||
const warnings = parts.get(KubeObjectStatusLevel.WARNING);
|
||||
const infos = parts.get(KubeObjectStatusLevel.INFO);
|
||||
const maxLevel = statusClassName(criticals[0]?.level ?? warnings[0]?.level ?? infos[0].level);
|
||||
|
||||
return { maxLevel, criticals, warnings, infos };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
object: KubeObject;
|
||||
}
|
||||
|
||||
export class KubeObjectStatusIcon extends React.Component<Props> {
|
||||
@computed get objectStatuses() {
|
||||
const { object } = this.props;
|
||||
const registrations = kubeObjectStatusRegistry.getItemsForKind(object.kind, object.apiVersion);
|
||||
|
||||
return registrations.map((item: KubeObjectStatusRegistration) => { return item.resolve(object); }).filter((item: KubeObjectStatus) => !!item);
|
||||
}
|
||||
|
||||
statusClassName(level: number): string {
|
||||
switch (level) {
|
||||
case KubeObjectStatusLevel.INFO:
|
||||
return "info";
|
||||
case KubeObjectStatusLevel.WARNING:
|
||||
return "warning";
|
||||
case KubeObjectStatusLevel.CRITICAL:
|
||||
return "error";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
statusTitle(level: number): string {
|
||||
switch (level) {
|
||||
case KubeObjectStatusLevel.INFO:
|
||||
return "Info";
|
||||
case KubeObjectStatusLevel.WARNING:
|
||||
return "Warning";
|
||||
case KubeObjectStatusLevel.CRITICAL:
|
||||
return "Critical";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
getAge(timestamp: string) {
|
||||
if (!timestamp) return "";
|
||||
const diff = Date.now() - new Date(timestamp).getTime();
|
||||
|
||||
return formatDuration(diff, true);
|
||||
}
|
||||
|
||||
renderStatuses(statuses: KubeObjectStatus[], level: number) {
|
||||
const filteredStatuses = statuses.filter((item) => item.level == level);
|
||||
|
||||
return filteredStatuses.length > 0 && (
|
||||
<div className={cssNames("level", this.statusClassName(level))}>
|
||||
<div className={cssNames("level", statusClassName(level))}>
|
||||
<span className="title">
|
||||
{this.statusTitle(level)}
|
||||
{statusTitle(level)}
|
||||
</span>
|
||||
{ filteredStatuses.map((status, index) =>{
|
||||
return (
|
||||
{
|
||||
filteredStatuses.map((status, index) => (
|
||||
<div key={`kube-resource-status-${level}-${index}`} className={cssNames("status", "msg")}>
|
||||
- {status.text} <span className="age"> · { this.getAge(status.timestamp) }</span>
|
||||
- {status.text} <span className="age"> · {getAge(status.timestamp)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { objectStatuses} = this;
|
||||
const statuses = kubeObjectStatusRegistry.getItemsForObject(this.props.object);
|
||||
|
||||
if (!objectStatuses.length) return null;
|
||||
if (statuses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortedStatuses = objectStatuses.sort((a: KubeObjectStatus, b: KubeObjectStatus) => {
|
||||
if (a.level < b.level ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.level > b.level ) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
const level = this.statusClassName(sortedStatuses[0].level);
|
||||
const { maxLevel, criticals, warnings, infos } = splitByLevel(statuses);
|
||||
|
||||
return (
|
||||
<Icon
|
||||
material={level}
|
||||
className={cssNames("KubeObjectStatusIcon", level)}
|
||||
material={maxLevel}
|
||||
className={cssNames("KubeObjectStatusIcon", maxLevel)}
|
||||
tooltip={{
|
||||
children: (
|
||||
<div className="KubeObjectStatusTooltip">
|
||||
{this.renderStatuses(sortedStatuses, KubeObjectStatusLevel.CRITICAL)}
|
||||
{this.renderStatuses(sortedStatuses, KubeObjectStatusLevel.WARNING)}
|
||||
{this.renderStatuses(sortedStatuses, KubeObjectStatusLevel.INFO)}
|
||||
{this.renderStatuses(criticals, KubeObjectStatusLevel.CRITICAL)}
|
||||
{this.renderStatuses(warnings, KubeObjectStatusLevel.WARNING)}
|
||||
{this.renderStatuses(infos, KubeObjectStatusLevel.INFO)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -24,13 +24,11 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const object = this.props.object;
|
||||
const { object } = this.props;
|
||||
const {
|
||||
getName, getNs, getLabels, getResourceVersion, selfLink,
|
||||
getAnnotations, getFinalizers, getId, getAge,
|
||||
metadata: { creationTimestamp },
|
||||
getNs, getLabels, getResourceVersion, selfLink, getAnnotations,
|
||||
getFinalizers, getId, getAge, getName, metadata: { creationTimestamp },
|
||||
} = object;
|
||||
|
||||
const ownerRefs = object.getOwnerRefs();
|
||||
|
||||
return (
|
||||
@@ -39,7 +37,8 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
|
||||
{getAge(true, false)} ago ({<LocaleDate date={creationTimestamp} />})
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Name" hidden={this.isHidden("name")}>
|
||||
{getName()} <KubeObjectStatusIcon key="icon" object={object} />
|
||||
{getName()}
|
||||
<KubeObjectStatusIcon key="icon" object={object} />
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Namespace" hidden={this.isHidden("namespace") || !getNs()}>
|
||||
{getNs()}
|
||||
@@ -68,7 +67,7 @@ export class KubeObjectMeta extends React.Component<KubeObjectMetaProps> {
|
||||
labels={getFinalizers()}
|
||||
hidden={this.isHidden("finalizers")}
|
||||
/>
|
||||
{ownerRefs && ownerRefs.length > 0 &&
|
||||
{ownerRefs?.length > 0 &&
|
||||
<DrawerItem name="Controlled By" hidden={this.isHidden("ownerReferences")}>
|
||||
{
|
||||
ownerRefs.map(ref => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import "./sidebar.scss";
|
||||
import type { TabLayoutRoute } from "./tab-layout";
|
||||
|
||||
import React from "react";
|
||||
import { computed } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { cssNames } from "../../utils";
|
||||
@@ -45,9 +44,13 @@ export class Sidebar extends React.Component<Props> {
|
||||
crdStore.reloadAll();
|
||||
}
|
||||
|
||||
@computed get crdSubMenus(): React.ReactNode {
|
||||
if (!crdStore.isLoaded && crdStore.isLoading) {
|
||||
return <Spinner centerHorizontal/>;
|
||||
renderCustomResources() {
|
||||
if (crdStore.isLoading) {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return Object.entries(crdStore.groups).map(([group, crds]) => {
|
||||
@@ -268,7 +271,7 @@ export class Sidebar extends React.Component<Props> {
|
||||
icon={<Icon material="extension"/>}
|
||||
>
|
||||
{this.renderTreeFromTabRoutes(CustomResources.tabRoutes)}
|
||||
{this.crdSubMenus}
|
||||
{this.renderCustomResources()}
|
||||
</SidebarItem>
|
||||
{this.renderRegisteredMenus()}
|
||||
</div>
|
||||
|
||||
@@ -34,12 +34,6 @@
|
||||
margin-top: calc(var(--spinner-size) / -2);
|
||||
}
|
||||
|
||||
&.centerHorizontal {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: calc(var(--spinner-size) / -2);
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
||||
@@ -6,7 +6,6 @@ import { cssNames } from "../../utils";
|
||||
export interface SpinnerProps extends React.HTMLProps<any> {
|
||||
singleColor?: boolean;
|
||||
center?: boolean;
|
||||
centerHorizontal?: boolean;
|
||||
}
|
||||
|
||||
export class Spinner extends React.Component<SpinnerProps, {}> {
|
||||
@@ -16,8 +15,8 @@ export class Spinner extends React.Component<SpinnerProps, {}> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { center, singleColor, centerHorizontal, className, ...props } = this.props;
|
||||
const classNames = cssNames("Spinner", className, { singleColor, center, centerHorizontal });
|
||||
const { center, singleColor, className, ...props } = this.props;
|
||||
const classNames = cssNames("Spinner", className, { singleColor, center });
|
||||
|
||||
return <div {...props} className={classNames} />;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user