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