Merge branch 'master' into wrap-pod-logs

This commit is contained in:
Alex Andreev committed 2022-09-01 09:49:29 +03:00
commit e02278d515
73 files changed
+2590 -661

No files matched your search

@@ -7,6 +7,7 @@ import styles from "./cluster-issues.module.scss";
import React from "react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import { computed, makeObservable } from "mobx";
import { Icon } from "../icon";
import { SubHeader } from "../layout/sub-header";
@@ -14,11 +15,9 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
import { cssNames, prevDefault } from "../../utils";
import type { ItemObject } from "../../../common/item.store";
import { Spinner } from "../spinner";
import type { ThemeStore } from "../../themes/store";
import type { ApiManager } from "../../../common/k8s-api/api-manager";
import { KubeObjectAge } from "../kube-object/age";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import type { NodeStore } from "../+nodes/store";
import type { EventStore } from "../+events/store";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
@@ -28,6 +27,8 @@ import type { PageParam } from "../../navigation";
import type { ToggleKubeDetailsPane } from "../kube-detail-params/toggle-details.injectable";
import kubeSelectedUrlParamInjectable from "../kube-detail-params/kube-selected-url.injectable";
import toggleKubeDetailsPaneInjectable from "../kube-detail-params/toggle-details.injectable";
import type { LensTheme } from "../../themes/store";
import activeThemeInjectable from "../../themes/active.injectable";
export interface ClusterIssuesProps {
className?: string;
@@ -48,7 +49,7 @@ enum sortBy {
}
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
nodeStore: NodeStore;
eventStore: EventStore;
apiManager: ApiManager;
@@ -166,7 +167,7 @@ class NonInjectedClusterIssues extends React.Component<ClusterIssuesProps & Depe
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
sortSyncWithUrl={false}
getTableRow={this.getTableRow}
className={cssNames("box grow", this.props.themeStore.activeTheme.type)}
className={cssNames("box grow", this.props.activeTheme.get().type)}
>
<TableHead nowrap>
<TableCell className="message">Message</TableCell>
@@ -191,7 +192,7 @@ class NonInjectedClusterIssues extends React.Component<ClusterIssuesProps & Depe
export const ClusterIssues = withInjectables<Dependencies, ClusterIssuesProps>(NonInjectedClusterIssues, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
apiManager: di.inject(apiManagerInjectable),
eventStore: di.inject(eventStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable),
@@ -16,12 +16,13 @@ import type { PieChartData } from "../chart";
import { PieChart } from "../chart";
import { ClusterNoMetrics } from "./cluster-no-metrics";
import { bytesToUnits, cssNames } from "../../utils";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api";
import { withInjectables } from "@ogre-tools/injectable-react";
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
import nodeStoreInjectable from "../+nodes/store.injectable";
import themeStoreInjectable from "../../themes/store.injectable";
import type { IComputedValue } from "mobx";
import activeThemeInjectable from "../../themes/active.injectable";
function createLabels(rawLabelData: [string, number | undefined][]): string[] {
return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`);
@@ -30,13 +31,13 @@ function createLabels(rawLabelData: [string, number | undefined][]): string[] {
interface Dependencies {
clusterOverviewStore: ClusterOverviewStore;
nodeStore: NodeStore;
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
const NonInjectedClusterPieCharts = observer(({
clusterOverviewStore,
nodeStore,
themeStore,
activeTheme,
}: Dependencies) => {
const renderLimitWarning = () => {
return (
@@ -54,7 +55,7 @@ const NonInjectedClusterPieCharts = observer(({
const { podUsage, podAllocatableCapacity, podCapacity } = data;
const cpuLimitsOverload = cpuLimits > cpuAllocatableCapacity;
const memoryLimitsOverload = memoryLimits > memoryAllocatableCapacity;
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor;
const defaultColor = activeTheme.get().colors.pieChartDefaultColor;
if (!memoryCapacity || !cpuCapacity || !podCapacity || !memoryAllocatableCapacity || !cpuAllocatableCapacity || !podAllocatableCapacity) return null;
const cpuData: PieChartData = {
@@ -261,6 +262,6 @@ export const ClusterPieCharts = withInjectables<Dependencies>(NonInjectedCluster
getProps: (di) => ({
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
});
@@ -91,7 +91,7 @@ const attemptInstallByInfo = ({
}
} else {
const versions = Object.keys(json.versions)
.map(version => new SemVer(version, { loose: true, includePrerelease: true }))
.map(version => new SemVer(version, { loose: true }))
// ignore pre-releases for auto picking the version
.filter(version => version.prerelease.length === 0);
@@ -55,7 +55,7 @@ export class HelmChartStore extends ItemStore<HelmChart> {
protected sortVersions = (versions: ChartVersion[]) => {
return versions
.map(chartVersion => ({ ...chartVersion, __version: semver.coerce(chartVersion.version, { includePrerelease: true, loose: true }) }))
.map(chartVersion => ({ ...chartVersion, __version: semver.coerce(chartVersion.version, { loose: true }) }))
.sort(sortCompareChartVersions)
.map(({ __version, ...chartVersion }) => chartVersion);
};
@@ -3,13 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { IObservableValue } from "mobx";
import type { IComputedValue, IObservableValue } from "mobx";
import { runInAction, action, observable, computed } from "mobx";
import type { TargetHelmRelease } from "../target-helm-release.injectable";
import type { CallForHelmRelease, DetailedHelmRelease } from "./call-for-helm-release/call-for-helm-release.injectable";
import callForHelmReleaseInjectable from "./call-for-helm-release/call-for-helm-release.injectable";
import type { ThemeStore } from "../../../../themes/store";
import themeStoreInjectable from "../../../../themes/store.injectable";
import type { LensTheme } from "../../../../themes/store";
import type { CallForHelmReleaseConfiguration } from "./call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
import callForHelmReleaseConfigurationInjectable from "./call-for-helm-release-configuration/call-for-helm-release-configuration.injectable";
import { toHelmRelease } from "../../releases.injectable";
@@ -31,6 +30,7 @@ import type { NavigateToHelmReleases } from "../../../../../common/front-end-rou
import navigateToHelmReleasesInjectable from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import assert from "assert";
import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";
import activeThemeInjectable from "../../../../themes/active.injectable";
const releaseDetailsModelInjectable = getInjectable({
id: "release-details-model",
@@ -38,7 +38,7 @@ const releaseDetailsModelInjectable = getInjectable({
instantiate: (di, targetRelease: TargetHelmRelease) => {
const callForHelmRelease = di.inject(callForHelmReleaseInjectable);
const callForHelmReleaseConfiguration = di.inject(callForHelmReleaseConfigurationInjectable);
const themeStore = di.inject(themeStoreInjectable);
const activeTheme = di.inject(activeThemeInjectable);
const getResourceDetailsUrl = di.inject(getResourceDetailsUrlInjectable);
const updateRelease = di.inject(updateReleaseInjectable);
const showCheckedErrorNotification = di.inject(showCheckedErrorNotificationInjectable);
@@ -50,7 +50,7 @@ const releaseDetailsModelInjectable = getInjectable({
const model = new ReleaseDetailsModel({
callForHelmRelease,
targetRelease,
themeStore,
activeTheme,
callForHelmReleaseConfiguration,
getResourceDetailsUrl,
updateRelease,
@@ -92,7 +92,7 @@ export interface ConfigurationInput {
interface Dependencies {
callForHelmRelease: CallForHelmRelease;
targetRelease: TargetHelmRelease;
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
callForHelmReleaseConfiguration: CallForHelmReleaseConfiguration;
getResourceDetailsUrl: GetResourceDetailsUrl;
updateRelease: CallForHelmReleaseUpdate;
@@ -259,7 +259,7 @@ export class ReleaseDetailsModel {
}
@computed get activeTheme() {
return this.dependencies.themeStore.activeTheme.type;
return this.dependencies.activeTheme.get().type;
}
close = () => {
@@ -12,18 +12,19 @@ import { ResourceMetricsContext } from "../resource-metrics";
import { observer } from "mobx-react";
import { mapValues } from "lodash";
import { type MetricsTab, metricTabOptions } from "../chart/options";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import type { IComputedValue } from "mobx";
import activeThemeInjectable from "../../themes/active.injectable";
export interface NodeChartsProps {}
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
const NonInjectedNodeCharts = observer(({
themeStore,
activeTheme,
}: Dependencies & NodeChartsProps) => {
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
@@ -31,7 +32,7 @@ const NonInjectedNodeCharts = observer(({
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const id = object.getId();
const { chartCapacityColor } = themeStore.activeTheme.colors;
const { chartCapacityColor } = activeTheme.get().colors;
const {
memoryUsage,
workloadMemoryUsage,
@@ -162,6 +163,6 @@ const NonInjectedNodeCharts = observer(({
export const NodeCharts = withInjectables<Dependencies, NodeChartsProps>(NonInjectedNodeCharts, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
});
@@ -10,18 +10,19 @@ import { BarChart, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics";
import { ResourceMetricsContext } from "../resource-metrics";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import type { IComputedValue } from "mobx";
import activeThemeInjectable from "../../themes/active.injectable";
export interface VolumeClaimDiskChartProps {}
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
const NonInjectedVolumeClaimDiskChart = observer(({
themeStore,
activeTheme,
}: Dependencies & VolumeClaimDiskChartProps) => {
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
@@ -29,7 +30,7 @@ const NonInjectedVolumeClaimDiskChart = observer(({
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const id = object.getId();
const { chartCapacityColor } = themeStore.activeTheme.colors;
const { chartCapacityColor } = activeTheme.get().colors;
const { diskUsage, diskCapacity } = metrics;
const usage = normalizeMetrics(diskUsage).data.result[0].values;
const capacity = normalizeMetrics(diskCapacity).data.result[0].values;
@@ -65,6 +66,6 @@ const NonInjectedVolumeClaimDiskChart = observer(({
export const VolumeClaimDiskChart = withInjectables<Dependencies, VolumeClaimDiskChartProps>(NonInjectedVolumeClaimDiskChart, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
});
@@ -31,7 +31,7 @@ const NonInjectedOverviewStatuses = observer(
</a>
</div>
<OverviewWorkloadStatus status={workload.status.get()} />
<OverviewWorkloadStatus workload={workload} />
</div>
))}
</div>
@@ -8,92 +8,101 @@ import "./overview-workload-status.scss";
import React from "react";
import capitalize from "lodash/capitalize";
import { observer } from "mobx-react";
import type { DatasetTooltipLabel, PieChartData } from "../chart";
import type { PieChartData } from "../chart";
import { PieChart } from "../chart";
import { cssVar, object } from "../../utils";
import type { ThemeStore } from "../../themes/store";
import { object } from "../../utils";
import type { LensTheme } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import type { PascalCase } from "type-fest";
import type { IComputedValue } from "mobx";
import activeThemeInjectable from "../../themes/active.injectable";
import type { Workload } from "./workloads/workload-injection-token";
export type LowercaseOrPascalCase<T extends string> = Lowercase<T> | PascalCase<T>;
export type WorkloadStatus = Partial<Record<LowercaseOrPascalCase<keyof typeof statusBackgroundColorMapping>, number>>;
function toLowercase<T extends string>(src: T): Lowercase<T> {
return src.toLowerCase() as Lowercase<T>;
}
export interface OverviewWorkloadStatusProps {
status: Partial<Record<string, number>>;
workload: Workload;
}
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
@observer
class NonInjectedOverviewWorkloadStatus extends React.Component<OverviewWorkloadStatusProps & Dependencies> {
private elem: HTMLElement | null = null;
const statusBackgroundColorMapping = {
"running": "colorOk",
"scheduled": "colorOk",
"pending": "colorWarning",
"suspended": "colorWarning",
"evicted": "colorError",
"succeeded": "colorSuccess",
"failed": "colorError",
"terminated": "colorTerminated",
"terminating": "colorTerminated",
"unknown": "colorVague",
"complete": "colorSuccess",
} as const;
renderChart() {
if (!this.elem) {
return null;
}
const NonInjectedOverviewWorkloadStatus = observer((props: OverviewWorkloadStatusProps & Dependencies) => {
const {
workload,
activeTheme,
} = props;
const cssVars = cssVar(this.elem);
const chartData: Required<PieChartData> = {
labels: [],
datasets: [],
};
const statusesToBeShown = object.entries(workload.status.get()).filter(([, val]) => val > 0);
const theme = activeTheme.get();
const statuses = object.entries(this.props.status).filter(([, val]) => val > 0);
const emptyDataSet = {
data: [1],
backgroundColor: [theme.colors.pieChartDefaultColor],
label: "Empty",
};
const statusDataSet = {
label: "Status",
data: statusesToBeShown.map(([, value]) => value),
backgroundColor: statusesToBeShown.map(([status]) => (
theme.colors[statusBackgroundColorMapping[toLowercase(status)]]
)),
tooltipLabels: statusesToBeShown.map(([status]) => (
(percent: string) => `${capitalize(status)}: ${percent}`
)),
};
if (statuses.length === 0) {
chartData.datasets.push({
data: [1],
backgroundColor: [this.props.themeStore.activeTheme.colors.pieChartDefaultColor],
label: "Empty",
});
} else {
const data: number[] = [];
const backgroundColor: string[] = [];
const tooltipLabels: DatasetTooltipLabel[] = [];
const chartData: Required<PieChartData> = {
datasets: [statusesToBeShown.length > 0 ? statusDataSet : emptyDataSet],
for (const [status, value] of statuses) {
data.push(value);
backgroundColor.push(cssVars.get(`--workload-status-${status.toLowerCase()}`).toString());
tooltipLabels.push(percent => `${capitalize(status)}: ${percent}`);
chartData.labels.push(`${capitalize(status)}: ${value}`);
}
labels: statusesToBeShown.map(
([status, value]) => `${capitalize(status)}: ${value}`,
),
};
chartData.datasets.push({
data,
backgroundColor,
label: "Status",
tooltipLabels,
});
}
return (
<PieChart
data={chartData}
options={{
elements: {
arc: {
borderWidth: 0,
return (
<div className="OverviewWorkloadStatus">
<div className="flex column align-center box grow">
<PieChart
data={chartData}
options={{
elements: {
arc: {
borderWidth: 0,
},
},
},
}}
/>
);
}
render() {
return (
<div className="OverviewWorkloadStatus" ref={e => this.elem = e}>
<div className="flex column align-center box grow">
{this.renderChart()}
</div>
}}
data-testid={`workload-overview-status-chart-${workload.title.toLowerCase().replace(/\s+/, "-")}`}
/>
</div>
);
}
}
</div>
);
});
export const OverviewWorkloadStatus = withInjectables<Dependencies, OverviewWorkloadStatusProps>(NonInjectedOverviewWorkloadStatus, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
});
@@ -104,7 +104,7 @@ class NonInjectedWorkloadsOverview extends React.Component<Dependencies> {
render() {
return (
<SiblingsInTabLayout>
<div className="WorkloadsOverview flex column gaps">
<div className="WorkloadsOverview flex column gaps" data-testid="page-for-workloads-overview">
<div className="header flex gaps align-center">
<h5 className="box grow">Overview</h5>
{this.renderLoadErrors()}
@@ -4,12 +4,13 @@
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type { WorkloadStatus } from "../overview-workload-status";
export interface Workload {
resourceName: string;
open: () => void;
amountOfItems: IComputedValue<number>;
status: IComputedValue<Partial<Record<string, number>>>;
status: IComputedValue<WorkloadStatus>;
title: string;
orderNumber: number;
}
@@ -10,27 +10,28 @@ import { BarChart } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics";
import { ResourceMetricsContext } from "../resource-metrics";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { mapValues } from "lodash";
import { type MetricsTab, metricTabOptions } from "../chart/options";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import activeThemeInjectable from "../../themes/active.injectable";
import type { IComputedValue } from "mobx";
export interface ContainerChartsProps {}
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
const NonInjectedContainerCharts = observer(({
themeStore,
activeTheme,
}: Dependencies & ContainerChartsProps) => {
const { metrics, tab, object } = useContext(ResourceMetricsContext) ?? {};
if (!metrics || !object || !tab) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>;
const { chartCapacityColor } = themeStore.activeTheme.colors;
const { chartCapacityColor } = activeTheme.get().colors;
const {
cpuUsage,
cpuRequests,
@@ -127,6 +128,6 @@ const NonInjectedContainerCharts = observer(({
export const ContainerCharts = withInjectables<Dependencies, ContainerChartsProps>(NonInjectedContainerCharts, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
});
+5 -1
View File
@@ -49,7 +49,11 @@ export const Button = withTooltip((props: ButtonProps) => {
// render as button
return (
<button type="button" {...btnProps}>
<button
type="button"
{...btnProps}
data-waiting={typeof waiting === "boolean" ? String(waiting) : undefined}
>
{label}
{children}
</button>
+7 -6
View File
@@ -13,11 +13,12 @@ import type { ChartProps } from "./chart";
import { Chart, ChartKind } from "./chart";
import { bytesToUnits, cssNames, isObject } from "../../utils";
import { ZebraStripesPlugin } from "./zebra-stripes.plugin";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { NoMetrics } from "../resource-metrics/no-metrics";
import assert from "assert";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import type { IComputedValue } from "mobx";
import activeThemeInjectable from "../../themes/active.injectable";
export interface BarChartProps extends ChartProps {
name?: string;
@@ -27,11 +28,11 @@ export interface BarChartProps extends ChartProps {
const getBarColor: Scriptable<string> = ({ dataset }) => Color(dataset?.borderColor).alpha(0.2).string();
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
const NonInjectedBarChart = observer(({
themeStore,
activeTheme,
name,
data,
className,
@@ -40,7 +41,7 @@ const NonInjectedBarChart = observer(({
options: customOptions,
...settings
}: Dependencies & BarChartProps) => {
const { textColorPrimary, borderFaintColor, chartStripesColor } = themeStore.activeTheme.colors;
const { textColorPrimary, borderFaintColor, chartStripesColor } = activeTheme.get().colors;
const { datasets: rawDatasets = [], ...rest } = data;
const datasets = rawDatasets
.filter(set => set.data?.length)
@@ -168,7 +169,7 @@ const NonInjectedBarChart = observer(({
export const BarChart = withInjectables<Dependencies, BarChartProps>(NonInjectedBarChart, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
});
+19 -17
View File
@@ -36,6 +36,7 @@ export interface ChartProps {
redraw?: boolean; // If true - recreate chart instance with no animation
title?: string;
className?: string;
"data-testid"?: string;
}
export enum ChartKind {
@@ -212,25 +213,26 @@ export class Chart extends React.Component<ChartProps> {
}
render() {
const { width, height, showChart, title, className } = this.props;
const { width, height, showChart, title, className, "data-testid": dataTestId } = this.props;
return (
<>
<div className={cssNames("Chart", className)}>
{title && <div className="chart-title">{title}</div>}
{showChart && (
<div className="chart-container">
<canvas
ref={this.canvas}
width={width}
height={height}
/>
<div className="chartjs-tooltip flex column"></div>
</div>
)}
{this.renderLegend()}
</div>
</>
<div
className={cssNames("Chart", className)}
data-testid={dataTestId}
>
{title && <div className="chart-title">{title}</div>}
{showChart && (
<div className="chart-container">
<canvas
ref={this.canvas}
width={width}
height={height}
/>
<div className="chartjs-tooltip flex column"></div>
</div>
)}
{this.renderLegend()}
</div>
);
}
}
+11 -17
View File
@@ -11,9 +11,10 @@ import ChartJS from "chart.js";
import type { ChartProps } from "./chart";
import { Chart } from "./chart";
import { cssNames } from "../../utils";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import type { IComputedValue } from "mobx";
import activeThemeInjectable from "../../themes/active.injectable";
export interface PieChartProps extends ChartProps {
}
@@ -44,18 +45,18 @@ function getCutout(length: number | undefined): number {
}
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
const NonInjectedPieChart = observer(({
themeStore,
activeTheme,
data,
className,
options,
showChart,
...chartProps
}: Dependencies & PieChartProps) => {
const { contentColor } = themeStore.activeTheme.colors;
const { contentColor } = activeTheme.get().colors;
const opts: ChartOptions = {
maintainAspectRatio: false,
tooltips: {
@@ -68,18 +69,11 @@ const NonInjectedPieChart = observer(({
const total = datasetData.reduce((acc, cur) => acc + cur, 0);
const percent = Math.round((datasetData[tooltipItem.index] as number / total) * 100);
const percentLabel = isNaN(percent) ? "N/A" : `${percent}%`;
const tooltipLabel = dataset.tooltipLabels?.[tooltipItem.index];
let tooltip = `${dataset.label}: ${percentLabel}`;
const tooltipLabelCustomizer = dataset.tooltipLabels?.[tooltipItem.index];
if (tooltipLabel) {
if (typeof tooltipLabel === "function") {
tooltip = tooltipLabel(percentLabel);
} else {
tooltip = tooltipLabel;
}
}
return tooltip;
return tooltipLabelCustomizer
? tooltipLabelCustomizer(percentLabel)
: `${dataset.label}: ${percentLabel}`;
},
},
filter: ({ datasetIndex, index }, { datasets = [] }) => {
@@ -120,7 +114,7 @@ const NonInjectedPieChart = observer(({
export const PieChart = withInjectables<Dependencies, PieChartProps>(NonInjectedPieChart, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
});
@@ -57,6 +57,8 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
getPodsByOwnerId: jest.fn(),
searchStore: new SearchStore(),
areLogsPresent: jest.fn(),
downloadLogs: jest.fn(),
downloadAllLogs: jest.fn(),
...deps,
});
}
@@ -31,6 +31,8 @@ function mockLogTabViewModel(tabId: TabId, deps: Partial<LogTabViewModelDependen
getPodsByOwnerId: jest.fn(),
areLogsPresent: jest.fn(),
searchStore: new SearchStore(),
downloadLogs: jest.fn(),
downloadAllLogs: jest.fn(),
...deps,
});
}
@@ -0,0 +1,11 @@
.controls {
@include hidden-scrollbar;
display: flex;
gap: var(--padding);
align-items: center;
justify-content: space-between;
flex-flow: row wrap;
background: var(--dockInfoBackground);
padding: var(--padding) calc(var(--padding) * 2);
}
@@ -1,9 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
.LogControls {
background: var(--dockInfoBackground);
padding: $padding $padding * 2;
}
+10 -33
View File
@@ -3,27 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./controls.scss";
import styles from "./controls.module.scss";
import React from "react";
import { observer } from "mobx-react";
import React from "react";
import { cssNames } from "../../../utils";
import { Checkbox } from "../../checkbox";
import { Icon } from "../../icon";
import { DownloadLogsDropdown } from "./download-logs-dropdown";
import type { LogTabViewModel } from "./logs-view-model";
import { withInjectables } from "@ogre-tools/injectable-react";
import openSaveFileDialogInjectable from "../../../utils/save-file.injectable";
export interface LogControlsProps {
model: LogTabViewModel;
}
interface Dependencies {
openSaveFileDialog: (filename: string, contents: BlobPart | BlobPart[], type: string) => void;
}
const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependencies & LogControlsProps) => {
export const LogControls = observer(({ model }: LogControlsProps) => {
const tabData = model.logTabData.get();
const pod = model.pod.get();
@@ -44,18 +37,9 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
model.reloadLogs();
};
const downloadLogs = () => {
const fileName = pod.getName();
const logsToDownload: string[] = showTimestamps
? model.logs.get()
: model.logsWithoutTimestamps.get();
openSaveFileDialog(`${fileName}.log`, logsToDownload.join("\n"), "text/plain");
};
return (
<div className={cssNames("LogControls flex gaps align-center justify-space-between wrap")}>
<div className="time-range">
<div className={styles.controls} data-testid="log-controls">
<div>
{since && (
<span>
Logs from
@@ -77,20 +61,13 @@ const NonInjectedLogControls = observer(({ openSaveFileDialog, model }: Dependen
onChange={togglePrevious}
className="show-previous"
/>
<Icon
material="get_app"
onClick={downloadLogs}
tooltip="Download"
className="download-icon"
<DownloadLogsDropdown
downloadVisibleLogs={model.downloadLogs}
downloadAllLogs={model.downloadAllLogs}
/>
</div>
</div>
);
});
export const LogControls = withInjectables<Dependencies, LogControlsProps>(NonInjectedLogControls, {
getProps: (di, props) => ({
openSaveFileDialog: di.inject(openSaveFileDialogInjectable),
...props,
}),
});
@@ -6,20 +6,21 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { DockTabCreate, DockTab, TabId } from "../dock/store";
import { TabKind } from "../dock/store";
import type { LogTabData } from "./tab-store";
import * as uuid from "uuid";
import { runInAction } from "mobx";
import createDockTabInjectable from "../dock/create-dock-tab.injectable";
import setLogTabDataInjectable from "./set-log-tab-data.injectable";
import getRandomIdForPodLogsTabInjectable from "./get-random-id-for-pod-logs-tab.injectable";
export type CreateLogsTabData = Pick<LogTabData, "owner" | "selectedPodId" | "selectedContainer" | "namespace"> & Omit<Partial<LogTabData>, "owner" | "selectedPodId" | "selectedContainer" | "namespace">;
interface Dependencies {
createDockTab: (rawTabDesc: DockTabCreate, addNumber?: boolean) => DockTab;
setLogTabData: (tabId: string, data: LogTabData) => void;
getRandomId: () => string;
}
const createLogsTab = ({ createDockTab, setLogTabData }: Dependencies) => (title: string, data: CreateLogsTabData): TabId => {
const id = `log-tab-${uuid.v4()}`;
const createLogsTab = ({ createDockTab, setLogTabData, getRandomId }: Dependencies) => (title: string, data: CreateLogsTabData): TabId => {
const id = `log-tab-${getRandomId()}`;
runInAction(() => {
createDockTab({
@@ -43,6 +44,7 @@ const createLogsTabInjectable = getInjectable({
instantiate: (di) => createLogsTab({
createDockTab: di.inject(createDockTabInjectable),
setLogTabData: di.inject(setLogTabDataInjectable),
getRandomId: di.inject(getRandomIdForPodLogsTabInjectable),
}),
});
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { PodLogsQuery } from "../../../../common/k8s-api/endpoints";
import type { ResourceDescriptor } from "../../../../common/k8s-api/kube-api";
import loggerInjectable from "../../../../common/logger.injectable";
import openSaveFileDialogInjectable from "../../../utils/save-file.injectable";
import callForLogsInjectable from "./call-for-logs.injectable";
const downloadAllLogsInjectable = getInjectable({
id: "download-all-logs",
instantiate: (di) => {
const callForLogs = di.inject(callForLogsInjectable);
const openSaveFileDialog = di.inject(openSaveFileDialogInjectable);
const logger = di.inject(loggerInjectable);
return async (params: ResourceDescriptor, query: PodLogsQuery) => {
const logs = await callForLogs(params, query).catch(error => {
logger.error("Can't download logs: ", error);
});
if (logs) {
openSaveFileDialog(`${params.name}.log`, logs, "text/plain");
}
};
},
});
export default downloadAllLogsInjectable;
@@ -0,0 +1,37 @@
.dropdown {
--accent-color: var(--colorInfo);
border: 1px solid var(--accent-color);
border-radius: 4px;
color: var(--accent-color);
display: flex;
align-items: center;
padding: calc(var(--padding) / 4) var(--padding);
gap: 6px;
position: relative;
&:disabled {
cursor: progress;
opacity: .7;
}
&:hover::before{
opacity: 0.25;
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--accent-color);
border-color: transparent;
}
&::before {
content: " ";
position: absolute;
background: var(--accent-color);
width: 100%;
height: 100%;
left: 0;
opacity: 0;
transition: opacity 0.1s;
}
}
@@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "./download-logs-dropdown.module.scss";
import React, { useState } from "react";
import { Icon } from "../../icon";
import { MenuItem } from "../../menu";
import { Dropdown } from "../../dropdown/dropdown";
interface DownloadLogsDropdownProps {
downloadVisibleLogs: () => void;
downloadAllLogs: () => Promise<void> | undefined;
}
export function DownloadLogsDropdown({ downloadAllLogs, downloadVisibleLogs }: DownloadLogsDropdownProps) {
const [waiting, setWaiting] = useState(false);
const downloadAll = async () => {
setWaiting(true);
try {
await downloadAllLogs();
} finally {
setWaiting(false);
}
};
return (
<Dropdown
id="download-logs-dropdown"
contentForToggle={(
<button
data-testid="download-logs-dropdown"
className={styles.dropdown}
disabled={waiting}
>
Download
<Icon material="arrow_drop_down" smallest/>
</button>
)}
>
<MenuItem onClick={downloadVisibleLogs} data-testid="download-visible-logs">
Visible logs
</MenuItem>
<MenuItem onClick={downloadAll} data-testid="download-all-logs">
All logs
</MenuItem>
</Dropdown>
);
}
@@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import openSaveFileDialogInjectable from "../../../utils/save-file.injectable";
const downloadLogsInjectable = getInjectable({
id: "download-logs",
instantiate: (di) => {
const openSaveFileDialog = di.inject(openSaveFileDialogInjectable);
return (filename: string, logs: string[]) => {
openSaveFileDialog(filename, logs.join("\n"), "text/plain");
};
},
});
export default downloadLogsInjectable;
@@ -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 } from "@ogre-tools/injectable";
import getRandomIdInjectable from "../../../../common/utils/get-random-id.injectable";
const getRandomIdForPodLogsTabInjectable = getInjectable({
id: "get-random-id-for-pod-logs-tab",
instantiate: (di) => di.inject(getRandomIdInjectable),
});
export default getRandomIdForPodLogsTabInjectable;
@@ -18,6 +18,8 @@ import areLogsPresentInjectable from "./are-logs-present.injectable";
import searchStoreInjectable from "../../../search-store/search-store.injectable";
import getPodsByOwnerIdInjectable from "../../+workloads-pods/get-pods-by-owner-id.injectable";
import getPodByIdInjectable from "../../+workloads-pods/get-pod-by-id.injectable";
import downloadLogsInjectable from "./download-logs.injectable";
import downloadAllLogsInjectable from "./download-all-logs.injectable";
export interface InstantiateArgs {
tabId: TabId;
@@ -39,6 +41,8 @@ const logsViewModelInjectable = getInjectable({
areLogsPresent: di.inject(areLogsPresentInjectable),
getPodById: di.inject(getPodByIdInjectable),
getPodsByOwnerId: di.inject(getPodsByOwnerIdInjectable),
downloadLogs: di.inject(downloadLogsInjectable),
downloadAllLogs: di.inject(downloadAllLogsInjectable),
searchStore: di.inject(searchStoreInjectable),
}),
lifecycle: lifecycleEnum.transient,
@@ -7,12 +7,13 @@ import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import type { TabId } from "../dock/store";
import type { SearchStore } from "../../../search-store/search-store";
import type { Pod } from "../../../../common/k8s-api/endpoints";
import type { Pod, PodLogsQuery } from "../../../../common/k8s-api/endpoints";
import { isDefined } from "../../../utils";
import assert from "assert";
import type { GetPodById } from "../../+workloads-pods/get-pod-by-id.injectable";
import type { GetPodsByOwnerId } from "../../+workloads-pods/get-pods-by-owner-id.injectable";
import type { LoadLogs } from "./load-logs.injectable";
import type { ResourceDescriptor } from "../../../../common/k8s-api/kube-api";
export interface LogTabViewModelDependencies {
getLogs: (tabId: TabId) => string[];
@@ -27,6 +28,8 @@ export interface LogTabViewModelDependencies {
getPodById: GetPodById;
getPodsByOwnerId: GetPodsByOwnerId;
areLogsPresent: (tabId: TabId) => boolean;
downloadLogs: (filename: string, logs: string[]) => void;
downloadAllLogs: (params: ResourceDescriptor, query: PodLogsQuery) => Promise<void>;
searchStore: SearchStore;
}
@@ -77,4 +80,32 @@ export class LogTabViewModel {
reloadLogs = () => this.dependencies.reloadLogs(this.tabId, this.pod, this.logTabData);
renameTab = (title: string) => this.dependencies.renameTab(this.tabId, title);
stopLoadingLogs = () => this.dependencies.stopLoadingLogs(this.tabId);
downloadLogs = () => {
const pod = this.pod.get();
const tabData = this.logTabData.get();
if (pod && tabData) {
const fileName = pod.getName();
const logsToDownload: string[] = tabData.showTimestamps
? this.logs.get()
: this.logsWithoutTimestamps.get();
this.dependencies.downloadLogs(`${fileName}.log`, logsToDownload);
}
};
downloadAllLogs = () => {
const pod = this.pod.get();
const tabData = this.logTabData.get();
if (pod && tabData) {
const params = { name: pod.getName(), namespace: pod.getNs() };
const query = { timestamps: tabData.showTimestamps, previous: tabData.showPrevious };
return this.dependencies.downloadAllLogs(params, query);
}
return;
};
}
@@ -10,13 +10,14 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { cssNames } from "../../../utils";
import type { Terminal } from "./terminal";
import type { TerminalStore } from "./store";
import type { ThemeStore } from "../../../themes/store";
import type { LensTheme } from "../../../themes/store";
import type { DockTab, DockStore } from "../dock/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import dockStoreInjectable from "../dock/store.injectable";
import terminalStoreInjectable from "./store.injectable";
import assert from "assert";
import themeStoreInjectable from "../../../themes/store.injectable";
import activeThemeInjectable from "../../../themes/active.injectable";
import type { IComputedValue } from "mobx";
export interface TerminalWindowProps {
tab: DockTab;
@@ -25,7 +26,7 @@ export interface TerminalWindowProps {
interface Dependencies {
dockStore: DockStore;
terminalStore: TerminalStore;
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
@observer
@@ -68,7 +69,7 @@ class NonInjectedTerminalWindow extends React.Component<TerminalWindowProps & De
render() {
return (
<div
className={cssNames("TerminalWindow", this.props.themeStore.activeTheme.type)}
className={cssNames("TerminalWindow", this.props.activeTheme.get().type)}
ref={elem => this.elem = elem}
/>
);
@@ -80,7 +81,7 @@ export const TerminalWindow = withInjectables<Dependencies, TerminalWindowProps>
...props,
dockStore: di.inject(dockStoreInjectable),
terminalStore: di.inject(terminalStoreInjectable),
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
});
@@ -0,0 +1,38 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { HTMLAttributes } from "react";
import React, { useState } from "react";
import { Menu } from "../menu";
interface DropdownProps extends HTMLAttributes<HTMLDivElement> {
contentForToggle: React.ReactNode;
}
export function Dropdown(props: DropdownProps) {
const { id, contentForToggle, children, ...rest } = props;
const [opened, setOpened] = useState(false);
const toggle = () => {
setOpened(!opened);
};
return (
<div {...rest}>
<div id={id}>
{contentForToggle}
</div>
<Menu
usePortal
htmlFor={id}
isOpen={opened}
close={toggle}
open={toggle}
>
{React.Children.toArray(children)}
</Menu>
</div>
);
}
@@ -7,6 +7,7 @@ import "./item-list-layout.scss";
import type { ReactNode } from "react";
import React from "react";
import type { IComputedValue } from "mobx";
import { computed, makeObservable } from "mobx";
import { Observer, observer } from "mobx-react";
import type { ConfirmDialogParams } from "../confirm-dialog";
@@ -20,18 +21,18 @@ import { NoItems } from "../no-items";
import { Spinner } from "../spinner";
import type { ItemObject } from "../../../common/item.store";
import type { Filter, PageFiltersStore } from "./page-filters/store";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { MenuActions } from "../menu/menu-actions";
import { MenuItem } from "../menu";
import { Checkbox } from "../checkbox";
import type { UserStore } from "../../../common/user-store";
import type { ItemListStore } from "./list-layout";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import pageFiltersStoreInjectable from "./page-filters/store.injectable";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
import activeThemeInjectable from "../../themes/active.injectable";
export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStores extends boolean> {
getFilters: () => Filter[];
@@ -71,7 +72,7 @@ export interface ItemListLayoutContentProps<Item extends ItemObject, PreLoadStor
}
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
userStore: UserStore;
pageFiltersStore: PageFiltersStore;
openConfirmDialog: OpenConfirmDialog;
@@ -291,10 +292,10 @@ class NonInjectedItemListLayoutContent<
render() {
const {
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks,
detailsItem, className, tableProps = {}, tableId, getItems, themeStore,
detailsItem, className, tableProps = {}, tableId, getItems, activeTheme,
} = this.props;
const selectedItemId = detailsItem && detailsItem.getId();
const classNames = cssNames(className, "box", "grow", themeStore.activeTheme.type);
const classNames = cssNames(className, "box", "grow", activeTheme.get().type);
const items = getItems();
const selectedItems = store.pickOnlySelected(items);
@@ -377,7 +378,7 @@ class NonInjectedItemListLayoutContent<
export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject, boolean>>(NonInjectedItemListLayoutContent, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
userStore: di.inject(userStoreInjectable),
pageFiltersStore: di.inject(pageFiltersStoreInjectable),
openConfirmDialog: di.inject(openConfirmDialogInjectable),
@@ -126,6 +126,7 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
<button
class="Button ok primary"
data-testid="confirm"
data-waiting="false"
type="button"
>
Remove
@@ -256,6 +257,7 @@ exports[`kube-object-menu given kube object when rerendered with different kube
<button
class="Button ok primary"
data-testid="confirm"
data-waiting="false"
type="button"
>
Remove
@@ -350,6 +352,7 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
<button
class="Button ok primary"
data-testid="confirm"
data-waiting="false"
type="button"
>
Remove
@@ -444,6 +447,7 @@ exports[`kube-object-menu given kube object without namespace when removing kube
<button
class="Button ok primary"
data-testid="confirm"
data-waiting="false"
type="button"
>
Remove
@@ -6,6 +6,7 @@
import styles from "./monaco-editor.module.scss";
import React from "react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import { action, computed, makeObservable, observable, reaction } from "mobx";
import { editor, Uri } from "monaco-editor";
import type { MonacoTheme } from "./monaco-themes";
@@ -13,10 +14,10 @@ import { type MonacoValidator, monacoValidators } from "./monaco-validators";
import { debounce, merge } from "lodash";
import { autoBind, cssNames, disposer } from "../../utils";
import type { UserStore } from "../../../common/user-store";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import activeThemeInjectable from "../../themes/active.injectable";
export type MonacoEditorId = string;
@@ -39,8 +40,8 @@ export interface MonacoEditorProps {
}
interface Dependencies {
themeStore: ThemeStore;
userStore: UserStore;
activeTheme: IComputedValue<LensTheme>;
}
export function createMonacoUri(id: MonacoEditorId): Uri {
@@ -83,7 +84,7 @@ class NonInjectedMonacoEditor extends React.Component<MonacoEditorProps & Depend
}
@computed get theme() {
return this.props.theme ?? this.props.themeStore.activeTheme.monacoTheme;
return this.props.theme ?? this.props.activeTheme.get().monacoTheme;
}
@computed get model(): editor.ITextModel {
@@ -306,8 +307,8 @@ export const MonacoEditor = withInjectables<Dependencies, MonacoEditorProps, Mon
{
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
userStore: di.inject(userStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
},
);
@@ -19,19 +19,15 @@ export function ResourceMetricsText({ metrics }: ResourceMetricsTextProps) {
return null;
}
const { cpuUsage, cpuRequests, cpuLimits, memoryUsage, memoryRequests, memoryLimits } = getMetricLastPoints(metrics);
const { cpuUsage, memoryUsage } = getMetricLastPoints(metrics);
return (
<>
<DrawerItem name="CPU" labelsOnly>
{cpuUsage > 0 && <Badge label={`Usage: ${cpuUsage.toPrecision(2)}`}/>}
{cpuRequests > 0 && <Badge label={`Requests: ${cpuRequests.toPrecision(2)}`}/>}
{cpuLimits > 0 && <Badge label={`Limits: ${cpuLimits.toPrecision(2)}`}/>}
</DrawerItem>
<DrawerItem name="Memory" labelsOnly>
{memoryUsage > 0 && <Badge label={`Usage: ${bytesToUnits(memoryUsage)}`}/>}
{memoryRequests > 0 && <Badge label={`Requests: ${bytesToUnits(memoryRequests)}`}/>}
{memoryLimits > 0 && <Badge label={`Limits: ${bytesToUnits(memoryLimits)}`}/>}
</DrawerItem>
</>
);
+6 -6
View File
@@ -8,15 +8,15 @@
import "./select.scss";
import React from "react";
import type { ObservableSet } from "mobx";
import type { IComputedValue, ObservableSet } from "mobx";
import { action, computed, makeObservable } from "mobx";
import { observer } from "mobx-react";
import ReactSelect, { components, createFilter } from "react-select";
import type { Props as ReactSelectProps, GroupBase, MultiValue, OptionsOrGroups, PropsValue, SingleValue } from "react-select";
import type { ThemeStore } from "../../themes/store";
import type { LensTheme } from "../../themes/store";
import { autoBind, cssNames } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react";
import themeStoreInjectable from "../../themes/store.injectable";
import activeThemeInjectable from "../../themes/active.injectable";
const { Menu } = components;
@@ -80,7 +80,7 @@ const defaultFilter = createFilter({
});
interface Dependencies {
themeStore: ThemeStore;
activeTheme: IComputedValue<LensTheme>;
}
export function onMultiSelectFor<Value, Option extends SelectOption<Value>, Group extends GroupBase<Option> = GroupBase<Option>>(collection: Set<Value> | ObservableSet<Value>): SelectProps<Value, Option, true, Group>["onChange"] {
@@ -124,7 +124,7 @@ class NonInjectedSelect<
}
@computed get themeClass() {
const themeName = this.props.themeName || this.props.themeStore.activeTheme.type;
const themeName = this.props.themeName || this.props.activeTheme.get().type;
return `theme-${themeName}`;
}
@@ -248,7 +248,7 @@ class NonInjectedSelect<
export const Select = withInjectables<Dependencies, SelectProps<unknown, SelectOption<unknown>, boolean>>(NonInjectedSelect, {
getProps: (di, props) => ({
...props,
themeStore: di.inject(themeStoreInjectable),
activeTheme: di.inject(activeThemeInjectable),
}),
}) as <
Value,
@@ -125,7 +125,7 @@ export interface ApplicationBuilder {
helmCharts: {
navigate: NavigateToHelmCharts;
};
navigateWith: (token: Injectable<() => void, any, void>) => void;
select: {
openMenu: (id: string) => { selectOption: (labelText: string) => void };
selectOption: (menuId: string, labelText: string) => void;
@@ -441,6 +441,13 @@ export const getApplicationBuilder = () => {
},
},
navigateWith: (token) => {
const windowDi = builder.applicationWindow.only.di;
const navigate = windowDi.inject(token);
navigate();
},
setEnvironmentToClusterFrame: () => {
environment = environments.clusterFrame;
@@ -1281,6 +1281,7 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
<main>
<div
class="WorkloadsOverview flex column gaps"
data-testid="page-for-workloads-overview"
>
<div
class="header flex gaps align-center"
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import themeStoreInjectable from "./store.injectable";
const activeThemeInjectable = getInjectable({
id: "active-theme",
instantiate: (di) => {
const store = di.inject(themeStoreInjectable);
return computed(() => store.activeTheme);
},
});
export default activeThemeInjectable;
+2 -2
View File
@@ -3,9 +3,9 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Theme } from "./store";
import type { LensTheme } from "./store";
const lensDarkTheme: Theme = {
const lensDarkTheme: LensTheme = {
"name": "Dark",
"type": "dark",
"description": "Original Lens dark theme",
+2 -2
View File
@@ -2,9 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Theme } from "./store";
import type { LensTheme } from "./store";
const lensLightTheme: Theme = {
const lensLightTheme: LensTheme = {
"name": "Light",
"type": "light",
"description": "Original Lens light theme",
+6 -6
View File
@@ -18,7 +18,7 @@ import assert from "assert";
export type ThemeId = string;
export interface Theme {
export interface LensTheme {
name: string;
type: "dark" | "light";
colors: Record<string, string>;
@@ -39,7 +39,7 @@ interface Dependencies {
export class ThemeStore {
private terminalColorPrefix = "terminal";
#themes = observable.map<ThemeId, Theme>({
#themes = observable.map<ThemeId, LensTheme>({
"lens-dark": lensDarkTheme,
"lens-light": lensLightTheme,
});
@@ -66,9 +66,9 @@ export class ThemeStore {
return this.dependencies.userStore.terminalTheme;
}
private readonly defaultTheme: Theme;
private readonly defaultTheme: LensTheme;
@computed get activeTheme(): Theme {
@computed get activeTheme(): LensTheme {
return this.themes.get(this.activeThemeId) ?? this.defaultTheme;
}
@@ -92,7 +92,7 @@ export class ThemeStore {
}
get themes() {
return this.#themes as ReadonlyDeep<Map<string, Theme>>;
return this.#themes as ReadonlyDeep<Map<string, LensTheme>>;
}
constructor(protected readonly dependencies: Dependencies) {
@@ -130,7 +130,7 @@ export class ThemeStore {
});
}
getThemeById(themeId: ThemeId): Theme | undefined {
getThemeById(themeId: ThemeId): LensTheme | undefined {
return this.themes.get(themeId);
}