mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
more crash fixing
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
16dbc63bba
commit
6a93ef0358
@ -21,6 +21,17 @@ export enum SecretType {
|
||||
BootstrapToken = "bootstrap.kubernetes.io/token",
|
||||
}
|
||||
|
||||
export const reverseSecretTypeMap = {
|
||||
[SecretType.Opaque]: "Opaque",
|
||||
[SecretType.ServiceAccountToken]: "ServiceAccountToken",
|
||||
[SecretType.Dockercfg]: "Dockercfg",
|
||||
[SecretType.DockerConfigJson]: "DockerConfigJson",
|
||||
[SecretType.BasicAuth]: "BasicAuth",
|
||||
[SecretType.SSHAuth]: "SSHAuth",
|
||||
[SecretType.TLS]: "TLS",
|
||||
[SecretType.BootstrapToken]: "BootstrapToken",
|
||||
};
|
||||
|
||||
export interface SecretReference {
|
||||
name: string;
|
||||
namespace?: string;
|
||||
|
||||
24
src/common/k8s-api/selected-filter-namespaces.injectable.ts
Normal file
24
src/common/k8s-api/selected-filter-namespaces.injectable.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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 namespaceStoreInjectable from "../../renderer/components/+namespaces/store.injectable";
|
||||
import { createStoresAndApisInjectionToken } from "./create-stores-apis.token";
|
||||
|
||||
const selectedFilterNamespacesInjectable = getInjectable({
|
||||
id: "selected-filter-namespaces",
|
||||
instantiate: (di) => {
|
||||
if (!di.inject(createStoresAndApisInjectionToken)) {
|
||||
// Dummy value so that this works in all environments
|
||||
return computed(() => []);
|
||||
}
|
||||
|
||||
const store = di.inject(namespaceStoreInjectable);
|
||||
|
||||
return computed(() => [...store.contextNamespaces]);
|
||||
},
|
||||
});
|
||||
|
||||
export default selectedFilterNamespacesInjectable;
|
||||
@ -49,7 +49,6 @@ import * as tuple from "./tuple";
|
||||
import * as base64 from "./base64";
|
||||
import * as object from "./objects";
|
||||
import * as json from "./json";
|
||||
import * as string from "./string";
|
||||
|
||||
export {
|
||||
iter,
|
||||
@ -58,5 +57,4 @@ export {
|
||||
base64,
|
||||
object,
|
||||
json,
|
||||
string,
|
||||
};
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms `val` so that the first character is uppercased
|
||||
*/
|
||||
export function uppercaseFirst(val: string): string {
|
||||
if (val.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (val.length === 1) {
|
||||
return val.toUpperCase();
|
||||
}
|
||||
|
||||
const [first, ...rest] = val;
|
||||
|
||||
return first.toUpperCase() + rest;
|
||||
}
|
||||
@ -3,6 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { SetRequired } from "type-fest";
|
||||
|
||||
export type RemoveUndefinedFromValues<K> = {
|
||||
[P in keyof K]: NonNullable<K[P]>;
|
||||
};
|
||||
@ -23,3 +25,7 @@ export type SingleOrMany<T> = T | T[];
|
||||
export type IfEquals<T, U, Y=unknown, N=never> =
|
||||
(<G>() => G extends T ? 1 : 2) extends
|
||||
(<G>() => G extends U ? 1 : 2) ? Y : N;
|
||||
|
||||
export type MaybeSetRequired<BaseType, Keys extends keyof BaseType, Query> = Query extends true
|
||||
? SetRequired<BaseType, Keys>
|
||||
: BaseType;
|
||||
|
||||
@ -180,7 +180,7 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
placeholder="Namespace"
|
||||
themeName="light"
|
||||
className="box grow"
|
||||
onChange={option => this.namespace = option?.namespace ?? null}
|
||||
onChange={option => this.namespace = option?.value ?? null}
|
||||
/>
|
||||
|
||||
<SubTitle title="Values" />
|
||||
@ -190,26 +190,24 @@ export class AddQuotaDialog extends React.Component<AddQuotaDialogProps> {
|
||||
className="quota-select"
|
||||
themeName="light"
|
||||
placeholder="Select a quota.."
|
||||
options={Object.keys(this.quotas).map(quota => ({ quota }))}
|
||||
isMulti={false}
|
||||
value={(
|
||||
this.quotaSelectValue
|
||||
? ({ quota: this.quotaSelectValue })
|
||||
: null
|
||||
)}
|
||||
onChange={option => this.quotaSelectValue = option?.quota ?? null}
|
||||
formatOptionLabel={({ quota }) => {
|
||||
const iconMaterial = this.getQuotaOptionLabelIconMaterial(quota);
|
||||
options={Object.keys(this.quotas).map(quota => ({
|
||||
value: quota,
|
||||
label: quota,
|
||||
}))}
|
||||
value={this.quotaSelectValue}
|
||||
onChange={option => this.quotaSelectValue = option?.value ?? null}
|
||||
formatOptionLabel={({ value }) => {
|
||||
const iconMaterial = this.getQuotaOptionLabelIconMaterial(value);
|
||||
|
||||
return iconMaterial
|
||||
? (
|
||||
<span className="nobr">
|
||||
<Icon material={iconMaterial} />
|
||||
{" "}
|
||||
{quota}
|
||||
{value}
|
||||
</span>
|
||||
)
|
||||
: quota;
|
||||
: value;
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
|
||||
@ -13,7 +13,7 @@ import { Dialog } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Input } from "../input";
|
||||
import { systemName } from "../input/input_validators";
|
||||
import { secretApi, SecretType } from "../../../common/k8s-api/endpoints";
|
||||
import { reverseSecretTypeMap, secretApi, SecretType } from "../../../common/k8s-api/endpoints";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import { Select } from "../select";
|
||||
@ -72,7 +72,10 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
};
|
||||
|
||||
private get secretTypeOptions() {
|
||||
return object.keys(this.secretTemplate).map(type => ({ type }));
|
||||
return object.keys(this.secretTemplate).map(type => ({
|
||||
value: type,
|
||||
label: reverseSecretTypeMap[type],
|
||||
}));
|
||||
}
|
||||
|
||||
@observable secret = this.secretTemplate;
|
||||
@ -224,7 +227,7 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
id="secret-namespace-input"
|
||||
themeName="light"
|
||||
value={namespace}
|
||||
onChange={value => this.namespace = value?.namespace ?? "default"}
|
||||
onChange={option => this.namespace = option?.value ?? "default"}
|
||||
/>
|
||||
</div>
|
||||
<div className="secret-type">
|
||||
@ -233,8 +236,8 @@ export class AddSecretDialog extends React.Component<AddSecretDialogProps> {
|
||||
id="secret-input"
|
||||
themeName="light"
|
||||
options={this.secretTypeOptions}
|
||||
value={({ type })}
|
||||
onChange={value => this.type = value?.type ?? SecretType.Opaque}
|
||||
value={type}
|
||||
onChange={option => this.type = option?.value ?? SecretType.Opaque}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -48,8 +48,17 @@ export class CustomResourceDefinitions extends React.Component {
|
||||
return customResourceDefinitionStore.items; // show all by default
|
||||
}
|
||||
|
||||
toggleSelection = (options: readonly ({ group: string })[]) => {
|
||||
const groups = options.map(({ group }) => group);
|
||||
@computed get groupSelectOptions() {
|
||||
return Object.keys(customResourceDefinitionStore.groups)
|
||||
.map(group => ({
|
||||
value: group,
|
||||
label: group,
|
||||
isSelected: this.selectedGroups.has(group),
|
||||
}));
|
||||
}
|
||||
|
||||
toggleSelection = (options: readonly ({ value: string })[]) => {
|
||||
const groups = options.map(({ value }) => value);
|
||||
|
||||
this.selectedGroups.replace(groups);
|
||||
crdGroupsUrlParam.set(groups);
|
||||
@ -68,8 +77,6 @@ export class CustomResourceDefinitions extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, selectedGroups } = this;
|
||||
|
||||
return (
|
||||
<TabLayout>
|
||||
<KubeObjectListLayout
|
||||
@ -79,7 +86,7 @@ export class CustomResourceDefinitions extends React.Component {
|
||||
store={customResourceDefinitionStore}
|
||||
// Don't subscribe the `customResourceDefinitionStore` because <Sidebar> already has and is always mounted
|
||||
subscribeStores={false}
|
||||
items={items}
|
||||
items={this.items}
|
||||
sortingCallbacks={{
|
||||
[columnId.kind]: crd => crd.getResourceKind(),
|
||||
[columnId.group]: crd => crd.getGroup(),
|
||||
@ -103,17 +110,16 @@ export class CustomResourceDefinitions extends React.Component {
|
||||
<Select
|
||||
className="group-select"
|
||||
placeholder={this.getPlaceholder()}
|
||||
options={Object.keys(customResourceDefinitionStore.groups).map(group => ({ group }))}
|
||||
options={this.groupSelectOptions}
|
||||
onChange={this.toggleSelection}
|
||||
closeMenuOnSelect={false}
|
||||
controlShouldRenderValue={false}
|
||||
isOptionSelected={opt => this.selectedGroups.has(opt.group)}
|
||||
isMulti={true}
|
||||
formatOptionLabel={({ group }) => (
|
||||
formatOptionLabel={({ value, isSelected }) => (
|
||||
<div className="flex gaps align-center">
|
||||
<Icon small material="folder" />
|
||||
<span>{group}</span>
|
||||
{selectedGroups.has(group) && (
|
||||
<span>{value}</span>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
small
|
||||
material="check"
|
||||
|
||||
@ -8,7 +8,7 @@ import "./helm-chart-details.scss";
|
||||
import React, { Component } from "react";
|
||||
import type { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { getChartDetails } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||
import { observable, reaction, runInAction } from "mobx";
|
||||
import { computed, observable, reaction, runInAction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { Drawer, DrawerItem } from "../drawer";
|
||||
import { autoBind, stopPropagation } from "../../utils";
|
||||
@ -21,6 +21,8 @@ import { Tooltip, withStyles } from "@material-ui/core";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import createInstallChartTabInjectable from "../dock/install-chart/create-install-chart-tab.injectable";
|
||||
import { Notifications } from "../notifications";
|
||||
import HelmLogoPlaceholder from "./helm-placeholder.svg";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
export interface HelmChartDetailsProps {
|
||||
chart: HelmChart;
|
||||
@ -42,6 +44,12 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
readonly chartVersions = observable.array<HelmChart>();
|
||||
readonly selectedChart = observable.box<HelmChart | undefined>();
|
||||
readonly readme = observable.box<string | undefined>(undefined);
|
||||
readonly chartVerionOptions = computed(() => (
|
||||
this.chartVersions.map(chart => ({
|
||||
value: chart,
|
||||
label: chart.version,
|
||||
}))
|
||||
));
|
||||
|
||||
private abortController = new AbortController();
|
||||
|
||||
@ -80,8 +88,8 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
]);
|
||||
}
|
||||
|
||||
async onVersionChange(c: HelmChart | null | undefined) {
|
||||
const chart = c ?? this.chartVersions[0];
|
||||
async onVersionChange(option: SingleValue<{ value: HelmChart }>) {
|
||||
const chart = option?.value ?? this.chartVersions[0];
|
||||
|
||||
runInAction(() => {
|
||||
this.selectedChart.set(chart ?? undefined);
|
||||
@ -106,15 +114,12 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
}
|
||||
|
||||
renderIntroduction(selectedChart: HelmChart) {
|
||||
const { chartVersions, onVersionChange } = this;
|
||||
const placeholder = require("./helm-placeholder.svg");
|
||||
|
||||
return (
|
||||
<div className="introduction flex align-flex-start">
|
||||
<img
|
||||
className="intro-logo"
|
||||
src={selectedChart.getIcon() || placeholder}
|
||||
onError={(event) => event.currentTarget.src = placeholder}
|
||||
src={selectedChart.getIcon() || HelmLogoPlaceholder}
|
||||
onError={(event) => event.currentTarget.src = HelmLogoPlaceholder}
|
||||
/>
|
||||
<div className="intro-contents box grow">
|
||||
<div className="description flex align-center justify-space-between">
|
||||
@ -134,9 +139,8 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
id="chart-version-input"
|
||||
themeName="outlined"
|
||||
menuPortalTarget={null}
|
||||
options={chartVersions.slice()}
|
||||
getOptionLabel={chart => chart.version}
|
||||
formatOptionLabel={chart => (
|
||||
options={this.chartVerionOptions.get()}
|
||||
formatOptionLabel={({ value: chart }) => (
|
||||
chart.deprecated
|
||||
? (
|
||||
<LargeTooltip title="Deprecated" placement="left">
|
||||
@ -145,9 +149,9 @@ class NonInjectedHelmChartDetails extends Component<HelmChartDetailsProps & Depe
|
||||
)
|
||||
: chart.version
|
||||
)}
|
||||
isOptionDisabled={chart => chart.deprecated}
|
||||
isOptionDisabled={({ value: chart }) => chart.deprecated}
|
||||
value={selectedChart}
|
||||
onChange={onVersionChange}
|
||||
onChange={this.onVersionChange}
|
||||
/>
|
||||
</DrawerItem>
|
||||
<DrawerItem name="Home">
|
||||
|
||||
@ -11,13 +11,13 @@ import { getChartDetails, listCharts } from "../../../common/k8s-api/endpoints/h
|
||||
import { ItemStore } from "../../../common/item.store";
|
||||
import flatten from "lodash/flatten";
|
||||
|
||||
export interface IChartVersion {
|
||||
export interface ChartVersion {
|
||||
repo: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
@observable versions = observable.map<string, IChartVersion[]>();
|
||||
@observable versions = observable.map<string, ChartVersion[]>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -53,14 +53,14 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
return this.items.find(chart => chart.getName() === name && chart.getRepository() === repo);
|
||||
}
|
||||
|
||||
protected sortVersions = (versions: IChartVersion[]) => {
|
||||
protected sortVersions = (versions: ChartVersion[]) => {
|
||||
return versions
|
||||
.map(chartVersion => ({ ...chartVersion, __version: semver.coerce(chartVersion.version, { includePrerelease: true, loose: true }) }))
|
||||
.sort(sortCompareChartVersions)
|
||||
.map(({ __version, ...chartVersion }) => chartVersion);
|
||||
};
|
||||
|
||||
async getVersions(chartName: string, force?: boolean): Promise<IChartVersion[]> {
|
||||
async getVersions(chartName: string, force?: boolean): Promise<ChartVersion[]> {
|
||||
const versions = this.versions.get(chartName);
|
||||
|
||||
if (versions && !force) {
|
||||
|
||||
@ -7,7 +7,7 @@ import "./dialog.scss";
|
||||
|
||||
import React from "react";
|
||||
import type { IObservableValue } from "mobx";
|
||||
import { observable, runInAction } from "mobx";
|
||||
import { computed, observable, runInAction } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import type { DialogProps } from "../../dialog";
|
||||
import { Dialog } from "../../dialog";
|
||||
@ -34,6 +34,12 @@ class NonInjectedReleaseRollbackDialog extends React.Component<ReleaseRollbackDi
|
||||
readonly isLoading = observable.box(false);
|
||||
readonly revision = observable.box<HelmReleaseRevision | undefined>();
|
||||
readonly revisions = observable.array<HelmReleaseRevision>();
|
||||
readonly revisionOptions = computed(() => (
|
||||
this.revisions.map(revision => ({
|
||||
value: revision,
|
||||
label: `${revision.revision} - ${revision.chart} - ${revision.app_version}, updated: ${new Date(revision.updated).toLocaleString()}`,
|
||||
}))
|
||||
));
|
||||
|
||||
close = () => {
|
||||
this.props.state.set(undefined);
|
||||
@ -79,9 +85,8 @@ class NonInjectedReleaseRollbackDialog extends React.Component<ReleaseRollbackDi
|
||||
<Select
|
||||
themeName="light"
|
||||
value={revision}
|
||||
options={this.revisions}
|
||||
formatOptionLabel={value => `${value.revision} - ${value.chart} - ${value.app_version}, updated: ${new Date(value.updated).toLocaleString()}`}
|
||||
onChange={value => this.revision.set(value ?? undefined)}
|
||||
options={this.revisionOptions.get()}
|
||||
onChange={option => this.revision.set(option?.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -3,11 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import React from "react";
|
||||
import { observable, makeObservable, action, untracked, computed } from "mobx";
|
||||
import { observable, action, untracked, computed } from "mobx";
|
||||
import type { NamespaceStore } from "../store";
|
||||
import { isMac } from "../../../../common/vars";
|
||||
import type { ActionMeta } from "react-select";
|
||||
import { Icon } from "../../icon";
|
||||
import type { SelectOption } from "../../select";
|
||||
|
||||
interface Dependencies {
|
||||
readonly namespaceStore: NamespaceStore;
|
||||
@ -17,19 +18,10 @@ export const selectAllNamespaces = Symbol("all-namespaces-selected");
|
||||
|
||||
export type SelectAllNamespaces = typeof selectAllNamespaces;
|
||||
|
||||
export interface NamespaceSelectFilterOption {
|
||||
namespace: string | SelectAllNamespaces;
|
||||
}
|
||||
export type NamespaceSelectFilterOption = SelectOption<string | SelectAllNamespaces>;
|
||||
|
||||
export class NamespaceSelectFilterModel {
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
makeObservable(this, {
|
||||
menuIsOpen: observable,
|
||||
closeMenu: action,
|
||||
openMenu: action,
|
||||
reset: action,
|
||||
});
|
||||
}
|
||||
constructor(private readonly dependencies: Dependencies) {}
|
||||
|
||||
readonly options = computed((): readonly NamespaceSelectFilterOption[] => {
|
||||
const baseOptions = this.dependencies.namespaceStore.items.map(ns => ns.getName());
|
||||
@ -40,22 +32,29 @@ export class NamespaceSelectFilterModel {
|
||||
- +this.selectedNames.has(left)
|
||||
));
|
||||
|
||||
const res = [selectAllNamespaces, ...baseOptions] as const;
|
||||
|
||||
return res.map(namespace => ({ namespace }));
|
||||
return [
|
||||
{
|
||||
value: selectAllNamespaces,
|
||||
label: "All Namespaces",
|
||||
isSelected: false,
|
||||
},
|
||||
...baseOptions.map(namespace => ({
|
||||
value: namespace,
|
||||
label: namespace,
|
||||
isSelected: this.selectedNames.has(namespace),
|
||||
})),
|
||||
];
|
||||
});
|
||||
|
||||
formatOptionLabel = ({ namespace }: NamespaceSelectFilterOption) => {
|
||||
if (namespace === selectAllNamespaces) {
|
||||
formatOptionLabel = ({ value, isSelected }: NamespaceSelectFilterOption) => {
|
||||
if (value === selectAllNamespaces) {
|
||||
return "All Namespaces";
|
||||
}
|
||||
|
||||
const isSelected = this.isSelected(namespace);
|
||||
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<Icon small material="layers" />
|
||||
<span>{namespace}</span>
|
||||
<span>{value}</span>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
small
|
||||
@ -67,23 +66,15 @@ export class NamespaceSelectFilterModel {
|
||||
);
|
||||
};
|
||||
|
||||
getOptionLabel = ({ namespace }: NamespaceSelectFilterOption) => {
|
||||
if (namespace === selectAllNamespaces) {
|
||||
return "All Namespaces";
|
||||
}
|
||||
readonly menuIsOpen = observable.box(false);
|
||||
|
||||
return namespace;
|
||||
};
|
||||
closeMenu = action(() => {
|
||||
this.menuIsOpen.set(false);
|
||||
});
|
||||
|
||||
menuIsOpen = false;
|
||||
|
||||
closeMenu = () => {
|
||||
this.menuIsOpen = false;
|
||||
};
|
||||
|
||||
openMenu = () => {
|
||||
this.menuIsOpen = true;
|
||||
};
|
||||
openMenu = action(() => {
|
||||
this.menuIsOpen.set(true);
|
||||
});
|
||||
|
||||
get selectedNames() {
|
||||
return untracked(() => this.dependencies.namespaceStore.selectedNames);
|
||||
@ -111,13 +102,13 @@ export class NamespaceSelectFilterModel {
|
||||
}
|
||||
break;
|
||||
case "select-option":
|
||||
if (action.option?.namespace === selectAllNamespaces) {
|
||||
if (action.option?.value === selectAllNamespaces) {
|
||||
this.dependencies.namespaceStore.selectAll();
|
||||
} else if (action.option) {
|
||||
if (this.isMultiSelection) {
|
||||
this.dependencies.namespaceStore.toggleSingle(action.option.namespace);
|
||||
this.dependencies.namespaceStore.toggleSingle(action.option.value);
|
||||
} else {
|
||||
this.dependencies.namespaceStore.selectSingle(action.option.namespace);
|
||||
this.dependencies.namespaceStore.selectSingle(action.option.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -146,10 +137,10 @@ export class NamespaceSelectFilterModel {
|
||||
}
|
||||
};
|
||||
|
||||
reset = () => {
|
||||
reset = action(() => {
|
||||
this.isMultiSelection = false;
|
||||
this.closeMenu();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const isSelectionKey = (event: React.KeyboardEvent): boolean => {
|
||||
|
||||
@ -31,18 +31,17 @@ const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies &
|
||||
onKeyDown={model.onKeyDown}
|
||||
onClick={model.onClick}
|
||||
>
|
||||
<Select<{ namespace: string | SelectAllNamespaces }, true>
|
||||
<Select<string | SelectAllNamespaces, NamespaceSelectFilterOption, true>
|
||||
id={id}
|
||||
isMulti={true}
|
||||
isClearable={false}
|
||||
menuIsOpen={model.menuIsOpen}
|
||||
menuIsOpen={model.menuIsOpen.get()}
|
||||
components={{ Placeholder }}
|
||||
closeMenuOnSelect={false}
|
||||
controlShouldRenderValue={false}
|
||||
onChange={model.onChange}
|
||||
onBlur={model.reset}
|
||||
formatOptionLabel={model.formatOptionLabel}
|
||||
getOptionLabel={model.getOptionLabel}
|
||||
options={model.options.get()}
|
||||
className="NamespaceSelect NamespaceSelectFilter"
|
||||
menuClass="NamespaceSelectFilterMenu"
|
||||
|
||||
@ -18,7 +18,7 @@ import namespaceStoreInjectable from "./store.injectable";
|
||||
|
||||
export type NamespaceSelectSort = (left: string, right: string) => number;
|
||||
|
||||
export interface NamespaceSelectProps<IsMulti extends boolean> extends Omit<SelectProps<{ namespace: string }, IsMulti>, "options" | "value"> {
|
||||
export interface NamespaceSelectProps<IsMulti extends boolean> extends Omit<SelectProps<string, { value: string; label: string }, IsMulti>, "options" | "value"> {
|
||||
showIcons?: boolean;
|
||||
sort?: NamespaceSelectSort;
|
||||
value: string | null | undefined;
|
||||
@ -36,7 +36,10 @@ function getOptions(namespaceStore: NamespaceStore, sort: NamespaceSelectSort |
|
||||
baseOptions.sort(sort);
|
||||
}
|
||||
|
||||
return baseOptions.map(namespace => ({ namespace }));
|
||||
return baseOptions.map(namespace => ({
|
||||
value: namespace,
|
||||
label: namespace,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@ -46,7 +49,6 @@ const NonInjectedNamespaceSelect = observer(({
|
||||
formatOptionLabel,
|
||||
sort,
|
||||
className,
|
||||
value,
|
||||
...selectProps
|
||||
}: Dependencies & NamespaceSelectProps<boolean>) => {
|
||||
const [baseOptions, setBaseOptions] = useState(getOptions(namespaceStore, sort));
|
||||
@ -58,16 +60,14 @@ const NonInjectedNamespaceSelect = observer(({
|
||||
className={cssNames("NamespaceSelect", className)}
|
||||
menuClass="NamespaceSelectMenu"
|
||||
formatOptionLabel={showIcons
|
||||
? ({ namespace }) => (
|
||||
? ({ value }) => (
|
||||
<>
|
||||
<Icon small material="layers" />
|
||||
{namespace}
|
||||
{value}
|
||||
</>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
getOptionLabel={({ namespace }) => namespace}
|
||||
value={value ? ({ namespace: value }) : null}
|
||||
options={baseOptions.get()}
|
||||
{...selectProps}
|
||||
/>
|
||||
|
||||
@ -31,21 +31,44 @@ interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
}
|
||||
|
||||
const timezoneOptions = moment.tz.names().map(timezone => ({ timezone }));
|
||||
const timezoneOptions = moment.tz.names()
|
||||
.map(timezone => ({
|
||||
value: timezone,
|
||||
label: timezone.replace("_", " "),
|
||||
}));
|
||||
const updateChannelOptions = Array.from(updateChannels, ([channel, { label }]) => ({
|
||||
value: channel,
|
||||
label,
|
||||
}));
|
||||
const extensionInstallRegistryOptions = [
|
||||
{
|
||||
value: "default",
|
||||
label: "Default Url",
|
||||
},
|
||||
{
|
||||
value: "npmrc",
|
||||
label: "Global .npmrc file's Url",
|
||||
},
|
||||
{
|
||||
value: "custom",
|
||||
label: "Custom Url",
|
||||
},
|
||||
] as const;
|
||||
|
||||
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 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 }));
|
||||
{
|
||||
value: "system", // TODO: replace with a sentinal value that isn't string (and serialize it differently)
|
||||
label: "Sync with computer",
|
||||
},
|
||||
...Array.from(themeStore.themes, ([themeId, { name }]) => ({
|
||||
value: themeId,
|
||||
label: name,
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<Preferences data-testid="application-preferences-page">
|
||||
@ -55,9 +78,8 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
|
||||
<SubTitle title="Theme" />
|
||||
<Select
|
||||
options={themeOptions}
|
||||
getOptionLabel={option => themeStore.themes.get(option.theme)?.name ?? "Sync with computer"}
|
||||
value={({ theme: userStore.colorTheme })}
|
||||
onChange={value => userStore.colorTheme = value?.theme ?? defaultTheme}
|
||||
value={userStore.colorTheme}
|
||||
onChange={value => userStore.colorTheme = value?.value ?? defaultTheme}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
@ -68,19 +90,9 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
|
||||
<SubTitle title="Extension Install Registry" />
|
||||
<Select
|
||||
options={extensionInstallRegistryOptions}
|
||||
value={({ registry: userStore.extensionRegistryUrl.location })}
|
||||
getOptionLabel={option => {
|
||||
switch (option.registry) {
|
||||
case "custom":
|
||||
return "Custom Url";
|
||||
case "default":
|
||||
return "Default Url";
|
||||
case "npmrc":
|
||||
return "Global .npmrc file's Url";
|
||||
}
|
||||
}}
|
||||
value={userStore.extensionRegistryUrl.location}
|
||||
onChange={value => runInAction(() => {
|
||||
userStore.extensionRegistryUrl.location = value?.registry ?? defaultExtensionRegistryUrlLocation;
|
||||
userStore.extensionRegistryUrl.location = value?.value ?? defaultExtensionRegistryUrlLocation;
|
||||
|
||||
if (userStore.extensionRegistryUrl.location === "custom") {
|
||||
userStore.extensionRegistryUrl.customUrl = "";
|
||||
@ -129,8 +141,8 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
|
||||
<SubTitle title="Update Channel" />
|
||||
<Select
|
||||
options={updateChannelOptions}
|
||||
value={({ channel: userStore.updateChannel })}
|
||||
onChange={value => userStore.updateChannel = value?.channel ?? defaultUpdateChannel}
|
||||
value={userStore.updateChannel}
|
||||
onChange={value => userStore.updateChannel = value?.value ?? defaultUpdateChannel}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
@ -141,9 +153,8 @@ const NonInjectedApplication: React.FC<Dependencies> = ({ appPreferenceItems, us
|
||||
<SubTitle title="Locale Timezone" />
|
||||
<Select
|
||||
options={timezoneOptions}
|
||||
value={({ timezone: userStore.localeTimezone })}
|
||||
getOptionLabel={option => option.timezone}
|
||||
onChange={value => userStore.localeTimezone = value?.timezone ?? defaultLocaleTimezone}
|
||||
value={userStore.localeTimezone}
|
||||
onChange={value => userStore.localeTimezone = value?.value ?? defaultLocaleTimezone}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
|
||||
@ -13,23 +13,30 @@ import { Input, InputValidators } from "../input";
|
||||
import { Preferences } from "./preferences";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
|
||||
import { string } from "../../utils";
|
||||
import { defaultEditorConfig } from "../../../common/user-store/preferences-helpers";
|
||||
import { capitalize } from "lodash";
|
||||
|
||||
interface Dependencies {
|
||||
userStore: UserStore;
|
||||
}
|
||||
|
||||
const minimapPositionOptions = (["left", "right"] as const)
|
||||
.map(side => ({
|
||||
value: side,
|
||||
label: side,
|
||||
}));
|
||||
const lineNumberOptions = ([
|
||||
"on",
|
||||
"off",
|
||||
"relative",
|
||||
"interval",
|
||||
] as const).map(lineNumbers => ({
|
||||
value: lineNumbers,
|
||||
label: capitalize(lineNumbers),
|
||||
}));
|
||||
|
||||
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">
|
||||
@ -52,8 +59,8 @@ const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
|
||||
<Select
|
||||
themeName="lens"
|
||||
options={minimapPositionOptions}
|
||||
value={editorConfiguration.minimap.side ? ({ side: editorConfiguration.minimap.side }) : null}
|
||||
onChange={value => editorConfiguration.minimap.side = value?.side}
|
||||
value={editorConfiguration.minimap.side}
|
||||
onChange={option => editorConfiguration.minimap.side = option?.value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -63,9 +70,8 @@ const NonInjectedEditor = observer(({ userStore }: Dependencies) => {
|
||||
<SubTitle title="Line numbers"/>
|
||||
<Select
|
||||
options={lineNumberOptions}
|
||||
getOptionLabel={option => string.uppercaseFirst(option.lineNumbers)}
|
||||
value={{ lineNumbers: editorConfiguration.lineNumbers }}
|
||||
onChange={value => editorConfiguration.lineNumbers = value?.lineNumbers ?? defaultEditorConfig.lineNumbers}
|
||||
value={editorConfiguration.lineNumbers}
|
||||
onChange={option => editorConfiguration.lineNumbers = option?.value ?? defaultEditorConfig.lineNumbers}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
|
||||
@ -6,13 +6,14 @@
|
||||
import styles from "./helm-charts.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { observable, makeObservable, computed } from "mobx";
|
||||
|
||||
import type { HelmRepo } from "../../../main/helm/helm-repo-manager";
|
||||
import { HelmRepoManager } from "../../../main/helm/helm-repo-manager";
|
||||
import { Button } from "../button";
|
||||
import { Icon } from "../icon";
|
||||
import { Notifications } from "../notifications";
|
||||
import type { SelectOption } from "../select";
|
||||
import { Select } from "../select";
|
||||
import { AddHelmRepoDialog } from "./add-helm-repo-dialog";
|
||||
import { observer } from "mobx-react";
|
||||
@ -20,6 +21,7 @@ import { RemovableItem } from "./removable-item";
|
||||
import { Notice } from "../+extensions/notice";
|
||||
import { Spinner } from "../spinner";
|
||||
import { noop } from "../../utils";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
@observer
|
||||
export class HelmCharts extends React.Component {
|
||||
@ -33,6 +35,14 @@ export class HelmCharts extends React.Component {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@computed get repoOptions() {
|
||||
return this.repos.map(repo => ({
|
||||
value: repo,
|
||||
label: repo.name,
|
||||
isSelected: this.addedRepos.has(repo.name),
|
||||
}));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadAvailableRepos().catch(noop);
|
||||
this.loadRepos().catch(noop);
|
||||
@ -98,40 +108,35 @@ export class HelmCharts extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onRepoSelect = async (repo: HelmRepo | null): Promise<void> => {
|
||||
if (!repo) {
|
||||
onRepoSelect = async (option: SingleValue<{ value: HelmRepo }>): Promise<void> => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.addedRepos.has(repo.name)) {
|
||||
if (this.addedRepos.has(option.value.name)) {
|
||||
return void Notifications.ok((
|
||||
<>
|
||||
{"Helm repo "}
|
||||
<b>{repo.name}</b>
|
||||
<b>{option.value.name}</b>
|
||||
{" already in use."}
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
await this.addRepo(repo);
|
||||
await this.addRepo(option.value);
|
||||
};
|
||||
|
||||
formatOptionLabel = (repo: HelmRepo) => {
|
||||
const isAdded = this.addedRepos.has(repo.name);
|
||||
|
||||
return (
|
||||
<div className="flex gaps">
|
||||
<span>{repo.name}</span>
|
||||
{isAdded && (
|
||||
<Icon
|
||||
small
|
||||
material="check"
|
||||
className="box right"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
formatOptionLabel = ({ value, isSelected }: SelectOption<HelmRepo>) => (
|
||||
<div className="flex gaps">
|
||||
<span>{value.name}</span>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
small
|
||||
material="check"
|
||||
className="box right" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
renderRepositories() {
|
||||
const repos = Array.from(this.addedRepos);
|
||||
@ -173,10 +178,10 @@ export class HelmCharts extends React.Component {
|
||||
placeholder="Repositories"
|
||||
isLoading={this.loadingAvailableRepos}
|
||||
isDisabled={this.loadingAvailableRepos}
|
||||
options={this.repos}
|
||||
options={this.repoOptions}
|
||||
onChange={this.onRepoSelect}
|
||||
value={this.repos}
|
||||
formatOptionLabel={this.formatOptionLabel}
|
||||
getOptionLabel={repo => repo.name}
|
||||
controlShouldRenderValue={false}
|
||||
className="box grow"
|
||||
themeName="lens"
|
||||
|
||||
@ -21,6 +21,11 @@ interface Dependencies {
|
||||
defaultPathForKubectlBinaries: string;
|
||||
userStore: UserStore;
|
||||
}
|
||||
const downloadMirrorOptions = Array.from(packageMirrors, ([name, mirror]) => ({
|
||||
value: name,
|
||||
label: mirror.label,
|
||||
isDisabled: !mirror.platforms.has(process.platform),
|
||||
}));
|
||||
|
||||
const NonInjectedKubectlBinaries= observer(({
|
||||
defaultPathForGeneralBinaries,
|
||||
@ -30,7 +35,6 @@ 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;
|
||||
@ -54,11 +58,9 @@ const NonInjectedKubectlBinaries= observer(({
|
||||
<Select
|
||||
placeholder="Download mirror for kubectl"
|
||||
options={downloadMirrorOptions}
|
||||
value={downloadMirrorOptions.find(opt => opt.name === userStore.downloadMirror)}
|
||||
onChange={option => userStore.downloadMirror = option?.name ?? defaultPackageMirror}
|
||||
getOptionLabel={option => option.mirror.label}
|
||||
value={userStore.downloadMirror}
|
||||
onChange={option => userStore.downloadMirror = option?.value ?? defaultPackageMirror}
|
||||
isDisabled={!userStore.downloadKubectlBinaries}
|
||||
isOptionDisabled={option => option.mirror.platforms.has(process.platform)}
|
||||
themeName="lens"
|
||||
/>
|
||||
</section>
|
||||
|
||||
@ -23,11 +23,21 @@ interface Dependencies {
|
||||
defaultShell: string;
|
||||
}
|
||||
|
||||
const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: Dependencies) => {
|
||||
const NonInjectedTerminal = observer(({
|
||||
userStore,
|
||||
themeStore,
|
||||
defaultShell,
|
||||
}: Dependencies) => {
|
||||
const themeOptions = [
|
||||
"",
|
||||
...themeStore.themes.keys(),
|
||||
].map(name => ({ name }));
|
||||
{
|
||||
value: "", // TODO: replace with a sentinal value that isn't string (and serialize it differently)
|
||||
label: "Match Lens Theme",
|
||||
},
|
||||
...Array.from(themeStore.themes, ([themeId, { name }]) => ({
|
||||
value: themeId,
|
||||
label: name,
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<Preferences data-testid="terminal-preferences-page">
|
||||
@ -59,17 +69,8 @@ const NonInjectedTerminal = observer(({ userStore, themeStore, defaultShell }: D
|
||||
<Select
|
||||
themeName="lens"
|
||||
options={themeOptions}
|
||||
getOptionLabel={option => {
|
||||
const theme = themeStore.themes.get(option.name);
|
||||
|
||||
if (theme) {
|
||||
return theme.name;
|
||||
}
|
||||
|
||||
return "Match System Theme";
|
||||
}}
|
||||
value={{ name: userStore.terminalTheme }}
|
||||
onChange={option => userStore.terminalTheme = option?.name ?? ""}
|
||||
value={userStore.terminalTheme}
|
||||
onChange={option => userStore.terminalTheme = option?.value ?? ""}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
@ -63,6 +63,21 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
|
||||
dialogState.isOpen = false;
|
||||
}
|
||||
|
||||
@computed get clusterRoleOptions() {
|
||||
return clusterRoleStore.items.map(clusterRole => ({
|
||||
value: clusterRole,
|
||||
label: clusterRole.getName(),
|
||||
}));
|
||||
}
|
||||
|
||||
@computed get serviceAccountOptions() {
|
||||
return serviceAccountStore.items.map(serviceAccount => ({
|
||||
value: serviceAccount,
|
||||
label: `${serviceAccount.getName()} (${serviceAccount.getNs()})`,
|
||||
isSelected: this.selectedAccounts.has(serviceAccount),
|
||||
}));
|
||||
}
|
||||
|
||||
get clusterRoleBinding() {
|
||||
return dialogState.data;
|
||||
}
|
||||
@ -164,29 +179,28 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
|
||||
themeName="light"
|
||||
placeholder="Select cluster role ..."
|
||||
isDisabled={this.isEditing}
|
||||
options={clusterRoleStore.items.slice()}
|
||||
options={this.clusterRoleOptions}
|
||||
value={this.selectedRoleRef}
|
||||
autoFocus={!this.isEditing}
|
||||
formatOptionLabel={value => (
|
||||
formatOptionLabel={option => (
|
||||
<>
|
||||
<Icon
|
||||
small
|
||||
material={value.kind === "Role" ? "person" : "people"}
|
||||
material="people"
|
||||
tooltip={{
|
||||
preferredPositions: TooltipPosition.LEFT,
|
||||
children: value.kind,
|
||||
children: option.value.kind,
|
||||
}}
|
||||
/>
|
||||
{" "}
|
||||
{value.getName()}
|
||||
{option.value.getName()}
|
||||
</>
|
||||
)}
|
||||
getOptionLabel={value => value.getName()}
|
||||
onChange={value => {
|
||||
this.selectedRoleRef = value ?? undefined;
|
||||
onChange={option => {
|
||||
this.selectedRoleRef = option?.value;
|
||||
|
||||
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
|
||||
this.bindingName = value?.getName() ?? "";
|
||||
this.bindingName = option?.value?.getName() ?? "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@ -223,16 +237,31 @@ export class ClusterRoleBindingDialog extends React.Component<ClusterRoleBinding
|
||||
isMulti
|
||||
themeName="light"
|
||||
placeholder="Select service accounts ..."
|
||||
options={serviceAccountStore.items.slice()}
|
||||
formatOptionLabel={value => (
|
||||
options={this.serviceAccountOptions}
|
||||
formatOptionLabel={option => (
|
||||
<>
|
||||
<Icon small material="account_box" />
|
||||
{` ${value.getName()} (${value.getNs()})`}
|
||||
{` ${option.label}`}
|
||||
</>
|
||||
)}
|
||||
getOptionLabel={value => `${value.getName()} (${value.getNs()})`}
|
||||
onChange={selected => {
|
||||
this.selectedAccounts.replace(selected);
|
||||
onChange={(selected, meta) => {
|
||||
switch (meta.action) {
|
||||
case "clear":
|
||||
this.selectedAccounts.clear();
|
||||
break;
|
||||
case "deselect-option":
|
||||
case "remove-value":
|
||||
case "pop-value":
|
||||
if (meta.option) {
|
||||
this.selectedAccounts.delete(meta.option.value);
|
||||
}
|
||||
break;
|
||||
case "select-option":
|
||||
if (meta.option) {
|
||||
this.selectedAccounts.add(meta.option.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}}
|
||||
maxMenuHeight={200}
|
||||
/>
|
||||
|
||||
@ -21,6 +21,7 @@ import { Icon } from "../../icon";
|
||||
import { showDetails } from "../../kube-detail-params";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Notifications } from "../../notifications";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import { Wizard, WizardStep } from "../../wizard";
|
||||
import { roleBindingStore } from "./legacy-store";
|
||||
@ -95,7 +96,7 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
|
||||
];
|
||||
}
|
||||
|
||||
@computed get roleRefOptions(): (Role | ClusterRole)[] {
|
||||
@computed get roleRefOptions(): SelectOption<Role | ClusterRole>[] {
|
||||
const roles = roleStore.items
|
||||
.filter(role => role.getNs() === this.bindingNamespace);
|
||||
const clusterRoles = clusterRoleStore.items;
|
||||
@ -103,7 +104,18 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
|
||||
return [
|
||||
...roles,
|
||||
...clusterRoles,
|
||||
];
|
||||
].map(r => ({
|
||||
value: r,
|
||||
label: r.getName(),
|
||||
}));
|
||||
}
|
||||
|
||||
@computed get serviceAccountOptions(): SelectOption<ServiceAccount>[] {
|
||||
return serviceAccountStore.items.map(serviceAccount => ({
|
||||
value: serviceAccount,
|
||||
label: `${serviceAccount.getName()} (${serviceAccount.getNs()})`,
|
||||
isSelected: this.selectedAccounts.has(serviceAccount),
|
||||
}));
|
||||
}
|
||||
|
||||
onOpen = action(() => {
|
||||
@ -182,7 +194,7 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
|
||||
isDisabled={this.isEditing}
|
||||
value={this.bindingNamespace}
|
||||
autoFocus={!this.isEditing}
|
||||
onChange={opt => this.bindingNamespace = opt ? opt.namespace : null}
|
||||
onChange={opt => this.bindingNamespace = opt?.value ?? null}
|
||||
/>
|
||||
|
||||
<SubTitle title="Role Reference" />
|
||||
@ -193,12 +205,11 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
|
||||
isDisabled={this.isEditing}
|
||||
options={this.roleRefOptions}
|
||||
value={this.selectedRoleRef}
|
||||
getOptionLabel={ref => ref.getName()}
|
||||
onChange={roleRef => {
|
||||
this.selectedRoleRef = roleRef;
|
||||
onChange={option => {
|
||||
this.selectedRoleRef = option?.value;
|
||||
|
||||
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
|
||||
this.bindingName = roleRef?.getName() ?? "";
|
||||
this.bindingName = option?.value.getName() ?? "";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@ -234,16 +245,32 @@ export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
|
||||
isMulti
|
||||
themeName="light"
|
||||
placeholder="Select service accounts ..."
|
||||
options={serviceAccountStore.items}
|
||||
isOptionSelected={account => this.selectedAccounts.has(account)}
|
||||
formatOptionLabel={account => (
|
||||
options={this.serviceAccountOptions}
|
||||
formatOptionLabel={option => (
|
||||
<>
|
||||
<Icon small material="account_box" />
|
||||
{` ${account.getName()} (${account.getNs()})`}
|
||||
{` ${option.label}`}
|
||||
</>
|
||||
)}
|
||||
getOptionLabel={account => `${account.getName()} (${account.getNs()})`}
|
||||
onChange={selected => this.selectedAccounts.replace(selected)}
|
||||
onChange={(selected, meta) => {
|
||||
switch (meta.action) {
|
||||
case "clear":
|
||||
this.selectedAccounts.clear();
|
||||
break;
|
||||
case "deselect-option":
|
||||
case "remove-value":
|
||||
case "pop-value":
|
||||
if (meta.option) {
|
||||
this.selectedAccounts.delete(meta.option.value);
|
||||
}
|
||||
break;
|
||||
case "select-option":
|
||||
if (meta.option) {
|
||||
this.selectedAccounts.add(meta.option.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}}
|
||||
maxMenuHeight={200}
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -90,7 +90,7 @@ export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
|
||||
id="add-dialog-namespace-select-input"
|
||||
themeName="light"
|
||||
value={this.namespace}
|
||||
onChange={option => this.namespace = option?.namespace ?? "default"}
|
||||
onChange={option => this.namespace = option?.value ?? "default"}
|
||||
/>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
|
||||
@ -86,7 +86,7 @@ export class CreateServiceAccountDialog extends React.Component<CreateServiceAcc
|
||||
id="create-dialog-namespace-select-input"
|
||||
themeName="light"
|
||||
value={namespace}
|
||||
onChange={option => this.namespace = option?.namespace ?? "default"}
|
||||
onChange={option => this.namespace = option?.value ?? "default"}
|
||||
/>
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
|
||||
@ -20,29 +20,30 @@ interface Dependencies {
|
||||
entities: IComputedValue<CatalogEntity[]>;
|
||||
}
|
||||
|
||||
const NonInjectedActivateEntityCommand = observer(({ closeCommandOverlay, entities }: Dependencies) => {
|
||||
const onSelect = (entity: CatalogEntity | null): void => {
|
||||
if (entity) {
|
||||
broadcastMessage(catalogEntityRunListener, entity.getId());
|
||||
closeCommandOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
id="activate-entity-input"
|
||||
menuPortalTarget={null}
|
||||
onChange={onSelect}
|
||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||
menuIsOpen={true}
|
||||
options={entities.get()}
|
||||
getOptionLabel={entity => `${entity.kind}: ${entity.getName()}`}
|
||||
autoFocus={true}
|
||||
escapeClearsValue={false}
|
||||
placeholder="Activate entity ..."
|
||||
/>
|
||||
);
|
||||
});
|
||||
const NonInjectedActivateEntityCommand = observer(({ closeCommandOverlay, entities }: Dependencies) => (
|
||||
<Select
|
||||
id="activate-entity-input"
|
||||
menuPortalTarget={null}
|
||||
onChange={(option) => {
|
||||
if (option) {
|
||||
broadcastMessage(catalogEntityRunListener, option.value.getId());
|
||||
closeCommandOverlay();
|
||||
}
|
||||
}}
|
||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||
menuIsOpen={true}
|
||||
options={(
|
||||
entities.get()
|
||||
.map(entity => ({
|
||||
value: entity,
|
||||
label: `${entity.kind}: ${entity.getName()}`,
|
||||
}))
|
||||
)}
|
||||
autoFocus={true}
|
||||
escapeClearsValue={false}
|
||||
placeholder="Activate entity ..."
|
||||
/>
|
||||
));
|
||||
|
||||
export const ActivateEntityCommand = withInjectables<Dependencies>(NonInjectedActivateEntityCommand, {
|
||||
getProps: di => ({
|
||||
|
||||
@ -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, runInAction } from "mobx";
|
||||
import { observable, reaction, makeObservable } from "mobx";
|
||||
import { ClusterMetricsResourceType } from "../../../../common/cluster-types";
|
||||
|
||||
export interface ClusterMetricsSettingProps {
|
||||
@ -52,7 +52,11 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
|
||||
|
||||
renderMetricsSelect() {
|
||||
const metricResourceTypeOptions = Object.values(ClusterMetricsResourceType)
|
||||
.map(type => ({ type }));
|
||||
.map(type => ({
|
||||
value: type,
|
||||
label: type,
|
||||
isSelected: this.hiddenMetrics.has(type),
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -67,16 +71,13 @@ export class ClusterMetricsSetting extends React.Component<ClusterMetricsSetting
|
||||
controlShouldRenderValue={false}
|
||||
options={metricResourceTypeOptions}
|
||||
onChange={(options) => {
|
||||
runInAction(() => {
|
||||
this.hiddenMetrics.replace(options.map(opt => opt.type));
|
||||
this.save();
|
||||
});
|
||||
this.hiddenMetrics.replace(options.map(opt => opt.value));
|
||||
this.save();
|
||||
}}
|
||||
getOptionLabel={option => option.type}
|
||||
formatOptionLabel={(option) => (
|
||||
<div className="flex gaps align-center">
|
||||
<span>{option.type}</span>
|
||||
{this.hiddenMetrics.has(option.type) && (
|
||||
<span>{option.value}</span>
|
||||
{option.isSelected && (
|
||||
<Icon
|
||||
smallest
|
||||
material="check"
|
||||
|
||||
@ -7,6 +7,7 @@ import React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import { Input } from "../../input";
|
||||
import { observable, computed, autorun, makeObservable } from "mobx";
|
||||
@ -14,7 +15,6 @@ import { productName } from "../../../../common/vars";
|
||||
import type { MetricProviderInfo } from "../../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { metricsApi } from "../../../../common/k8s-api/endpoints/metrics.api";
|
||||
import { Spinner } from "../../spinner";
|
||||
import type { GroupBase } from "react-select";
|
||||
|
||||
export interface ClusterPrometheusSettingProps {
|
||||
cluster: Cluster;
|
||||
@ -22,23 +22,28 @@ export interface ClusterPrometheusSettingProps {
|
||||
|
||||
const autoDetectPrometheus = Symbol("auto-detect-prometheus");
|
||||
|
||||
interface ProviderOption {
|
||||
provider: typeof autoDetectPrometheus | string;
|
||||
}
|
||||
type ProviderValue = typeof autoDetectPrometheus | string;
|
||||
|
||||
@observer
|
||||
export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusSettingProps> {
|
||||
@observable path = "";
|
||||
@observable selectedOption = this.options.find(opt => opt.provider === autoDetectPrometheus);
|
||||
@observable selectedOption: ProviderValue = autoDetectPrometheus;
|
||||
@observable loading = true;
|
||||
loadedOptions = observable.map<string, MetricProviderInfo>();
|
||||
readonly loadedOptions = observable.map<string, MetricProviderInfo>();
|
||||
|
||||
@computed get options(): ProviderOption[] {
|
||||
return ([
|
||||
autoDetectPrometheus,
|
||||
...this.loadedOptions.keys(),
|
||||
] as const)
|
||||
.map(provider => ({ provider }));
|
||||
@computed get options(): SelectOption<ProviderValue>[] {
|
||||
return [
|
||||
{
|
||||
value: autoDetectPrometheus,
|
||||
label: "Auto Detect Prometheus",
|
||||
isSelected: autoDetectPrometheus === this.selectedOption,
|
||||
},
|
||||
...Array.from(this.loadedOptions, ([id, provider]) => ({
|
||||
value: id,
|
||||
label: provider.name,
|
||||
isSelected: id === this.selectedOption,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
constructor(props: ClusterPrometheusSettingProps) {
|
||||
@ -47,11 +52,11 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
|
||||
}
|
||||
|
||||
@computed get canEditPrometheusPath(): boolean {
|
||||
if (!this.selectedOption || this.selectedOption.provider === autoDetectPrometheus) {
|
||||
if (!this.selectedOption || this.selectedOption === autoDetectPrometheus) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.loadedOptions.get(this.selectedOption.provider)?.isConfigurable ?? false;
|
||||
return this.loadedOptions.get(this.selectedOption)?.isConfigurable ?? false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -68,9 +73,9 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
|
||||
}
|
||||
|
||||
if (prometheusProvider) {
|
||||
this.selectedOption = this.options.find(opt => opt.provider === prometheusProvider.type);
|
||||
this.selectedOption = this.options.find(opt => opt.value === prometheusProvider.type)?.value ?? autoDetectPrometheus;
|
||||
} else {
|
||||
this.selectedOption = undefined;
|
||||
this.selectedOption = autoDetectPrometheus;
|
||||
}
|
||||
}),
|
||||
);
|
||||
@ -122,11 +127,11 @@ export class ClusterPrometheusSetting extends React.Component<ClusterPrometheusS
|
||||
? <Spinner />
|
||||
: (
|
||||
<>
|
||||
<Select<ProviderOption, false, GroupBase<ProviderOption>>
|
||||
<Select
|
||||
id="cluster-prometheus-settings-input"
|
||||
value={this.selectedOption}
|
||||
onChange={provider => {
|
||||
this.selectedOption = provider ?? { provider: autoDetectPrometheus };
|
||||
onChange={option => {
|
||||
this.selectedOption = option?.value ?? autoDetectPrometheus;
|
||||
this.onSaveProvider();
|
||||
}}
|
||||
options={this.options}
|
||||
|
||||
@ -36,7 +36,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
|
||||
|
||||
try {
|
||||
closeCommandOverlay();
|
||||
option.action({
|
||||
option.value.action({
|
||||
entity: activeEntity,
|
||||
navigate: (url, opts = {}) => {
|
||||
const { forceRootFrame = false } = opts;
|
||||
@ -49,7 +49,7 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[COMMAND-DIALOG] failed to execute command", option.id, error);
|
||||
console.error("[COMMAND-DIALOG] failed to execute command", option.value.id, error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -65,6 +65,12 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
|
||||
return void console.error(`[COMMAND-DIALOG]: isActive for ${command.id} threw an error, defaulting to false`, error);
|
||||
}
|
||||
})
|
||||
.map(command => ({
|
||||
value: command,
|
||||
label: typeof command.title === "string"
|
||||
? command.title
|
||||
: command.title(context),
|
||||
}))
|
||||
.collect(items => Array.from(items));
|
||||
|
||||
return (
|
||||
@ -78,11 +84,6 @@ const NonInjectedCommandDialog = observer(({ commands, activeEntity, closeComman
|
||||
}}
|
||||
menuIsOpen
|
||||
options={activeCommands}
|
||||
getOptionLabel={({ title }) => (
|
||||
typeof title === "string"
|
||||
? title
|
||||
: title(context)
|
||||
)}
|
||||
autoFocus={true}
|
||||
escapeClearsValue={false}
|
||||
data-test-id="command-palette-search"
|
||||
|
||||
@ -99,22 +99,23 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contextName = this.newCurrentContext.get();
|
||||
const selectOptions = config
|
||||
.contexts
|
||||
.filter(context => context.name !== cluster.contextName);
|
||||
const selectedOption = selectOptions.find(ctx => ctx.name === contextName);
|
||||
.filter(context => context.name !== cluster.contextName)
|
||||
.map(context => ({
|
||||
value: context.name,
|
||||
label: context.name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Select
|
||||
id="delete-cluster-input"
|
||||
options={selectOptions}
|
||||
getOptionLabel={opt => opt.name}
|
||||
value={selectedOption}
|
||||
value={this.newCurrentContext.get()}
|
||||
onChange={opt => {
|
||||
if (opt) {
|
||||
this.newCurrentContext.set(opt.name);
|
||||
this.newCurrentContext.set(opt.value);
|
||||
}
|
||||
}}
|
||||
themeName="light"
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import yaml from "js-yaml";
|
||||
import type { IComputedValue } from "mobx";
|
||||
@ -107,7 +108,7 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
|
||||
renderControls() {
|
||||
return (
|
||||
<div className="flex gaps align-center">
|
||||
<Select<{ label: string; value: string }, false>
|
||||
<Select<string, SelectOption<string>, false>
|
||||
id="create-resource-resource-templates-input"
|
||||
controlShouldRenderValue={false} // always keep initial placeholder
|
||||
className="TemplateSelect"
|
||||
@ -116,9 +117,9 @@ class NonInjectedCreateResource extends React.Component<CreateResourceProps & De
|
||||
formatGroupLabel={group => group.label}
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
onChange={(item) => {
|
||||
if (item) {
|
||||
this.props.createResourceTabStore.setData(this.tabId, item.value);
|
||||
onChange={(option) => {
|
||||
if (option) {
|
||||
this.props.createResourceTabStore.setData(this.tabId, option.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -18,6 +18,7 @@ import { Spinner } from "../../spinner";
|
||||
import { Icon } from "../../icon";
|
||||
import { Button } from "../../button";
|
||||
import { LogsDialog } from "../../dialog/logs-dialog";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import { Input } from "../../input";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
@ -88,7 +89,7 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
|
||||
this.props.installChartStore.setData(this.tabId, { ...this.chartData, ...data });
|
||||
}
|
||||
|
||||
onVersionChange = (option: SingleValue<{ version: string }>) => {
|
||||
onVersionChange = (option: SingleValue<SelectOption<string>>) => {
|
||||
if (option) {
|
||||
this.save({ ...option, values: "" });
|
||||
this.props.installChartStore.loadValues(this.tabId);
|
||||
@ -104,9 +105,9 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
|
||||
this.error = error.toString();
|
||||
});
|
||||
|
||||
onNamespaceChange = (option: SingleValue<{ namespace: string }>) => {
|
||||
onNamespaceChange = (option: SingleValue<SelectOption<string>>) => {
|
||||
if (option) {
|
||||
this.save(option);
|
||||
this.save({ namespace: option.value });
|
||||
}
|
||||
};
|
||||
|
||||
@ -179,8 +180,10 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
|
||||
}
|
||||
|
||||
const { repo, name, version, namespace, releaseName } = chartData;
|
||||
const versionOptions = versions.map(version => ({ version }));
|
||||
const selectedVersionOption = versionOptions.find(opt => opt.version === version);
|
||||
const versionOptions = versions.map(version => ({
|
||||
value: version,
|
||||
label: version,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="InstallChart flex column">
|
||||
@ -193,7 +196,7 @@ class NonInjectedInstallChart extends Component<InstallCharProps & Dependencies>
|
||||
<span>Version</span>
|
||||
<Select
|
||||
className="chart-version"
|
||||
value={selectedVersionOption}
|
||||
value={version}
|
||||
options={versionOptions}
|
||||
onChange={this.onVersionChange}
|
||||
menuPlacement="top"
|
||||
|
||||
@ -9,10 +9,11 @@ import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import { Badge } from "../../badge";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import type { LogTabViewModel } from "./logs-view-model";
|
||||
import type { PodContainer, Pod } from "../../../../common/k8s-api/endpoints";
|
||||
import type { GroupBase } from "react-select";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
export interface LogResourceSelectorProps {
|
||||
model: LogTabViewModel;
|
||||
@ -33,40 +34,50 @@ export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps
|
||||
return null;
|
||||
}
|
||||
|
||||
const podOptions = pods.map(pod => ({
|
||||
value: pod,
|
||||
label: pod.getName(),
|
||||
}));
|
||||
const allContainers = pod.getAllContainers();
|
||||
const container = allContainers.find(container => container.name === selectedContainer) ?? null;
|
||||
const onContainerChange = (container: PodContainer | null) => {
|
||||
if (!container) {
|
||||
const onContainerChange = (option: SingleValue<SelectOption<PodContainer>>) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.updateLogTabData({
|
||||
selectedContainer: container.name,
|
||||
selectedContainer: option.value.name,
|
||||
});
|
||||
model.reloadLogs();
|
||||
};
|
||||
|
||||
const onPodChange = (value: Pod | null) => {
|
||||
if (!value) {
|
||||
const onPodChange = (option: SingleValue<SelectOption<Pod>>) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.updateLogTabData({
|
||||
selectedPodId: value.getId(),
|
||||
selectedContainer: value.getAllContainers()[0]?.name,
|
||||
selectedPodId: option.value.getId(),
|
||||
selectedContainer: option.value.getAllContainers()[0]?.name,
|
||||
});
|
||||
model.renameTab(`Pod ${value.getName()}`);
|
||||
model.renameTab(`Pod ${option.value.getName()}`);
|
||||
model.reloadLogs();
|
||||
};
|
||||
|
||||
const containerSelectOptions = [
|
||||
{
|
||||
label: "Containers",
|
||||
options: pod.getContainers(),
|
||||
options: pod.getContainers().map(container => ({
|
||||
value: container,
|
||||
label: container.name,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Init Containers",
|
||||
options: pod.getInitContainers(),
|
||||
options: pod.getInitContainers().map(container => ({
|
||||
value: container,
|
||||
label: container.name,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
@ -86,21 +97,19 @@ export const LogResourceSelector = observer(({ model }: LogResourceSelectorProps
|
||||
}
|
||||
<span>Pod</span>
|
||||
<Select
|
||||
options={pods}
|
||||
options={podOptions}
|
||||
value={pod}
|
||||
isClearable={false}
|
||||
formatOptionLabel={option => option.getName()}
|
||||
onChange={onPodChange}
|
||||
className="pod-selector"
|
||||
menuClass="pod-selector-menu"
|
||||
/>
|
||||
<span>Container</span>
|
||||
<Select<PodContainer, false, GroupBase<PodContainer>>
|
||||
<Select<PodContainer, SelectOption<PodContainer>, false>
|
||||
id="container-selector-input"
|
||||
options={containerSelectOptions}
|
||||
value={container}
|
||||
onChange={onContainerChange}
|
||||
getOptionLabel={opt => opt.name}
|
||||
className="container-selector"
|
||||
menuClass="container-selector-menu"
|
||||
controlShouldRenderValue
|
||||
|
||||
@ -15,15 +15,15 @@ import type { UpgradeChartTabStore } from "./store";
|
||||
import { Spinner } from "../../spinner";
|
||||
import { Badge } from "../../badge";
|
||||
import { EditorPanel } from "../editor-panel";
|
||||
import { helmChartStore, type IChartVersion } from "../../+helm-charts/helm-chart.store";
|
||||
import { helmChartStore, type ChartVersion } from "../../+helm-charts/helm-chart.store";
|
||||
import type { HelmRelease, HelmReleaseUpdateDetails, HelmReleaseUpdatePayload } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { Select } from "../../select";
|
||||
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import upgradeChartTabStoreInjectable from "./store.injectable";
|
||||
import updateReleaseInjectable from "../../+helm-releases/update-release/update-release.injectable";
|
||||
import releasesInjectable from "../../+helm-releases/releases.injectable";
|
||||
import type { GroupBase } from "react-select";
|
||||
|
||||
export interface UpgradeChartProps {
|
||||
className?: string;
|
||||
@ -39,8 +39,8 @@ interface Dependencies {
|
||||
@observer
|
||||
export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps & Dependencies> {
|
||||
@observable error?: string;
|
||||
@observable versions = observable.array<IChartVersion>();
|
||||
@observable version: IChartVersion | undefined = undefined;
|
||||
@observable versions = observable.array<ChartVersion>();
|
||||
@observable version: ChartVersion | undefined = undefined;
|
||||
|
||||
constructor(props: UpgradeChartProps & Dependencies) {
|
||||
super(props);
|
||||
@ -133,13 +133,6 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
|
||||
);
|
||||
};
|
||||
|
||||
formatVersionLabel = (value: IChartVersion) => {
|
||||
const chartName = this.release?.getChart() ?? "<unknown chart>";
|
||||
const { repo, version } = value;
|
||||
|
||||
return `${repo}/${chartName}-${version}`;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { tabId, release, value, error, onChange, onError, upgrade, versions, version } = this;
|
||||
const { className } = this.props;
|
||||
@ -148,6 +141,10 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
|
||||
return <Spinner center />;
|
||||
}
|
||||
const currentVersion = release.getVersion();
|
||||
const versionOptions = versions.map(version => ({
|
||||
value: version,
|
||||
label: `${version.repo}/${release.getChart()}-${version.version}`,
|
||||
}));
|
||||
const controlsAndInfo = (
|
||||
<div className="upgrade flex gaps align-center">
|
||||
<span>Release</span>
|
||||
@ -160,15 +157,14 @@ export class NonInjectedUpgradeChart extends React.Component<UpgradeChartProps &
|
||||
{" "}
|
||||
<Badge label={currentVersion} />
|
||||
<span>Upgrade version</span>
|
||||
<Select<IChartVersion, false, GroupBase<IChartVersion>>
|
||||
<Select<ChartVersion, SelectOption<ChartVersion>, false>
|
||||
id="char-version-input"
|
||||
className="chart-version"
|
||||
menuPlacement="top"
|
||||
themeName="outlined"
|
||||
value={version}
|
||||
options={versions}
|
||||
getOptionLabel={this.formatVersionLabel}
|
||||
onChange={value => this.version = value ?? undefined}
|
||||
options={versionOptions}
|
||||
onChange={option => this.version = option?.value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -23,8 +23,8 @@ const NonInjectedHotbarRemoveCommand = observer(({
|
||||
}: Dependencies) => (
|
||||
<Select
|
||||
menuPortalTarget={null}
|
||||
onChange={hotbar => {
|
||||
if (!hotbar) {
|
||||
onChange={option => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -36,14 +36,14 @@ const NonInjectedHotbarRemoveCommand = observer(({
|
||||
primary: false,
|
||||
accent: true,
|
||||
},
|
||||
ok: () => hotbarStore.remove(hotbar),
|
||||
ok: () => hotbarStore.remove(option.value),
|
||||
message: (
|
||||
<div className="confirm flex column gaps">
|
||||
<p>
|
||||
Are you sure you want remove hotbar
|
||||
{" "}
|
||||
<b>
|
||||
{hotbar.name}
|
||||
{option.value.name}
|
||||
</b>
|
||||
?
|
||||
</p>
|
||||
@ -53,8 +53,13 @@ const NonInjectedHotbarRemoveCommand = observer(({
|
||||
} }
|
||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||
menuIsOpen={true}
|
||||
options={hotbarStore.hotbars}
|
||||
getOptionLabel={hotbar => hotbarStore.getDisplayLabel(hotbar)}
|
||||
options={(
|
||||
hotbarStore.hotbars
|
||||
.map(hotbar => ({
|
||||
value: hotbar,
|
||||
label: hotbarStore.getDisplayLabel(hotbar),
|
||||
}))
|
||||
)}
|
||||
autoFocus={true}
|
||||
escapeClearsValue={false}
|
||||
placeholder="Remove hotbar"
|
||||
|
||||
@ -9,7 +9,6 @@ import { Select } from "../select";
|
||||
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
|
||||
import type { InputValidator } from "../input";
|
||||
import { Input } from "../input";
|
||||
import type { Hotbar } from "../../../common/hotbars/types";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable";
|
||||
@ -29,12 +28,6 @@ const NonInjectedHotbarRenameCommand = observer(({
|
||||
const [hotbarId, setHotbarId] = useState("");
|
||||
const [hotbarName, setHotbarName] = useState("");
|
||||
|
||||
const onSelect = (hotbar: Hotbar | null) => {
|
||||
if (hotbar) {
|
||||
setHotbarId(hotbar.id);
|
||||
setHotbarName(hotbar.name);
|
||||
}
|
||||
};
|
||||
const onSubmit = (name: string) => {
|
||||
if (!name.trim()) {
|
||||
return;
|
||||
@ -69,11 +62,21 @@ const NonInjectedHotbarRenameCommand = observer(({
|
||||
<Select
|
||||
id="rename-hotbar-input"
|
||||
menuPortalTarget={null}
|
||||
onChange={onSelect}
|
||||
onChange={(option) => {
|
||||
if (option) {
|
||||
setHotbarId(option.value.id);
|
||||
setHotbarName(option.value.name);
|
||||
}
|
||||
}}
|
||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||
menuIsOpen={true}
|
||||
options={hotbarStore.hotbars}
|
||||
getOptionLabel={hotbar => hotbarStore.getDisplayLabel(hotbar)}
|
||||
options={(
|
||||
hotbarStore.hotbars
|
||||
.map(hotbar => ({
|
||||
value: hotbar,
|
||||
label: hotbarStore.getDisplayLabel(hotbar),
|
||||
}))
|
||||
)}
|
||||
autoFocus={true}
|
||||
escapeClearsValue={false}
|
||||
placeholder="Rename hotbar"
|
||||
|
||||
@ -14,7 +14,6 @@ import { HotbarRenameCommand } from "./hotbar-rename-command";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbars/store";
|
||||
import type { Hotbar } from "../../../common/hotbars/types";
|
||||
|
||||
const hotbarAddAction = Symbol("hotbar-add");
|
||||
const hotbarRemoveAction = Symbol("hotbar-remove");
|
||||
@ -29,31 +28,6 @@ function ignoreIf<T>(check: boolean, menuItems: T[]): T[] {
|
||||
return check ? [] : menuItems;
|
||||
}
|
||||
|
||||
interface HotbarSwitchActionOption {
|
||||
action: typeof hotbarAddAction | typeof hotbarRemoveAction | typeof hotbarRenameAction;
|
||||
}
|
||||
|
||||
interface SwitchToHotbarOption {
|
||||
hotbar: Hotbar;
|
||||
}
|
||||
|
||||
type HotbarSwitchOption = SwitchToHotbarOption | HotbarSwitchActionOption;
|
||||
|
||||
function getHotbarSwitchOptions(hotbars: Hotbar[]): HotbarSwitchOption[] {
|
||||
return [
|
||||
...hotbars.map(hotbar => ({ hotbar })),
|
||||
{ action: hotbarAddAction },
|
||||
...ignoreIf(hotbars.length > 1, [
|
||||
{ action: hotbarRemoveAction } as const,
|
||||
]),
|
||||
{ action: hotbarRenameAction },
|
||||
];
|
||||
}
|
||||
|
||||
function isActionOption(option: HotbarSwitchOption): option is HotbarSwitchActionOption {
|
||||
return Boolean((option as HotbarSwitchActionOption).action);
|
||||
}
|
||||
|
||||
const NonInjectedHotbarSwitchCommand = observer(({
|
||||
hotbarStore,
|
||||
commandOverlay,
|
||||
@ -66,8 +40,8 @@ const NonInjectedHotbarSwitchCommand = observer(({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActionOption(option)) {
|
||||
switch (option.action) {
|
||||
if (typeof option.value === "symbol") {
|
||||
switch (option.value) {
|
||||
case hotbarAddAction:
|
||||
return commandOverlay.open(<HotbarAddCommand />);
|
||||
case hotbarRemoveAction:
|
||||
@ -75,28 +49,33 @@ const NonInjectedHotbarSwitchCommand = observer(({
|
||||
case hotbarRenameAction:
|
||||
return commandOverlay.open(<HotbarRenameCommand />);
|
||||
}
|
||||
} else {
|
||||
hotbarStore.setActiveHotbar(option.value);
|
||||
commandOverlay.close();
|
||||
}
|
||||
|
||||
hotbarStore.setActiveHotbar(option.hotbar);
|
||||
commandOverlay.close();
|
||||
}}
|
||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||
menuIsOpen={true}
|
||||
options={getHotbarSwitchOptions(hotbarStore.hotbars)}
|
||||
getOptionLabel={option => {
|
||||
if (isActionOption(option)) {
|
||||
switch (option.action) {
|
||||
case hotbarAddAction:
|
||||
return "Add hotbar ...";
|
||||
case hotbarRemoveAction:
|
||||
return "Remove hotbar ...";
|
||||
case hotbarRenameAction:
|
||||
return "Rename hotbar ...";
|
||||
}
|
||||
}
|
||||
|
||||
return hotbarStore.getDisplayLabel(option.hotbar);
|
||||
}}
|
||||
options={[
|
||||
...hotbarStore.hotbars.map(hotbar => ({
|
||||
value: hotbar,
|
||||
label: hotbarStore.getDisplayLabel(hotbar),
|
||||
})),
|
||||
{
|
||||
value: hotbarAddAction,
|
||||
label: "Add hotbar ...",
|
||||
},
|
||||
...ignoreIf(hotbarStore.hotbars.length > 1, [
|
||||
{
|
||||
value: hotbarRemoveAction,
|
||||
label: "Remove hotbar ...",
|
||||
},
|
||||
]),
|
||||
{
|
||||
value: hotbarRenameAction,
|
||||
label: "Rename hotbar ...",
|
||||
},
|
||||
]}
|
||||
autoFocus={true}
|
||||
escapeClearsValue={false}
|
||||
isClearable={false}
|
||||
|
||||
@ -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, untracked } from "mobx";
|
||||
import type { ConfirmDialogParams } from "../confirm-dialog";
|
||||
import type { TableCellProps, TableProps, TableRowProps, TableSortCallbacks } from "../table";
|
||||
@ -17,8 +18,6 @@ import type { ItemObject } from "../../../common/item.store";
|
||||
import type { SearchInputUrlProps } from "../input";
|
||||
import { FilterType, pageFilters } from "./page-filters.store";
|
||||
import { PageFiltersList } from "./page-filters-list";
|
||||
import type { NamespaceStore } from "../+namespaces/store";
|
||||
import namespaceStoreInjectable from "../+namespaces/store.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import itemListLayoutStorageInjectable from "./storage.injectable";
|
||||
import { ItemListLayoutContent } from "./content";
|
||||
@ -28,6 +27,7 @@ import { ItemListLayoutFilters } from "./filters";
|
||||
import { observer } from "mobx-react";
|
||||
import type { Primitive } from "type-fest";
|
||||
import type { SubscribableStore } from "../../kube-watch-api/kube-watch-api";
|
||||
import selectedFilterNamespacesInjectable from "../../../common/k8s-api/selected-filter-namespaces.injectable";
|
||||
|
||||
export type SearchFilter<I extends ItemObject> = (item: I) => SingleOrMany<string | number | undefined | null>;
|
||||
export type SearchFilters<I extends ItemObject> = Record<string, SearchFilter<I>>;
|
||||
@ -159,7 +159,7 @@ export interface ItemListLayoutStorage {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
namespaceStore: NamespaceStore;
|
||||
selectedFilterNamespaces: IComputedValue<string[]>;
|
||||
itemListLayoutStorage: StorageLayer<ItemListLayoutStorage>;
|
||||
}
|
||||
|
||||
@ -184,7 +184,7 @@ class NonInjectedItemListLayout<I extends ItemObject, PreLoadStores extends bool
|
||||
const { store, dependentStores = [] } = this.props;
|
||||
const stores = Array.from(new Set([store, ...dependentStores])) as ItemListStore<I, true>[];
|
||||
|
||||
stores.forEach(store => store.loadAll(this.props.namespaceStore.contextNamespaces));
|
||||
stores.forEach(store => store.loadAll(this.props.selectedFilterNamespaces.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,7 +328,7 @@ class NonInjectedItemListLayout<I extends ItemObject, PreLoadStores extends bool
|
||||
|
||||
const InjectedItemListLayout = withInjectables<Dependencies, ItemListLayoutProps<ItemObject, boolean>>(NonInjectedItemListLayout, {
|
||||
getProps: (di, props) => ({
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
selectedFilterNamespaces: di.inject(selectedFilterNamespacesInjectable),
|
||||
itemListLayoutStorage: di.inject(itemListLayoutStorageInjectable),
|
||||
...props,
|
||||
}),
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import type { SelectOption } from "./select";
|
||||
import { Select } from "./select";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
@ -94,7 +95,7 @@ describe("<Select />", () => {
|
||||
|
||||
const { container } = render((
|
||||
<Select
|
||||
value={options[0]}
|
||||
value={options[0].value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
@ -120,7 +121,7 @@ describe("<Select />", () => {
|
||||
|
||||
const { container, rerender } = render((
|
||||
<Select
|
||||
value={options[0]}
|
||||
value={options[0].value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
@ -131,7 +132,7 @@ describe("<Select />", () => {
|
||||
|
||||
rerender((
|
||||
<Select
|
||||
value={options[1]}
|
||||
value={options[1].value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
@ -156,7 +157,7 @@ describe("<Select />", () => {
|
||||
|
||||
const { container, rerender } = render((
|
||||
<Select
|
||||
value={options[0]}
|
||||
value={options[0].value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
@ -166,7 +167,7 @@ describe("<Select />", () => {
|
||||
expect(selectedValueContainer?.textContent).toBe(options[0].label);
|
||||
|
||||
rerender((
|
||||
<Select
|
||||
<Select<string, SelectOption<string>>
|
||||
value={null}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
@ -192,7 +193,7 @@ describe("<Select />", () => {
|
||||
|
||||
const { container, rerender } = render((
|
||||
<Select
|
||||
value={options[0]}
|
||||
value={options[0].value}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
@ -202,7 +203,7 @@ describe("<Select />", () => {
|
||||
expect(selectedValueContainer?.textContent).toBe(options[0].label);
|
||||
|
||||
rerender((
|
||||
<Select
|
||||
<Select<string, SelectOption<string>>
|
||||
value={undefined}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
|
||||
@ -8,51 +8,71 @@
|
||||
import "./select.scss";
|
||||
|
||||
import React from "react";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import { action, computed, makeObservable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import ReactSelect, { components } from "react-select";
|
||||
import type { Props as ReactSelectProps, GroupBase } from "react-select";
|
||||
import ReactSelect, { components, createFilter } from "react-select";
|
||||
import type { Props as ReactSelectProps, GroupBase, MultiValue, OptionsOrGroups, PropsValue, SingleValue } from "react-select";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { autoBind, cssNames } from "../../utils";
|
||||
import type { SetRequired } from "type-fest";
|
||||
|
||||
const { Menu } = components;
|
||||
|
||||
/**
|
||||
* @deprecated This type is no longer used
|
||||
*/
|
||||
export interface SelectOption<T> {
|
||||
value: T;
|
||||
label?: React.ReactElement | string;
|
||||
export interface SelectOption<Value> {
|
||||
value: Value;
|
||||
label: string;
|
||||
isDisabled?: boolean;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface SelectProps<
|
||||
Value,
|
||||
/**
|
||||
* This needs to extend `object` because even though `ReactSelectProps` allows for any `T`, the
|
||||
* maintainers of `react-select` says that they don't support it.
|
||||
*
|
||||
* Ref: https://github.com/JedWatson/react-select/issues/5032
|
||||
*
|
||||
* Futhermore, we mandate the option is of this shape because it is easier than requiring
|
||||
* `getOptionValue` and `getOptionLabel` all over the place.
|
||||
*/
|
||||
Option extends object,
|
||||
Option extends SelectOption<Value>,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>,
|
||||
> extends ReactSelectProps<Option, IsMulti, Group> {
|
||||
> extends SetRequired<Omit<ReactSelectProps<Option, IsMulti, Group>, "value">, "options"> {
|
||||
id?: string; // Optional only because of Extension API. Required to make Select deterministic in unit tests
|
||||
themeName?: "dark" | "light" | "outlined" | "lens";
|
||||
menuClass?: string;
|
||||
value?: PropsValue<Value>;
|
||||
}
|
||||
|
||||
function isGroup<Option, Group extends GroupBase<Option>>(optionOrGroup: Option | Group): optionOrGroup is Group {
|
||||
return Array.isArray((optionOrGroup as Group).options);
|
||||
}
|
||||
|
||||
const defaultFilter = createFilter({
|
||||
stringify(option) {
|
||||
if (typeof option.data === "symbol") {
|
||||
return option.label;
|
||||
}
|
||||
|
||||
return `${option.label} ${option.value}`;
|
||||
},
|
||||
});
|
||||
|
||||
@observer
|
||||
export class Select<
|
||||
Option extends object,
|
||||
Value,
|
||||
Option extends SelectOption<Value>,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>,
|
||||
> extends React.Component<SelectProps<Option, IsMulti, Group>> {
|
||||
> extends React.Component<SelectProps<Value, Option, IsMulti, Group>> {
|
||||
static defaultProps = {
|
||||
menuPortalTarget: document.body,
|
||||
menuPlacement: "auto",
|
||||
};
|
||||
|
||||
constructor(props: SelectProps<Option, IsMulti, Group>) {
|
||||
constructor(props: SelectProps<Value, Option, IsMulti, Group>) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
@ -72,6 +92,48 @@ export class Select<
|
||||
}
|
||||
}
|
||||
|
||||
private filterSelectedMultiValue(values: MultiValue<Value> | null, options: OptionsOrGroups<Option, Group>): MultiValue<Option> | null {
|
||||
if (!values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return options
|
||||
.flatMap(option => (
|
||||
isGroup(option)
|
||||
? option.options
|
||||
: option
|
||||
))
|
||||
.filter(option => values.includes(option.value));
|
||||
}
|
||||
|
||||
private findSelectedSingleValue(value: SingleValue<Value>, options: OptionsOrGroups<Option, Group>): SingleValue<Option> {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const optionOrGroup of options) {
|
||||
if (isGroup(optionOrGroup)) {
|
||||
for (const option of optionOrGroup.options) {
|
||||
if (option.value === value) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
} else if (optionOrGroup.value === value) {
|
||||
return optionOrGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private findSelectedPropsValue(value: PropsValue<Value>, options: OptionsOrGroups<Option, Group>, isMulti: IsMulti | undefined): PropsValue<Option> {
|
||||
if (isMulti) {
|
||||
return this.filterSelectedMultiValue(value as MultiValue<Value>, options);
|
||||
}
|
||||
|
||||
return this.findSelectedSingleValue(value as SingleValue<Value>, options);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
@ -79,10 +141,17 @@ export class Select<
|
||||
components = {},
|
||||
styles,
|
||||
value = null,
|
||||
options,
|
||||
isMulti,
|
||||
onChange,
|
||||
...props
|
||||
} = this.props;
|
||||
const WrappedMenu = components.Menu ?? Menu;
|
||||
|
||||
if (options.length > 0 && !(options?.[0] as { label?: string }).label) {
|
||||
console.warn("[SELECT]: will not display any label in dropdown");
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
{...props}
|
||||
@ -93,10 +162,14 @@ export class Select<
|
||||
}),
|
||||
...styles,
|
||||
}}
|
||||
value={value}
|
||||
filterOption={defaultFilter} // This is done because the default filter crashes on symbols
|
||||
isMulti={isMulti}
|
||||
options={options}
|
||||
value={this.findSelectedPropsValue(value, options, isMulti)}
|
||||
onKeyDown={this.onKeyDown}
|
||||
className={cssNames("Select", this.themeClass, className)}
|
||||
classNamePrefix="Select"
|
||||
onChange={action(onChange)} // This is done so that all changes are actionable
|
||||
components={{
|
||||
...components,
|
||||
Menu: props => (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user