1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

fix type errors, most <Select /> errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-04-12 16:55:38 -04:00
parent d55b363df9
commit 595bb715a9
25 changed files with 203 additions and 182 deletions

View File

@ -88,7 +88,7 @@ const colorTheme: PreferenceDescription<string> = {
},
};
const terminalTheme: PreferenceDescription<string | undefined> = {
const terminalTheme: PreferenceDescription<string> = {
fromStore(val) {
return val || "";
},

View File

@ -11,6 +11,12 @@ export function fromEntries<T, Key extends string>(entries: Iterable<readonly [K
return Object.fromEntries(entries) as Record<Key, T>;
}
export function keys<K extends keyof any>(obj: Partial<Record<K, any>>): K[];
export function keys<K extends keyof any>(obj: Record<K, any>): K[] {
return Object.keys(obj) as K[];
}
export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][];

View File

@ -73,7 +73,7 @@ const clusterStub = {
} as unknown as Cluster;
describe("ContextHandler", () => {
let createContextHandler: (cluster: Cluster) => ClusterContextHandler;
let createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });

View File

@ -19,12 +19,13 @@ import type { DiContainer } from "@ogre-tools/injectable";
import { parse } from "url";
import loggerInjectable from "../../common/logger.injectable";
import type { Logger } from "../../common/logger";
import assert from "assert";
console = new Console(process.stdout, process.stderr); // fix mockFS
describe("kubeconfig manager tests", () => {
let clusterFake: Cluster;
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager | undefined;
let di: DiContainer;
let logger: jest.Mocked<Logger>;
@ -92,6 +93,8 @@ describe("kubeconfig manager tests", () => {
it("should create 'temp' kube config with proxy", async () => {
const kubeConfManager = createKubeconfigManager(clusterFake);
assert(kubeConfManager, "should actually create one");
expect(logger.error).not.toBeCalled();
expect(await kubeConfManager.getPath()).toBe(`some-directory-for-temp${path.sep}kubeconfig-foo`);
// this causes an intermittent "ENXIO: no such device or address, read" error
@ -107,6 +110,8 @@ describe("kubeconfig manager tests", () => {
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
const kubeConfManager = createKubeconfigManager(clusterFake);
assert(kubeConfManager, "should actually create one");
const configPath = await kubeConfManager.getPath();
expect(await fse.pathExists(configPath)).toBe(true);

View File

@ -4,3 +4,4 @@
*/
export * from "./catalog-entity-registry";
export * from "./entity-registry";

View File

@ -51,7 +51,12 @@ export class NodeShellSession extends ShellSession {
data: `Error occurred: ${get(error, "response.body.message", error ? String(error) : "unknown error")}`,
});
throw new ShellOpenError("failed to create node pod", error);
throw new ShellOpenError(
"failed to create node pod",
error instanceof Error
? { cause: error }
: undefined,
);
}
const env = await this.getCachedShellEnv();

View File

@ -23,8 +23,8 @@ import { stat } from "fs/promises";
import { getOrInsertWith } from "../../common/utils";
export class ShellOpenError extends Error {
constructor(message: string, public cause: unknown) {
super(`${message}: ${cause}`);
constructor(message: string, options?: ErrorOptions) {
super(`${message}`, options);
this.name = this.constructor.name;
Error.captureStackTrace(this);
}

View File

@ -94,20 +94,6 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
return undefined;
}
private formatQuotaOptionLabel = (quota: string) => {
const iconMaterial = this.getQuotaOptionLabelIconMaterial(quota);
return iconMaterial
? (
<span className="nobr">
<Icon material={iconMaterial} />
{" "}
{quota}
</span>
)
: quota;
};
setQuota = () => {
if (!this.quotaSelectValue) return;
this.quotas[this.quotaSelectValue] = this.quotaInputValue;
@ -194,7 +180,7 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
placeholder="Namespace"
themeName="light"
className="box grow"
onChange={value => this.namespace = value}
onChange={option => this.namespace = option?.namespace ?? null}
/>
<SubTitle title="Values" />
@ -204,11 +190,27 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
className="quota-select"
themeName="light"
placeholder="Select a quota.."
options={Object.keys(this.quotas)}
options={Object.keys(this.quotas).map(quota => ({ quota }))}
isMulti={false}
value={this.quotaSelectValue}
onChange={value => this.quotaSelectValue = value}
formatOptionLabel={this.formatQuotaOptionLabel}
value={(
this.quotaSelectValue
? ({ quota: this.quotaSelectValue })
: null
)}
onChange={option => this.quotaSelectValue = option?.quota ?? null}
formatOptionLabel={({ quota }) => {
const iconMaterial = this.getQuotaOptionLabelIconMaterial(quota);
return iconMaterial
? (
<span className="nobr">
<Icon material={iconMaterial} />
{" "}
{quota}
</span>
)
: quota;
}}
/>
<Input
maxLength={10}

View File

@ -18,7 +18,7 @@ import { SubTitle } from "../layout/sub-title";
import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Select } from "../select";
import { Icon } from "../icon";
import { base64, iter } from "../../utils";
import { base64, iter, object } from "../../utils";
import { Notifications } from "../notifications";
import upperFirst from "lodash/upperFirst";
import { showDetails } from "../kube-detail-params";
@ -27,20 +27,20 @@ import { fromEntries } from "../../../common/utils/objects";
export interface AddSecretDialogProps extends Partial<DialogProps> {
}
interface ISecretTemplateField {
interface SecretTemplateField {
key: string;
value?: string;
required?: boolean;
}
interface ISecretTemplate {
[field: string]: ISecretTemplateField[] | undefined;
annotations?: ISecretTemplateField[];
labels?: ISecretTemplateField[];
data?: ISecretTemplateField[];
interface SecretTemplate {
[field: string]: SecretTemplateField[] | undefined;
annotations?: SecretTemplateField[];
labels?: SecretTemplateField[];
data?: SecretTemplateField[];
}
type ISecretField = keyof ISecretTemplate;
type ISecretField = keyof SecretTemplate;
const dialogState = observable.object({
isOpen: false,
@ -61,7 +61,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
dialogState.isOpen = false;
}
private secretTemplate: Partial<Record<SecretType, ISecretTemplate>> = {
private secretTemplate: Partial<Record<SecretType, SecretTemplate>> = {
[SecretType.Opaque]: {},
[SecretType.ServiceAccountToken]: {
annotations: [
@ -71,8 +71,8 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
},
};
get types() {
return Object.keys(this.secretTemplate) as SecretType[];
private get secretTypeOptions() {
return object.keys(this.secretTemplate).map(type => ({ type }));
}
@observable secret = this.secretTemplate;
@ -89,7 +89,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
AddSecretDialog.close();
};
private getDataFromFields = (fields: ISecretTemplateField[] = [], processValue: (val: string) => string = (val => val)) => {
private getDataFromFields = (fields: SecretTemplateField[] = [], processValue: (val: string) => string = (val => val)) => {
return iter.pipeline(fields)
.filterMap(({ key, value }) => (
value
@ -224,7 +224,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
id="secret-namespace-input"
themeName="light"
value={namespace}
onChange={value => this.namespace = value ?? "default"}
onChange={value => this.namespace = value?.namespace ?? "default"}
/>
</div>
<div className="secret-type">
@ -232,9 +232,9 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
<Select
id="secret-input"
themeName="light"
options={this.types}
value={type}
onChange={value => this.type = value ?? SecretType.Opaque}
options={this.secretTypeOptions}
value={({ type })}
onChange={value => this.type = value?.type ?? SecretType.Opaque}
/>
</div>
</div>

View File

@ -188,7 +188,7 @@ class NonInjectedEvents extends React.Component<Dependencies & EventsProps> {
return [
type, // type of event: "Normal" or "Warning"
{
className: { warning: isWarning },
className: cssNames({ warning: isWarning }),
title: (
<>
<span id={tooltipId}>{message}</span>
@ -209,9 +209,7 @@ class NonInjectedEvents extends React.Component<Dependencies & EventsProps> {
event.getSource(),
event.count,
<KubeObjectAge key="age" object={event} />,
event.lastTimestamp
? <ReactiveDuration key="last-seen" timestamp={event.lastTimestamp} />
: "<unknown>",
<ReactiveDuration key="last-seen" timestamp={event.lastTimestamp} />,
];
}}
/>

View File

@ -31,9 +31,21 @@ interface Dependencies {
themeStore: ThemeStore;
}
const timezoneOptions = moment.tz.names().map(timezone => ({ timezone }));
const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, userStore, themeStore }) => {
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application");
const themeOptions = [
"system",
...themeStore.themes.keys(),
].map(theme => ({ theme }));
const extensionInstallRegistryOptions = ([
"default",
"npmrc",
"custom",
] as const).map(registry => ({ registry }));
const updateChannelOptions = [...updateChannels.keys()].map(channel => ({ channel }));
return (
<Preferences data-testid="application-preferences-page">
@ -42,13 +54,10 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<section id="appearance">
<SubTitle title="Theme" />
<Select
options={[
"system",
...themeStore.themes.keys(),
]}
getOptionLabel={option => themeStore.themes.get(option)?.name ?? "Sync with computer"}
value={userStore.colorTheme}
onChange={value => userStore.colorTheme = value ?? defaultTheme}
options={themeOptions}
getOptionLabel={option => themeStore.themes.get(option.theme)?.name ?? "Sync with computer"}
value={({ theme: userStore.colorTheme })}
onChange={value => userStore.colorTheme = value?.theme ?? defaultTheme}
themeName="lens"
/>
</section>
@ -58,14 +67,10 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<section id="extensionRegistryUrl">
<SubTitle title="Extension Install Registry" />
<Select
options={[
"default",
"npmrc",
"custom",
]}
value={userStore.extensionRegistryUrl.location}
options={extensionInstallRegistryOptions}
value={({ registry: userStore.extensionRegistryUrl.location })}
getOptionLabel={option => {
switch (option) {
switch (option.registry) {
case "custom":
return "Custom Url";
case "default":
@ -75,7 +80,7 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
}
}}
onChange={value => runInAction(() => {
userStore.extensionRegistryUrl.location = value ?? defaultExtensionRegistryUrlLocation;
userStore.extensionRegistryUrl.location = value?.registry ?? defaultExtensionRegistryUrlLocation;
if (userStore.extensionRegistryUrl.location === "custom") {
userStore.extensionRegistryUrl.customUrl = "";
@ -123,9 +128,9 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<section id="update-channel">
<SubTitle title="Update Channel" />
<Select
options={[...updateChannels.keys()]}
value={userStore.updateChannel}
onChange={value => userStore.updateChannel = value ?? defaultUpdateChannel}
options={updateChannelOptions}
value={({ channel: userStore.updateChannel })}
onChange={value => userStore.updateChannel = value?.channel ?? defaultUpdateChannel}
themeName="lens"
/>
</section>
@ -135,10 +140,10 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
<section id="locale">
<SubTitle title="Locale Timezone" />
<Select
options={moment.tz.names()}
value={userStore.localeTimezone}
getOptionLabel={timeZone => moment.tz.zone(timeZone)?.name ?? "<unknown>"}
onChange={value => userStore.localeTimezone = value ?? defaultLocaleTimezone}
options={timezoneOptions}
value={({ timezone: userStore.localeTimezone })}
getOptionLabel={option => option.timezone}
onChange={value => userStore.localeTimezone = value?.timezone ?? defaultLocaleTimezone}
themeName="lens"
/>
</section>

View File

@ -22,6 +22,14 @@ interface Dependencies {
const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
const editorConfiguration = userStore.editorConfiguration;
const minimapPositionOptions = (["left", "right"] as const)
.map(side => ({ side }));
const lineNumberOptions = ([
"on",
"off",
"relative",
"interval",
] as const).map(lineNumbers => ({ lineNumbers }));
return (
<Preferences data-testid="editor-preferences-page">
@ -43,9 +51,9 @@ const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
<SubHeader compact>Position</SubHeader>
<Select
themeName="lens"
options={["left", "right"]}
value={editorConfiguration.minimap.side}
onChange={value => editorConfiguration.minimap.side = value ?? undefined}
options={minimapPositionOptions}
value={editorConfiguration.minimap.side ? ({ side: editorConfiguration.minimap.side }) : null}
onChange={value => editorConfiguration.minimap.side = value?.side}
/>
</div>
</div>
@ -54,15 +62,10 @@ const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
<section>
<SubTitle title="Line numbers"/>
<Select
options={[
"on",
"off",
"relative",
"interval",
]}
getOptionLabel={string.uppercaseFirst}
value={editorConfiguration.lineNumbers}
onChange={value => editorConfiguration.lineNumbers = value ?? defaultEditorConfig.lineNumbers}
options={lineNumberOptions}
getOptionLabel={option => string.uppercaseFirst(option.lineNumbers)}
value={{ lineNumbers: editorConfiguration.lineNumbers }}
onChange={value => editorConfiguration.lineNumbers = value?.lineNumbers ?? defaultEditorConfig.lineNumbers}
themeName="lens"
/>
</section>

View File

@ -30,6 +30,7 @@ const NonInjectedKubectlBinaries= observer(({
const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || "");
const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || "");
const pathValidator = downloadPath ? InputValidators.isPath : undefined;
const downloadMirrorOptions = [...packageMirrors].map(([name, mirror]) => ({ name, mirror }));
const save = () => {
userStore.downloadBinariesPath = downloadPath;
@ -52,12 +53,12 @@ const NonInjectedKubectlBinaries= observer(({
<SubTitle title="Download mirror" />
<Select
placeholder="Download mirror for kubectl"
options={[...packageMirrors.keys()]}
value={userStore.downloadMirror}
onChange={value => userStore.downloadMirror = value ?? defaultPackageMirror}
getOptionLabel={mirrorKey => packageMirrors.get(mirrorKey)?.label ?? "<unknown>"}
options={downloadMirrorOptions}
value={downloadMirrorOptions.find(opt => opt.name === userStore.downloadMirror)}
onChange={option => userStore.downloadMirror = option?.name ?? defaultPackageMirror}
getOptionLabel={option => option.mirror.label}
isDisabled={!userStore.downloadKubectlBinaries}
isOptionDisabled={mirrorKey => !packageMirrors.get(mirrorKey)?.platforms.has(process.platform)}
isOptionDisabled={option => option.mirror.platforms.has(process.platform)}
themeName="lens"
/>
</section>

View File

@ -24,6 +24,11 @@ interface Dependencies {
}
const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => {
const themeOptions = [
"",
...themeStore.themes.keys(),
].map(name => ({ name }));
return (
<Preferences data-testid="terminal-preferences-page">
<section>
@ -53,12 +58,9 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D
<SubTitle title="Terminal theme" />
<Select
themeName="lens"
options={[
"",
...themeStore.themes.keys(),
]}
getOptionLabel={themeName => {
const theme = themeStore.themes.get(themeName);
options={themeOptions}
getOptionLabel={option => {
const theme = themeStore.themes.get(option.name);
if (theme) {
return theme.name;
@ -66,8 +68,8 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D
return "Match System Theme";
}}
value={userStore.terminalTheme}
onChange={value => userStore.terminalTheme = value ?? ""}
value={{ name: userStore.terminalTheme }}
onChange={option => userStore.terminalTheme = option?.name ?? ""}
/>
</section>

View File

@ -11,8 +11,8 @@ import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { ClusterRoleBindingDialog } from "./dialog";
import { clusterRoleBindingStore } from "./legacy-store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
import { clusterRoleStore } from "../+cluster-roles/legacy-store";
import { serviceAccountStore } from "../+service-accounts/legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age";
@ -33,7 +33,7 @@ export class ClusterRoleBindings extends React.Component {
tableId="access_cluster_role_bindings"
className="ClusterRoleBindings"
store={clusterRoleBindingStore}
dependentStores={[clusterRolesStore, serviceAccountsStore]}
dependentStores={[clusterRoleStore, serviceAccountStore]}
sortingCallbacks={{
[columnId.name]: binding => binding.getName(),
[columnId.bindings]: binding => binding.getSubjectNames(),

View File

@ -10,9 +10,9 @@ import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { RoleBindingDialog } from "./dialog";
import { roleBindingStore } from "./store";
import { rolesStore } from "../+roles/store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
import { roleStore } from "../+roles/legacy-store";
import { clusterRoleStore } from "../+cluster-roles/legacy-store";
import { serviceAccountStore } from "../+service-accounts/legacy-store";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age";
@ -33,7 +33,7 @@ export class RoleBindings extends React.Component {
tableId="access_role_bindings"
className="RoleBindings"
store={roleBindingStore}
dependentStores={[rolesStore, clusterRolesStore, serviceAccountsStore]}
dependentStores={[roleStore, clusterRoleStore, serviceAccountStore]}
sortingCallbacks={{
[columnId.name]: binding => binding.getName(),
[columnId.namespace]: binding => binding.getNs(),

View File

@ -17,7 +17,7 @@ import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard";
import { rolesStore } from "./store";
import { roleStore } from "./legacy-store";
export interface AddRoleDialogProps extends Partial<DialogProps> {
}
@ -49,7 +49,7 @@ export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
createRole = async () => {
try {
const role = await rolesStore.create({ name: this.roleName, namespace: this.namespace });
const role = await roleStore.create({ name: this.roleName, namespace: this.namespace });
showDetails(role.selfLink);
this.reset();
@ -90,7 +90,7 @@ export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
id="add-dialog-namespace-select-input"
themeName="light"
value={this.namespace}
onChange={namespace => this.namespace = namespace ?? "default"}
onChange={option => this.namespace = option?.namespace ?? "default"}
/>
</WizardStep>
</Wizard>

View File

@ -7,7 +7,8 @@ import "./welcome.scss";
import React from "react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import Carousel from "react-material-ui-carousel";
import type { CarouselProps } from "react-material-ui-carousel";
import LegacyCarousel from "react-material-ui-carousel";
import { Icon } from "../icon";
import { productName, slackUrl } from "../../../common/vars";
import { withInjectables } from "@ogre-tools/injectable-react";
@ -18,6 +19,9 @@ import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-b
export const defaultWidth = 320;
// This is to fix some react 18 type errors
const Carousel = LegacyCarousel as React.ComponentType<CarouselProps>;
interface Dependencies {
welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>;
welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>;

View File

@ -10,11 +10,11 @@ import capitalize from "lodash/capitalize";
import { observer } from "mobx-react";
import type { DatasetTooltipLabel, PieChartData } from "../chart";
import { PieChart } from "../chart";
import { cssVar } from "../../utils";
import { cssVar, object } from "../../utils";
import { ThemeStore } from "../../theme.store";
export interface OverviewWorkloadStatusProps {
status: Record<string, number>;
status: Partial<Record<string, number>>;
}
@observer
@ -32,7 +32,7 @@ export class OverviewWorkloadStatus extends React.Component<OverviewWorkloadStat
datasets: [],
};
const statuses = Object.entries(this.props.status).filter(([, val]) => val > 0);
const statuses = object.entries(this.props.status).filter(([, val]) => val > 0);
if (statuses.length === 0) {
chartData.datasets.push({

View File

@ -10,7 +10,7 @@ import { Icon } from "../../icon/icon";
import { Button } from "../../button/button";
import { SubTitle } from "../../layout/sub-title";
import type { Cluster } from "../../../../common/cluster/cluster";
import { observable, reaction, makeObservable } from "mobx";
import { observable, reaction, makeObservable, runInAction } from "mobx";
import { ClusterMetricsResourceType } from "../../../../common/cluster-types";
export interface ClusterMetricsSettingProps {
@ -40,21 +40,8 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
this.props.cluster.preferences.hiddenMetrics = Array.from(this.hiddenMetrics);
};
onChangeSelect = (values: readonly ClusterMetricsResourceType[]) => {
for (const value of values) {
if (this.hiddenMetrics.has(value)) {
this.hiddenMetrics.delete(value);
} else {
this.hiddenMetrics.add(value);
}
}
this.save();
};
onChangeButton = () => {
Object.keys(ClusterMetricsResourceType).map(value =>
this.hiddenMetrics.add(value),
);
this.hiddenMetrics.replace(Object.keys(ClusterMetricsResourceType));
this.save();
};
@ -63,20 +50,9 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
this.save();
};
formatOptionLabel = (resource: ClusterMetricsResourceType) => (
<div className="flex gaps align-center">
<span>{resource}</span>
{this.hiddenMetrics.has(resource) && (
<Icon
smallest
material="check"
className="box right"
/>
)}
</div>
);
renderMetricsSelect() {
const metricResourceTypeOptions = Object.values(ClusterMetricsResourceType)
.map(type => ({ type }));
return (
<>
@ -89,9 +65,26 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
onMenuClose={this.save}
closeMenuOnSelect={false}
controlShouldRenderValue={false}
options={Object.values(ClusterMetricsResourceType)}
onChange={this.onChangeSelect}
formatOptionLabel={this.formatOptionLabel}
options={metricResourceTypeOptions}
onChange={(options) => {
runInAction(() => {
this.hiddenMetrics.replace(options.map(opt => opt.type));
this.save();
});
}}
getOptionLabel={option => option.type}
formatOptionLabel={(option) => (
<div className="flex gaps align-center">
<span>{option.type}</span>
{this.hiddenMetrics.has(option.type) && (
<Icon
smallest
material="check"
className="box right"
/>
)}
</div>
)}
themeName="lens"
/>
<Button

View File

@ -22,20 +22,23 @@ export interface ClusterPrometheusSettingProps {
const autoDetectPrometheus = Symbol("auto-detect-prometheus");
type ProviderOption = typeof autoDetectPrometheus | string;
interface ProviderOption {
provider: typeof autoDetectPrometheus | string;
}
@observer
export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusSettingProps> {
@observable path = "";
@observable provider: ProviderOption = autoDetectPrometheus;
@observable selectedOption = this.options.find(opt => opt.provider === autoDetectPrometheus);
@observable loading = true;
loadedOptions = observable.map<string, MetricProviderInfo>();
@computed get options(): ProviderOption[] {
return [
return ([
autoDetectPrometheus,
...this.loadedOptions.keys(),
];
] as const)
.map(provider => ({ provider }));
}
constructor(props: ClusterPrometheusSettingProps) {
@ -44,11 +47,11 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
}
@computed get canEditPrometheusPath(): boolean {
if (this.provider === autoDetectPrometheus) {
if (!this.selectedOption || this.selectedOption.provider === autoDetectPrometheus) {
return false;
}
return this.loadedOptions.get(this.provider)?.isConfigurable ?? false;
return this.loadedOptions.get(this.selectedOption.provider)?.isConfigurable ?? false;
}
componentDidMount() {
@ -65,9 +68,9 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
}
if (prometheusProvider) {
this.provider = prometheusProvider.type;
this.selectedOption = this.options.find(opt => opt.provider === prometheusProvider.type);
} else {
this.provider = "";
this.selectedOption = undefined;
}
}),
);
@ -81,7 +84,7 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
}
parsePrometheusPath = () => {
if (!this.provider || !this.path) {
if (!this.selectedOption || !this.path) {
return undefined;
}
const parsed = this.path.split(/\/|:/, 3);
@ -100,8 +103,8 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
};
onSaveProvider = () => {
this.props.cluster.preferences.prometheusProvider = typeof this.provider === "string"
? { type: this.provider }
this.props.cluster.preferences.prometheusProvider = typeof this.selectedOption === "string"
? { type: this.selectedOption }
: undefined;
};
@ -121,9 +124,9 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
<>
<Select<ProviderOption, false, GroupBase<ProviderOption>>
id="cluster-prometheus-settings-input"
value={this.provider}
value={this.selectedOption}
onChange={provider => {
this.provider = provider ?? autoDetectPrometheus;
this.selectedOption = provider ?? { provider: autoDetectPrometheus };
this.onSaveProvider();
}}
options={this.options}

View File

@ -18,6 +18,7 @@ import { iter } from "../../utils";
import { withInjectables } from "@ogre-tools/injectable-react";
import registeredCommandsInjectable from "./registered-commands/registered-commands.injectable";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import type { SingleValue } from "react-select";
interface Dependencies {
commands: IComputedValue<Map<string, RegisteredCommand>>;
@ -28,20 +29,14 @@ interface Dependencies {
const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeCommandOverlay }: Dependencies) => {
const [searchValue, setSearchValue] = useState("");
const executeAction = (commandId: string | null) => {
if (!commandId) {
return;
}
const command = commands.get().get(commandId);
if (!command) {
const executeAction = (option: SingleValue<typeof activeCommands[number]>) => {
if (!option) {
return;
}
try {
closeCommandOverlay();
command.action({
option.action({
entity: activeEntity,
navigate: (url, opts = {}) => {
const { forceRootFrame = false } = opts;
@ -54,7 +49,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
},
});
} catch (error) {
console.error("[COMMAND-DIALOG] failed to execute command", command.id, error);
console.error("[COMMAND-DIALOG] failed to execute command", option.id, error);
}
};
@ -70,8 +65,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
return void console.error(`[COMMAND-DIALOG]: isActive for ${command.id} threw an error, defaulting to false`, error);
}
})
.map(({ id, ...rest }) => [id, rest] as const)
.collect(items => new Map(items));
.collect(items => Array.from(items));
return (
<Select
@ -83,15 +77,12 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
IndicatorSeparator: null,
}}
menuIsOpen
options={Array.from(activeCommands.keys())}
getOptionLabel={commandId => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { title } = activeCommands.get(commandId)!;
return typeof title === "string"
options={activeCommands}
getOptionLabel={({ title }) => (
typeof title === "string"
? title
: title(context);
}}
: title(context)
)}
autoFocus={true}
escapeClearsValue={false}
data-test-id="command-palette-search"

View File

@ -9,7 +9,7 @@ import React from "react";
import { formatDuration } from "../../utils";
export interface ReactiveDurationProps {
timestamp: string;
timestamp: string | undefined;
/**
* Whether the display string should prefer length over precision
@ -34,6 +34,10 @@ function computeUpdateInterval(creationTimestampEpoch: number): number {
}
export const ReactiveDuration = observer(({ timestamp, compact = true }: ReactiveDurationProps) => {
if (!timestamp) {
return <>{"<unknown>"}</>;
}
const creationTimestamp = new Date(timestamp).getTime();
return (

View File

@ -21,12 +21,8 @@ export interface KubeObjectAgeProps {
}
export const KubeObjectAge = ({ object, compact = true }: KubeObjectAgeProps) => (
object.metadata.creationTimestamp
? (
<ReactiveDuration
timestamp={object.metadata.creationTimestamp}
compact={compact}
/>
)
: <>{"<unknown>"}</>
<ReactiveDuration
timestamp={object.metadata.creationTimestamp}
compact={compact}
/>
);

View File

@ -5,12 +5,14 @@
import "./sub-header.scss";
import React from "react";
import type { SingleOrMany } from "../../utils";
import { cssNames } from "../../utils";
export interface SubHeaderProps {
className?: string;
withLine?: boolean; // add bottom line
compact?: boolean; // no extra padding around content
children: SingleOrMany<React.ReactNode>;
}
export class SubHeader extends React.Component<SubHeaderProps> {