mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
cleanup Lens repo with tighter linting
Signed-off-by: Sebastian Malton <smalton@mirantis.com>
This commit is contained in:
1 parent
e468105143
commit
b1ff34879a
470 files changed
+8447
-7443
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { autobind } from "../../utils";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||
import { dockStore, DockTabData, TabKind } from "./dock.store";
|
||||
|
||||
@autobind()
|
||||
export class CreateResourceStore extends DockTabStore<string> {
|
||||
@@ -13,7 +13,7 @@ export class CreateResourceStore extends DockTabStore<string> {
|
||||
|
||||
export const createResourceStore = new CreateResourceStore();
|
||||
|
||||
export function createResourceTab(tabParams: Partial<IDockTab> = {}) {
|
||||
export function createResourceTab(tabParams: Partial<DockTabData> = {}): DockTabData {
|
||||
return dockStore.createTab({
|
||||
kind: TabKind.CREATE_RESOURCE,
|
||||
title: "Create resource",
|
||||
@@ -21,6 +21,6 @@ export function createResourceTab(tabParams: Partial<IDockTab> = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export function isCreateResourceTab(tab: IDockTab) {
|
||||
return tab && tab.kind === TabKind.CREATE_RESOURCE;
|
||||
export function isCreateResourceTab(tab: DockTabData): boolean {
|
||||
return tab?.kind === TabKind.CREATE_RESOURCE;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import "./create-resource.scss";
|
||||
|
||||
import React from "react";
|
||||
import jsYaml from "js-yaml"
|
||||
import jsYaml from "js-yaml";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { Plural, t, Trans } from "@lingui/macro";
|
||||
import { cssNames } from "../../utils";
|
||||
import { createResourceStore } from "./create-resource.store";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { DockTabData } from "./dock.store";
|
||||
import { EditorPanel } from "./editor-panel";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import { resourceApplierApi } from "../../api/endpoints/resource-applier.api";
|
||||
@@ -17,52 +17,56 @@ import { Notifications } from "../notifications";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
tab: IDockTab;
|
||||
tab: DockTabData;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class CreateResource extends React.Component<Props> {
|
||||
@observable error = ""
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
get tabId(): string {
|
||||
return this.props.tab.id || "";
|
||||
}
|
||||
|
||||
get data() {
|
||||
get data(): string {
|
||||
return createResourceStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
onChange = (value: string, error?: string) => {
|
||||
onChange = (value: string, error?: string): void => {
|
||||
createResourceStore.setData(this.tabId, value);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
create = async () => {
|
||||
if (this.error) return;
|
||||
create = async (): Promise<JSX.Element> => {
|
||||
if (this.error) {
|
||||
return;
|
||||
}
|
||||
const resources = jsYaml.safeLoadAll(this.data)
|
||||
.filter(v => !!v) // skip empty documents if "---" pasted at the beginning or end
|
||||
.filter(v => !!v); // skip empty documents if "---" pasted at the beginning or end
|
||||
const createdResources: string[] = [];
|
||||
const errors: string[] = [];
|
||||
await Promise.all(
|
||||
resources.map(data => {
|
||||
return resourceApplierApi.update(data)
|
||||
.then(item => createdResources.push(item.getName()))
|
||||
.catch((err: JsonApiErrorParsed) => errors.push(err.toString()))
|
||||
.catch((err: JsonApiErrorParsed) => errors.push(err.toString()));
|
||||
})
|
||||
);
|
||||
if (errors.length) {
|
||||
errors.forEach(Notifications.error);
|
||||
if (!createdResources.length) throw errors[0];
|
||||
if (!createdResources.length) {
|
||||
throw errors[0];
|
||||
}
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
<Plural value={createdResources.length} one="Resource" other="Resources"/>{" "}
|
||||
<Trans><b>{createdResources.join(", ")}</b> successfully created</Trans>
|
||||
</p>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { tabId, data, error, create, onChange } = this;
|
||||
const { className } = this.props;
|
||||
return (
|
||||
@@ -80,6 +84,6 @@ export class CreateResource extends React.Component<Props> {
|
||||
showNotifications={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { autorun, observable, reaction } from "mobx";
|
||||
import { autobind, createStorage } from "../../utils";
|
||||
import { autobind, StorageHelper } from "../../utils";
|
||||
import { dockStore, TabId } from "./dock.store";
|
||||
|
||||
interface Options<T = any> {
|
||||
@@ -16,7 +16,7 @@ export class DockTabStore<T = any> {
|
||||
|
||||
// auto-save to local-storage
|
||||
if (storageName) {
|
||||
const storage = createStorage<[TabId, T][]>(storageName, []);
|
||||
const storage = new StorageHelper<[TabId, T][]>(storageName, []);
|
||||
this.data.replace(storage.get());
|
||||
reaction(() => this.serializeData(), (data: T | any) => storage.set(data));
|
||||
}
|
||||
@@ -28,31 +28,34 @@ export class DockTabStore<T = any> {
|
||||
if (!currentTabs.includes(tabId)) {
|
||||
this.clearData(tabId);
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected serializeData() {
|
||||
protected serializeData(): [string, Partial<T>][] {
|
||||
const { storageSerializer } = this.options;
|
||||
return Array.from(this.data).map(([tabId, tabData]) => {
|
||||
if (storageSerializer) return [tabId, storageSerializer(tabData)]
|
||||
return [tabId, tabData];
|
||||
})
|
||||
return Array.from(this.data)
|
||||
.map(([tabId, tabData]) => {
|
||||
if (storageSerializer) {
|
||||
return [tabId, storageSerializer(tabData)];
|
||||
}
|
||||
return [tabId, tabData];
|
||||
});
|
||||
}
|
||||
|
||||
getData(tabId: TabId) {
|
||||
getData(tabId: TabId): T {
|
||||
return this.data.get(tabId);
|
||||
}
|
||||
|
||||
setData(tabId: TabId, data: T) {
|
||||
setData(tabId: TabId, data: T): void {
|
||||
this.data.set(tabId, data);
|
||||
}
|
||||
|
||||
clearData(tabId: TabId) {
|
||||
clearData(tabId: TabId): void {
|
||||
this.data.delete(tabId);
|
||||
}
|
||||
|
||||
reset() {
|
||||
reset(): void {
|
||||
this.data.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,30 @@
|
||||
import "./dock-tab.scss"
|
||||
import "./dock-tab.scss";
|
||||
|
||||
import React from "react"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { t } from "@lingui/macro";
|
||||
import { autobind, cssNames, prevDefault } from "../../utils";
|
||||
import { dockStore, IDockTab } from "./dock.store";
|
||||
import { dockStore, DockTabData } from "./dock.store";
|
||||
import { Tab, TabProps } from "../tabs";
|
||||
import { Icon } from "../icon";
|
||||
import { _i18n } from "../../i18n";
|
||||
|
||||
export interface DockTabProps extends TabProps<IDockTab> {
|
||||
export interface DockTabProps extends TabProps<DockTabData> {
|
||||
moreActions?: React.ReactNode;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class DockTab extends React.Component<DockTabProps> {
|
||||
get tabId() {
|
||||
return this.props.value.id;
|
||||
get tabId(): string {
|
||||
return this.props.value.id || "";
|
||||
}
|
||||
|
||||
@autobind()
|
||||
close() {
|
||||
close(): void {
|
||||
dockStore.closeTab(this.tabId);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { className, moreActions, ...tabProps } = this.props;
|
||||
const { title, pinned } = tabProps.value;
|
||||
const label = (
|
||||
@@ -39,13 +39,13 @@ export class DockTab extends React.Component<DockTabProps> {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
return (
|
||||
<Tab
|
||||
{...tabProps}
|
||||
className={cssNames("DockTab", className, { pinned })}
|
||||
label={label}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import MD5 from "crypto-js/md5";
|
||||
import { action, computed, IReactionOptions, observable, reaction } from "mobx";
|
||||
import { autobind, createStorage } from "../../utils";
|
||||
import throttle from "lodash/throttle"
|
||||
import { action, computed, IReactionOptions, observable, reaction, IReactionDisposer } from "mobx";
|
||||
import { autobind, StorageHelper } from "../../utils";
|
||||
import throttle from "lodash/throttle";
|
||||
|
||||
export type TabId = string;
|
||||
|
||||
@@ -13,7 +13,7 @@ export enum TabKind {
|
||||
UPGRADE_CHART = "upgrade-chart",
|
||||
}
|
||||
|
||||
export interface IDockTab {
|
||||
export interface DockTabData {
|
||||
id?: TabId;
|
||||
kind: TabKind;
|
||||
title?: string;
|
||||
@@ -22,29 +22,29 @@ export interface IDockTab {
|
||||
|
||||
@autobind()
|
||||
export class DockStore {
|
||||
protected initialTabs: IDockTab[] = [
|
||||
protected initialTabs: DockTabData[] = [
|
||||
{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal" },
|
||||
];
|
||||
|
||||
protected storage = createStorage("dock", {}); // keep settings in localStorage
|
||||
protected storage = new StorageHelper("dock", {}); // keep settings in localStorage
|
||||
public defaultTabId = this.initialTabs[0].id;
|
||||
public minHeight = 100;
|
||||
|
||||
@observable isOpen = false;
|
||||
@observable fullSize = false;
|
||||
@observable height = this.defaultHeight;
|
||||
@observable tabs = observable.array<IDockTab>(this.initialTabs);
|
||||
@observable tabs = observable.array<DockTabData>(this.initialTabs);
|
||||
@observable selectedTabId = this.defaultTabId;
|
||||
|
||||
@computed get selectedTab() {
|
||||
@computed get selectedTab(): DockTabData {
|
||||
return this.tabs.find(tab => tab.id === this.selectedTabId);
|
||||
}
|
||||
|
||||
get defaultHeight() {
|
||||
get defaultHeight(): number {
|
||||
return Math.round(window.innerHeight / 2.5);
|
||||
}
|
||||
|
||||
get maxHeight() {
|
||||
get maxHeight(): number {
|
||||
const mainLayoutHeader = 40;
|
||||
const mainLayoutTabs = 33;
|
||||
const mainLayoutMargin = 16;
|
||||
@@ -69,7 +69,7 @@ export class DockStore {
|
||||
window.addEventListener("resize", throttle(this.checkMaxHeight, 250));
|
||||
}
|
||||
|
||||
protected checkMaxHeight() {
|
||||
protected checkMaxHeight(): void {
|
||||
if (!this.height) {
|
||||
this.setHeight(this.defaultHeight || this.minHeight);
|
||||
}
|
||||
@@ -78,20 +78,20 @@ export class DockStore {
|
||||
}
|
||||
}
|
||||
|
||||
onResize(callback: () => void, options?: IReactionOptions) {
|
||||
onResize(callback: () => void, options?: IReactionOptions): IReactionDisposer {
|
||||
return reaction(() => [this.height, this.fullSize], callback, options);
|
||||
}
|
||||
|
||||
onTabChange(callback: (tabId: TabId) => void, options?: IReactionOptions) {
|
||||
onTabChange(callback: (tabId: TabId) => void, options?: IReactionOptions): IReactionDisposer {
|
||||
return reaction(() => this.selectedTabId, callback, options);
|
||||
}
|
||||
|
||||
hasTabs() {
|
||||
hasTabs(): boolean {
|
||||
return this.tabs.length > 0;
|
||||
}
|
||||
|
||||
@action
|
||||
open(fullSize?: boolean) {
|
||||
open(fullSize?: boolean): void {
|
||||
this.isOpen = true;
|
||||
if (typeof fullSize === "boolean") {
|
||||
this.fullSize = fullSize;
|
||||
@@ -99,27 +99,32 @@ export class DockStore {
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
close(): void {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
@action
|
||||
toggle() {
|
||||
if (this.isOpen) this.close();
|
||||
else this.open();
|
||||
toggle(): void {
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleFillSize() {
|
||||
if (!this.isOpen) this.open();
|
||||
toggleFillSize(): void {
|
||||
if (!this.isOpen) {
|
||||
this.open();
|
||||
}
|
||||
this.fullSize = !this.fullSize;
|
||||
}
|
||||
|
||||
getTabById(tabId: TabId) {
|
||||
getTabById(tabId: TabId): DockTabData {
|
||||
return this.tabs.find(tab => tab.id === tabId);
|
||||
}
|
||||
|
||||
protected getNewTabNumber(kind: TabKind) {
|
||||
protected getNewTabNumber(kind: TabKind): number {
|
||||
const tabNumbers = this.tabs
|
||||
.filter(tab => tab.kind === kind)
|
||||
.map(tab => {
|
||||
@@ -127,17 +132,21 @@ export class DockStore {
|
||||
return tabNumber === 0 ? 1 : tabNumber; // tab without a number is first
|
||||
});
|
||||
for (let i = 1; ; i++) {
|
||||
if (!tabNumbers.includes(i)) return i;
|
||||
if (!tabNumbers.includes(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
createTab(anonTab: IDockTab, addNumber = true): IDockTab {
|
||||
createTab(anonTab: DockTabData, addNumber = true): DockTabData {
|
||||
const tabId = MD5(Math.random().toString() + Date.now()).toString();
|
||||
const tab: IDockTab = { id: tabId, ...anonTab };
|
||||
const tab: DockTabData = { id: tabId, ...anonTab };
|
||||
if (addNumber) {
|
||||
const tabNumber = this.getNewTabNumber(tab.kind);
|
||||
if (tabNumber > 1) tab.title += ` (${tabNumber})`
|
||||
if (tabNumber > 1) {
|
||||
tab.title += ` (${tabNumber})`;
|
||||
}
|
||||
}
|
||||
this.tabs.push(tab);
|
||||
this.selectTab(tab.id);
|
||||
@@ -146,7 +155,7 @@ export class DockStore {
|
||||
}
|
||||
|
||||
@action
|
||||
async closeTab(tabId: TabId) {
|
||||
async closeTab(tabId: TabId): Promise<void> {
|
||||
const tab = this.getTabById(tabId);
|
||||
if (!tab || tab.pinned) {
|
||||
return;
|
||||
@@ -158,11 +167,12 @@ export class DockStore {
|
||||
if (newTab.kind === TabKind.TERMINAL) {
|
||||
// close the dock when selected sibling inactive terminal tab
|
||||
const { terminalStore } = await import("./terminal.store");
|
||||
if (!terminalStore.isConnected(newTab.id)) this.close();
|
||||
if (!terminalStore.isConnected(newTab.id)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
this.selectTab(newTab.id);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.selectedTabId = null;
|
||||
this.close();
|
||||
}
|
||||
@@ -170,18 +180,18 @@ export class DockStore {
|
||||
}
|
||||
|
||||
@action
|
||||
selectTab(tabId: TabId) {
|
||||
selectTab(tabId: TabId): void {
|
||||
const tab = this.getTabById(tabId);
|
||||
this.selectedTabId = tab ? tab.id : null;
|
||||
}
|
||||
|
||||
@action
|
||||
setHeight(height: number) {
|
||||
setHeight(height: number): void {
|
||||
this.height = Math.max(0, Math.min(height, this.maxHeight));
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
reset(): void {
|
||||
this.selectedTabId = this.defaultTabId;
|
||||
this.tabs.replace(this.initialTabs);
|
||||
this.height = this.defaultHeight;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Icon } from "../icon";
|
||||
import { Tabs } from "../tabs/tabs";
|
||||
import { MenuItem } from "../menu";
|
||||
import { MenuActions } from "../menu/menu-actions";
|
||||
import { dockStore, IDockTab } from "./dock.store";
|
||||
import { dockStore, DockTabData } from "./dock.store";
|
||||
import { DockTab } from "./dock-tab";
|
||||
import { TerminalTab } from "./terminal-tab";
|
||||
import { TerminalWindow } from "./terminal-window";
|
||||
@@ -29,7 +29,7 @@ interface Props {
|
||||
|
||||
@observer
|
||||
export class Dock extends React.Component<Props> {
|
||||
onResizeStart = () => {
|
||||
onResizeStart = (): void => {
|
||||
const { isOpen, open, setHeight, minHeight } = dockStore;
|
||||
if (!isOpen) {
|
||||
open();
|
||||
@@ -37,53 +37,59 @@ export class Dock extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
onResize = ({ offsetY }: DraggableState) => {
|
||||
onResize = ({ offsetY }: DraggableState): void => {
|
||||
const { isOpen, close, height, setHeight, minHeight, defaultHeight } = dockStore;
|
||||
const newHeight = height + offsetY;
|
||||
if (height > newHeight && newHeight < minHeight) {
|
||||
setHeight(defaultHeight);
|
||||
close();
|
||||
}
|
||||
else if (isOpen) {
|
||||
} else if (isOpen) {
|
||||
setHeight(newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
onKeydown = (evt: React.KeyboardEvent<HTMLElement>) => {
|
||||
const { close, closeTab, selectedTab, fullSize, toggleFillSize } = dockStore;
|
||||
if (!selectedTab) return;
|
||||
onKeydown = (evt: React.KeyboardEvent<HTMLElement>): void => {
|
||||
const { close, closeTab, selectedTab } = dockStore;
|
||||
if (!selectedTab) {
|
||||
return;
|
||||
}
|
||||
const { code, ctrlKey, shiftKey } = evt.nativeEvent;
|
||||
if (shiftKey && code === "Escape") {
|
||||
close();
|
||||
}
|
||||
if (ctrlKey && code === "KeyW") {
|
||||
if (selectedTab.pinned) close();
|
||||
else closeTab(selectedTab.id);
|
||||
if (selectedTab.pinned) {
|
||||
close();
|
||||
} else {
|
||||
closeTab(selectedTab.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChangeTab = (tab: IDockTab) => {
|
||||
onChangeTab = (tab: DockTabData): void => {
|
||||
const { open, selectTab } = dockStore;
|
||||
open();
|
||||
selectTab(tab.id);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
renderTab(tab: IDockTab) {
|
||||
renderTab(tab: DockTabData): JSX.Element {
|
||||
if (isTerminalTab(tab)) {
|
||||
return <TerminalTab value={tab}/>
|
||||
return <TerminalTab value={tab}/>;
|
||||
}
|
||||
if (isCreateResourceTab(tab) || isEditResourceTab(tab)) {
|
||||
return <DockTab value={tab} icon="edit"/>
|
||||
return <DockTab value={tab} icon="edit"/>;
|
||||
}
|
||||
if (isInstallChartTab(tab) || isUpgradeChartTab(tab)) {
|
||||
return <DockTab value={tab} icon={<Icon svg="install"/>}/>
|
||||
return <DockTab value={tab} icon={<Icon svg="install"/>}/>;
|
||||
}
|
||||
}
|
||||
|
||||
renderTabContent() {
|
||||
renderTabContent(): JSX.Element {
|
||||
const { isOpen, height, selectedTab: tab } = dockStore;
|
||||
if (!isOpen || !tab) return;
|
||||
if (!isOpen || !tab) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="tab-content" style={{ flexBasis: height }}>
|
||||
{isCreateResourceTab(tab) && <CreateResource tab={tab}/>}
|
||||
@@ -92,10 +98,10 @@ export class Dock extends React.Component<Props> {
|
||||
{isUpgradeChartTab(tab) && <UpgradeChart tab={tab}/>}
|
||||
{isTerminalTab(tab) && <TerminalWindow tab={tab}/>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { className } = this.props;
|
||||
const { isOpen, toggle, tabs, toggleFillSize, selectedTab, hasTabs, fullSize } = dockStore;
|
||||
return (
|
||||
@@ -115,16 +121,21 @@ export class Dock extends React.Component<Props> {
|
||||
autoFocus={isOpen}
|
||||
className="dock-tabs"
|
||||
value={selectedTab} onChange={this.onChangeTab}
|
||||
children={tabs.map(tab => <Fragment key={tab.id}>{this.renderTab(tab)}</Fragment>)}
|
||||
/>
|
||||
>
|
||||
{tabs.map(tab => <Fragment key={tab.id}>{this.renderTab(tab)}</Fragment>)}
|
||||
</Tabs>
|
||||
<div className="toolbar flex gaps align-center box grow">
|
||||
<div className="dock-menu box grow">
|
||||
<MenuActions usePortal triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: <Trans>New tab</Trans> }} closeOnScroll={false}>
|
||||
<MenuItem className="create-terminal-tab" onClick={() => createTerminalTab()}>
|
||||
<MenuItem className="create-terminal-tab" onClick={(): void => {
|
||||
createTerminalTab();
|
||||
}}>
|
||||
<Icon small svg="terminal" size={15}/>
|
||||
<Trans>Terminal session</Trans>
|
||||
</MenuItem>
|
||||
<MenuItem className="create-resource-tab" onClick={() => createResourceTab()}>
|
||||
<MenuItem className="create-resource-tab" onClick={(): void => {
|
||||
createResourceTab();
|
||||
}}>
|
||||
<Icon small material="create"/>
|
||||
<Trans>Create resource</Trans>
|
||||
</MenuItem>
|
||||
@@ -148,6 +159,6 @@ export class Dock extends React.Component<Props> {
|
||||
</div>
|
||||
{this.renderTabContent()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { autobind, noop } from "../../utils";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { autorun, IReactionDisposer } from "mobx";
|
||||
import { dockStore, IDockTab, TabKind } from "./dock.store";
|
||||
import { dockStore, DockTabData, TabKind } from "./dock.store";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
@@ -17,7 +17,7 @@ export class EditResourceStore extends DockTabStore<KubeEditResource> {
|
||||
constructor() {
|
||||
super({
|
||||
storageName: "edit_resource_store",
|
||||
storageSerializer: ({ draft, ...data }) => data, // skip saving draft in local-storage
|
||||
storageSerializer: ({ draft: _draft, ...data }) => data, // skip saving draft in local-storage
|
||||
});
|
||||
|
||||
autorun(() => {
|
||||
@@ -33,38 +33,37 @@ export class EditResourceStore extends DockTabStore<KubeEditResource> {
|
||||
// preload resource for editing
|
||||
if (!obj && !store.isLoaded && !store.isLoading && isActiveTab) {
|
||||
store.loadFromPath(resource).catch(noop);
|
||||
}
|
||||
// auto-close tab when resource removed from store
|
||||
else if (!obj && store.isLoaded) {
|
||||
} else if (!obj && store.isLoaded) {
|
||||
// auto-close tab when resource removed from store
|
||||
dockStore.closeTab(tabId);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
delay: 100 // make sure all stores initialized
|
||||
}));
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getTabByResource(object: KubeObject): IDockTab {
|
||||
const [tabId] = Array.from(this.data).find(([tabId, { resource }]) => {
|
||||
getTabByResource(object: KubeObject): DockTabData {
|
||||
const [tabId] = Array.from(this.data).find(([_tabId, { resource }]) => {
|
||||
return object.selfLink === resource;
|
||||
}) || [];
|
||||
return dockStore.getTabById(tabId);
|
||||
}
|
||||
|
||||
reset() {
|
||||
reset(): void {
|
||||
super.reset();
|
||||
Array.from(this.watchers).forEach(([tabId, dispose]) => {
|
||||
this.watchers.delete(tabId);
|
||||
dispose();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const editResourceStore = new EditResourceStore();
|
||||
|
||||
export function editResourceTab(object: KubeObject, tabParams: Partial<IDockTab> = {}) {
|
||||
export function editResourceTab(object: KubeObject, tabParams: Partial<DockTabData> = {}): DockTabData {
|
||||
// use existing tab if already opened
|
||||
let tab = editResourceStore.getTabByResource(object);
|
||||
if (tab) {
|
||||
@@ -85,6 +84,6 @@ export function editResourceTab(object: KubeObject, tabParams: Partial<IDockTab>
|
||||
return tab;
|
||||
}
|
||||
|
||||
export function isEditResourceTab(tab: IDockTab) {
|
||||
return tab && tab.kind === TabKind.EDIT_RESOURCE;
|
||||
export function isEditResourceTab(tab: DockTabData): boolean {
|
||||
return tab?.kind === TabKind.EDIT_RESOURCE;
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import "./edit-resource.scss";
|
||||
import React from "react";
|
||||
import { autorun, observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import jsYaml from "js-yaml"
|
||||
import jsYaml from "js-yaml";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { DockTabData } from "./dock.store";
|
||||
import { cssNames } from "../../utils";
|
||||
import { editResourceStore } from "./edit-resource.store";
|
||||
import { editResourceStore, KubeEditResource } from "./edit-resource.store";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import { Badge } from "../badge";
|
||||
import { EditorPanel } from "./editor-panel";
|
||||
@@ -18,7 +18,7 @@ import { KubeObject } from "../../api/kube-object";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
tab: IDockTab;
|
||||
tab: DockTabData;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -27,17 +27,19 @@ export class EditResource extends React.Component<Props> {
|
||||
|
||||
@disposeOnUnmount
|
||||
autoDumpResourceOnInit = autorun(() => {
|
||||
if (!this.tabData) return;
|
||||
if (!this.tabData) {
|
||||
return;
|
||||
}
|
||||
if (this.tabData.draft === undefined && this.resource) {
|
||||
this.saveDraft(this.resource);
|
||||
}
|
||||
});
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
get tabId(): string {
|
||||
return this.props.tab.id || "";
|
||||
}
|
||||
|
||||
get tabData() {
|
||||
get tabData(): KubeEditResource {
|
||||
return editResourceStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
@@ -49,7 +51,7 @@ export class EditResource extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
saveDraft(draft: string | object) {
|
||||
saveDraft(draft: string | object): void {
|
||||
if (typeof draft === "object") {
|
||||
draft = draft ? jsYaml.dump(draft) : undefined;
|
||||
}
|
||||
@@ -59,12 +61,12 @@ export class EditResource extends React.Component<Props> {
|
||||
});
|
||||
}
|
||||
|
||||
onChange = (draft: string, error?: string) => {
|
||||
onChange = (draft: string, error?: string): void => {
|
||||
this.error = error;
|
||||
this.saveDraft(draft);
|
||||
}
|
||||
|
||||
save = async () => {
|
||||
save = async (): Promise<JSX.Element> => {
|
||||
if (this.error) {
|
||||
return;
|
||||
}
|
||||
@@ -81,7 +83,7 @@ export class EditResource extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { tabId, resource, tabData, error, onChange, save } = this;
|
||||
const { draft } = tabData;
|
||||
if (!resource || draft === undefined) {
|
||||
@@ -110,6 +112,6 @@ export class EditResource extends React.Component<Props> {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import jsYaml from "js-yaml"
|
||||
import jsYaml from "js-yaml";
|
||||
import { observable } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { cssNames } from "../../utils";
|
||||
@@ -23,17 +23,17 @@ export class EditorPanel extends React.Component<Props> {
|
||||
|
||||
@observable yamlError = ""
|
||||
|
||||
componentDidMount() {
|
||||
componentDidMount(): void {
|
||||
// validate and run callback with optional error
|
||||
this.onChange(this.props.value || "");
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
dockStore.onTabChange(this.onTabChange, { delay: 250 }),
|
||||
dockStore.onResize(this.onResize, { delay: 250 }),
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
validate(value: string) {
|
||||
validate(value: string): void {
|
||||
try {
|
||||
jsYaml.safeLoadAll(value);
|
||||
this.yamlError = "";
|
||||
@@ -42,26 +42,26 @@ export class EditorPanel extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
onTabChange = () => {
|
||||
onTabChange = (): void => {
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
onResize = () => {
|
||||
onResize = (): void => {
|
||||
this.editor.resize();
|
||||
}
|
||||
|
||||
onCursorPosChange = (pos: Ace.Point) => {
|
||||
onCursorPosChange = (pos: Ace.Point): void => {
|
||||
EditorPanel.cursorPos.setData(this.props.tabId, pos);
|
||||
}
|
||||
|
||||
onChange = (value: string) => {
|
||||
onChange = (value: string): void => {
|
||||
this.validate(value);
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(value, this.yamlError);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { value, tabId } = this.props;
|
||||
let { className } = this.props;
|
||||
className = cssNames("EditorPanel", className);
|
||||
@@ -74,8 +74,10 @@ export class EditorPanel extends React.Component<Props> {
|
||||
cursorPos={cursorPos}
|
||||
onChange={this.onChange}
|
||||
onCursorPosChange={this.onCursorPosChange}
|
||||
ref={e => this.editor = e}
|
||||
ref={(e): void => {
|
||||
this.editor = e;
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export * from "./dock"
|
||||
export * from "./dock";
|
||||
@@ -42,21 +42,21 @@ export class InfoPanel extends Component<Props> {
|
||||
@observable error = "";
|
||||
@observable waiting = false;
|
||||
|
||||
componentDidMount() {
|
||||
componentDidMount(): void {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.tabId, () => {
|
||||
this.result = ""
|
||||
this.error = ""
|
||||
this.waiting = false
|
||||
this.result = "";
|
||||
this.error = "";
|
||||
this.waiting = false;
|
||||
})
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@computed get errorInfo() {
|
||||
@computed get errorInfo(): string {
|
||||
return this.error || this.props.error;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
submit = async (): Promise<void> => {
|
||||
const { showNotifications } = this.props;
|
||||
this.result = "";
|
||||
this.error = "";
|
||||
@@ -65,24 +65,28 @@ export class InfoPanel extends Component<Props> {
|
||||
this.result = await this.props.submit().finally(() => {
|
||||
this.waiting = false;
|
||||
});
|
||||
if (showNotifications) Notifications.ok(this.result);
|
||||
if (showNotifications) {
|
||||
Notifications.ok(this.result);
|
||||
}
|
||||
} catch (error) {
|
||||
this.error = error.toString();
|
||||
if (showNotifications) Notifications.error(this.error);
|
||||
if (showNotifications) {
|
||||
Notifications.error(this.error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
submitAndClose = async () => {
|
||||
submitAndClose = async (): Promise<void> => {
|
||||
await this.submit();
|
||||
this.close();
|
||||
}
|
||||
|
||||
close = () => {
|
||||
close = (): void => {
|
||||
dockStore.closeTab(this.props.tabId);
|
||||
}
|
||||
|
||||
renderInfo() {
|
||||
renderInfo(): JSX.Element {
|
||||
if (!this.props.showInlineInfo) {
|
||||
return;
|
||||
}
|
||||
@@ -101,10 +105,10 @@ export class InfoPanel extends Component<Props> {
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { className, controls, submitLabel, disableSubmit, error, submittingMessage, showSubmitClose } = this.props;
|
||||
const { submit, close, submitAndClose, waiting } = this;
|
||||
const isDisabled = !!(disableSubmit || waiting || error);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { action, autorun } from "mobx";
|
||||
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
||||
import { dockStore, DockTabData, TabId, TabKind } from "./dock.store";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { t } from "@lingui/macro";
|
||||
import { HelmChart, helmChartsApi } from "../../api/endpoints/helm-charts.api";
|
||||
import { IReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
|
||||
import { ReleaseUpdateDetails } from "../../api/endpoints/helm-releases.api";
|
||||
import { _i18n } from "../../i18n";
|
||||
|
||||
export interface IChartInstallData {
|
||||
export interface ChartInstallData {
|
||||
name: string;
|
||||
repo: string;
|
||||
version: string;
|
||||
@@ -17,9 +17,13 @@ export interface IChartInstallData {
|
||||
lastVersion?: boolean;
|
||||
}
|
||||
|
||||
export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
export function isInstallChartTab(tab: DockTabData): boolean {
|
||||
return tab?.kind === TabKind.INSTALL_CHART;
|
||||
}
|
||||
|
||||
export class InstallChartStore extends DockTabStore<ChartInstallData> {
|
||||
public versions = new DockTabStore<string[]>();
|
||||
public details = new DockTabStore<IReleaseUpdateDetails>();
|
||||
public details = new DockTabStore<ReleaseUpdateDetails>();
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -27,25 +31,27 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
});
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = dockStore;
|
||||
if (!isInstallChartTab(selectedTab)) return;
|
||||
if (!isInstallChartTab(selectedTab)) {
|
||||
return;
|
||||
}
|
||||
if (isOpen) {
|
||||
this.loadData();
|
||||
}
|
||||
}, { delay: 250 })
|
||||
}, { delay: 250 });
|
||||
}
|
||||
|
||||
@action
|
||||
async loadData(tabId = dockStore.selectedTabId) {
|
||||
async loadData(tabId = dockStore.selectedTabId): Promise<void> {
|
||||
const { values } = this.getData(tabId);
|
||||
const versions = this.versions.getData(tabId);
|
||||
return Promise.all([
|
||||
await Promise.all([
|
||||
!versions && this.loadVersions(tabId),
|
||||
!values && this.loadValues(tabId),
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
@action
|
||||
async loadVersions(tabId: TabId) {
|
||||
async loadVersions(tabId: TabId): Promise<void> {
|
||||
const { repo, name } = this.getData(tabId);
|
||||
this.versions.clearData(tabId); // reset
|
||||
const charts = await helmChartsApi.get(repo, name);
|
||||
@@ -54,13 +60,15 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
}
|
||||
|
||||
@action
|
||||
async loadValues(tabId: TabId) {
|
||||
async loadValues(tabId: TabId): Promise<void> {
|
||||
const data = this.getData(tabId);
|
||||
const { repo, name, version } = data;
|
||||
let values = "";
|
||||
const fetchValues = async (retry = 1, maxRetries = 3) => {
|
||||
const fetchValues = async (retry = 1, maxRetries = 3): Promise<void> => {
|
||||
values = await helmChartsApi.getValues(repo, name, version);
|
||||
if (values || retry == maxRetries) return;
|
||||
if (values || retry == maxRetries) {
|
||||
return;
|
||||
}
|
||||
await fetchValues(retry + 1);
|
||||
};
|
||||
this.setData(tabId, { ...data, values: undefined }); // reset
|
||||
@@ -71,7 +79,7 @@ export class InstallChartStore extends DockTabStore<IChartInstallData> {
|
||||
|
||||
export const installChartStore = new InstallChartStore();
|
||||
|
||||
export function createInstallChartTab(chart: HelmChart, tabParams: Partial<IDockTab> = {}) {
|
||||
export function createInstallChartTab(chart: HelmChart, tabParams: Partial<DockTabData> = {}): DockTabData {
|
||||
const { name, repo, version } = chart;
|
||||
|
||||
const tab = dockStore.createTab({
|
||||
@@ -91,7 +99,3 @@ export function createInstallChartTab(chart: HelmChart, tabParams: Partial<IDock
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
export function isInstallChartTab(tab: IDockTab) {
|
||||
return tab && tab.kind === TabKind.INSTALL_CHART;
|
||||
}
|
||||
@@ -4,12 +4,12 @@ import React, { Component } from "react";
|
||||
import { observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { dockStore, IDockTab } from "./dock.store";
|
||||
import { dockStore, DockTabData } from "./dock.store";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import { Badge } from "../badge";
|
||||
import { NamespaceSelect } from "../+namespaces/namespace-select";
|
||||
import { autobind, prevDefault } from "../../utils";
|
||||
import { IChartInstallData, installChartStore } from "./install-chart.store";
|
||||
import { ChartInstallData, installChartStore } from "./install-chart.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { Icon } from "../icon";
|
||||
import { Button } from "../button";
|
||||
@@ -21,9 +21,10 @@ import { Input } from "../input";
|
||||
import { EditorPanel } from "./editor-panel";
|
||||
import { navigate } from "../../navigation";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { ReleaseUpdateDetails } from "client/api/endpoints/helm-releases.api";
|
||||
|
||||
interface Props {
|
||||
tab: IDockTab;
|
||||
tab: DockTabData;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -31,28 +32,28 @@ export class InstallChart extends Component<Props> {
|
||||
@observable error = "";
|
||||
@observable showNotes = false;
|
||||
|
||||
get values() {
|
||||
return this.chartData.values;
|
||||
get values(): string {
|
||||
return this.chartData.values || "";
|
||||
}
|
||||
|
||||
get chartData() {
|
||||
get chartData(): ChartInstallData {
|
||||
return installChartStore.getData(this.tabId);
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
get tabId(): string {
|
||||
return this.props.tab.id || "";
|
||||
}
|
||||
|
||||
get versions() {
|
||||
get versions(): string[] {
|
||||
return installChartStore.versions.getData(this.tabId);
|
||||
}
|
||||
|
||||
get releaseDetails() {
|
||||
get releaseDetails(): ReleaseUpdateDetails {
|
||||
return installChartStore.details.getData(this.tabId);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
viewRelease() {
|
||||
viewRelease(): void {
|
||||
const { release } = this.releaseDetails;
|
||||
navigate(releaseURL({
|
||||
params: {
|
||||
@@ -64,35 +65,35 @@ export class InstallChart extends Component<Props> {
|
||||
}
|
||||
|
||||
@autobind()
|
||||
save(data: Partial<IChartInstallData>) {
|
||||
save(data: Partial<ChartInstallData>): void {
|
||||
const chart = { ...this.chartData, ...data };
|
||||
installChartStore.setData(this.tabId, chart);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onVersionChange(option: SelectOption) {
|
||||
onVersionChange(option: SelectOption): void {
|
||||
const version = option.value;
|
||||
this.save({ version, values: "" });
|
||||
installChartStore.loadValues(this.tabId);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onValuesChange(values: string, error?: string) {
|
||||
onValuesChange(values: string, error?: string): void {
|
||||
this.error = error;
|
||||
this.save({ values });
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onNamespaceChange(opt: SelectOption) {
|
||||
onNamespaceChange(opt: SelectOption): void {
|
||||
this.save({ namespace: opt.value });
|
||||
}
|
||||
|
||||
@autobind()
|
||||
onReleaseNameChange(name: string) {
|
||||
onReleaseNameChange(name: string): void {
|
||||
this.save({ releaseName: name });
|
||||
}
|
||||
|
||||
install = async () => {
|
||||
install = async (): Promise<JSX.Element> => {
|
||||
const { repo, name, version, namespace, values, releaseName } = this.chartData;
|
||||
const details = await releaseStore.create({
|
||||
name: releaseName || undefined,
|
||||
@@ -105,7 +106,7 @@ export class InstallChart extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { tabId, chartData, values, versions, install } = this;
|
||||
if (!chartData || chartData.values === undefined || !versions) {
|
||||
return <Spinner center/>;
|
||||
@@ -127,17 +128,21 @@ export class InstallChart extends Component<Props> {
|
||||
<Button
|
||||
plain active
|
||||
label={_i18n._(t`Show Notes`)}
|
||||
onClick={() => this.showNotes = true}
|
||||
onClick={(): void => {
|
||||
this.showNotes = true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<LogsDialog
|
||||
title={_i18n._(t`Helm Chart Install`)}
|
||||
isOpen={this.showNotes}
|
||||
close={() => this.showNotes = false}
|
||||
close={(): void => {
|
||||
this.showNotes = false;
|
||||
}}
|
||||
logs={this.releaseDetails.log}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { repo, name, version, namespace, releaseName } = chartData;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./terminal-tab.scss"
|
||||
import "./terminal-tab.scss";
|
||||
|
||||
import React from "react"
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { autobind, cssNames } from "../../utils";
|
||||
@@ -15,26 +15,26 @@ interface Props extends DockTabProps {
|
||||
|
||||
@observer
|
||||
export class TerminalTab extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
componentDidMount(): void {
|
||||
reaction(() => this.isDisconnected === true, () => {
|
||||
dockStore.closeTab(this.tabId)
|
||||
})
|
||||
dockStore.closeTab(this.tabId);
|
||||
});
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.value.id;
|
||||
get tabId(): string {
|
||||
return this.props.value.id || "";
|
||||
}
|
||||
|
||||
get isDisconnected() {
|
||||
get isDisconnected(): boolean {
|
||||
return terminalStore.isDisconnected(this.tabId);
|
||||
}
|
||||
|
||||
@autobind()
|
||||
reconnect() {
|
||||
reconnect(): void {
|
||||
terminalStore.reconnect(this.tabId);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const tabIcon = <Icon svg="terminal"/>;
|
||||
const className = cssNames("TerminalTab", this.props.className, {
|
||||
disconnected: this.isDisconnected,
|
||||
@@ -55,6 +55,6 @@ export class TerminalTab extends React.Component<Props> {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import React from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { cssNames } from "../../utils";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { DockTabData } from "./dock.store";
|
||||
import { Terminal } from "./terminal";
|
||||
import { terminalStore } from "./terminal.store";
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
tab: IDockTab;
|
||||
tab: DockTabData;
|
||||
}
|
||||
|
||||
@observer
|
||||
@@ -19,27 +19,31 @@ export class TerminalWindow extends React.Component<Props> {
|
||||
public elem: HTMLElement;
|
||||
public terminal: Terminal;
|
||||
|
||||
componentDidMount() {
|
||||
componentDidMount(): void {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.tab.id, tabId => this.activate(tabId), {
|
||||
fireImmediately: true
|
||||
})
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
activate(tabId = this.props.tab.id) {
|
||||
if (this.terminal) this.terminal.detach(); // detach previous
|
||||
activate(tabId = this.props.tab.id): void {
|
||||
if (this.terminal) {
|
||||
this.terminal.detach();
|
||||
} // detach previous
|
||||
this.terminal = terminalStore.getTerminal(tabId);
|
||||
this.terminal.attachTo(this.elem);
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { className } = this.props;
|
||||
return (
|
||||
<div
|
||||
className={cssNames("TerminalWindow", className, themeStore.activeTheme.type)}
|
||||
ref={e => this.elem = e}
|
||||
ref={(e): void => {
|
||||
this.elem = e;
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,21 @@ import { t } from "@lingui/macro";
|
||||
import { autobind } from "../../utils";
|
||||
import { Terminal } from "./terminal";
|
||||
import { TerminalApi } from "../../api/terminal-api";
|
||||
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
||||
import { dockStore, DockTabData, TabId, TabKind } from "./dock.store";
|
||||
import { WebSocketApiState } from "../../api/websocket-api";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { themeStore } from "../../theme.store";
|
||||
|
||||
export interface ITerminalTab extends IDockTab {
|
||||
export interface TerminalTabData extends DockTabData {
|
||||
node?: string; // activate node shell mode
|
||||
}
|
||||
|
||||
export function isTerminalTab(tab: IDockTab) {
|
||||
return tab && tab.kind === TabKind.TERMINAL;
|
||||
export function isTerminalTab(tab: DockTabData): boolean {
|
||||
return tab?.kind === TabKind.TERMINAL;
|
||||
}
|
||||
|
||||
|
||||
export function createTerminalTab(tabParams: Partial<ITerminalTab> = {}) {
|
||||
export function createTerminalTab(tabParams: Partial<TerminalTabData> = {}): DockTabData {
|
||||
return dockStore.createTab({
|
||||
kind: TabKind.TERMINAL,
|
||||
title: _i18n._(t`Terminal`),
|
||||
@@ -34,7 +34,9 @@ export class TerminalStore {
|
||||
// connect active tab
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = dockStore;
|
||||
if (!isTerminalTab(selectedTab)) return;
|
||||
if (!isTerminalTab(selectedTab)) {
|
||||
return;
|
||||
}
|
||||
if (isOpen) {
|
||||
this.connect(selectedTab.id);
|
||||
}
|
||||
@@ -43,16 +45,18 @@ export class TerminalStore {
|
||||
autorun(() => {
|
||||
const currentTabs = dockStore.tabs.map(tab => tab.id);
|
||||
for (const [tabId] of this.connections) {
|
||||
if (!currentTabs.includes(tabId)) this.disconnect(tabId);
|
||||
if (!currentTabs.includes(tabId)) {
|
||||
this.disconnect(tabId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async connect(tabId: TabId) {
|
||||
async connect(tabId: TabId): Promise<void> {
|
||||
if (this.isConnected(tabId)) {
|
||||
return;
|
||||
}
|
||||
const tab: ITerminalTab = dockStore.getTabById(tabId);
|
||||
const tab: TerminalTabData = dockStore.getTabById(tabId);
|
||||
const api = new TerminalApi({
|
||||
id: tabId,
|
||||
node: tab.node,
|
||||
@@ -63,7 +67,7 @@ export class TerminalStore {
|
||||
this.terminals.set(tabId, terminal);
|
||||
}
|
||||
|
||||
disconnect(tabId: TabId) {
|
||||
disconnect(tabId: TabId): void {
|
||||
if (!this.isConnected(tabId)) {
|
||||
return;
|
||||
}
|
||||
@@ -75,29 +79,35 @@ export class TerminalStore {
|
||||
this.terminals.delete(tabId);
|
||||
}
|
||||
|
||||
reconnect(tabId: TabId) {
|
||||
reconnect(tabId: TabId): void {
|
||||
const terminalApi = this.connections.get(tabId);
|
||||
if (terminalApi) terminalApi.connect();
|
||||
if (terminalApi) {
|
||||
terminalApi.connect();
|
||||
}
|
||||
}
|
||||
|
||||
isConnected(tabId: TabId) {
|
||||
isConnected(tabId: TabId): boolean {
|
||||
return !!this.connections.get(tabId);
|
||||
}
|
||||
|
||||
isDisconnected(tabId: TabId) {
|
||||
isDisconnected(tabId: TabId): boolean {
|
||||
const terminalApi = this.connections.get(tabId);
|
||||
if (terminalApi) {
|
||||
return terminalApi.readyState === WebSocketApiState.CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
sendCommand(command: string, options: { enter?: boolean; newTab?: boolean; tabId?: TabId } = {}) {
|
||||
sendCommand(command: string, options: { enter?: boolean; newTab?: boolean; tabId?: TabId } = {}): void {
|
||||
const { enter, newTab, tabId } = options;
|
||||
const { selectTab, getTabById } = dockStore;
|
||||
|
||||
const tab = tabId && getTabById(tabId);
|
||||
if (tab) selectTab(tabId);
|
||||
if (newTab) createTerminalTab();
|
||||
if (tab) {
|
||||
selectTab(tabId);
|
||||
}
|
||||
if (newTab) {
|
||||
createTerminalTab();
|
||||
}
|
||||
|
||||
const terminalApi = this.connections.get(dockStore.selectedTabId);
|
||||
if (terminalApi) {
|
||||
@@ -105,11 +115,11 @@ export class TerminalStore {
|
||||
}
|
||||
}
|
||||
|
||||
getTerminal(tabId: TabId) {
|
||||
getTerminal(tabId: TabId): Terminal {
|
||||
return this.terminals.get(tabId);
|
||||
}
|
||||
|
||||
reset() {
|
||||
reset(): void {
|
||||
[...this.connections].forEach(([tabId]) => {
|
||||
this.disconnect(tabId);
|
||||
});
|
||||
|
||||
@@ -10,12 +10,12 @@ import { autobind } from "../../utils";
|
||||
export class Terminal {
|
||||
static spawningPool: HTMLElement;
|
||||
|
||||
static init() {
|
||||
static init(): void {
|
||||
// terminal element must be in DOM before attaching via xterm.open(elem)
|
||||
// https://xtermjs.org/docs/api/terminal/classes/terminal/#open
|
||||
const pool = document.createElement("div");
|
||||
pool.className = "terminal-init";
|
||||
pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden"
|
||||
pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden";
|
||||
document.body.appendChild(pool);
|
||||
Terminal.spawningPool = pool;
|
||||
}
|
||||
@@ -26,10 +26,10 @@ export class Terminal {
|
||||
public disposers: Function[] = [];
|
||||
|
||||
@autobind()
|
||||
protected setTheme(colors = themeStore.activeTheme.colors) {
|
||||
protected setTheme(colors = themeStore.activeTheme.colors): void {
|
||||
// Replacing keys stored in styles to format accepted by terminal
|
||||
// E.g. terminalBrightBlack -> brightBlack
|
||||
const colorPrefix = "terminal"
|
||||
const colorPrefix = "terminal";
|
||||
const terminalColors = Object.entries(colors)
|
||||
.filter(([name]) => name.startsWith(colorPrefix))
|
||||
.reduce<any>((colors, [name, color]) => {
|
||||
@@ -41,11 +41,11 @@ export class Terminal {
|
||||
this.xterm.setOption("theme", terminalColors);
|
||||
}
|
||||
|
||||
get elem() {
|
||||
get elem(): HTMLElement {
|
||||
return this.xterm.element;
|
||||
}
|
||||
|
||||
get viewport() {
|
||||
get viewport(): Element {
|
||||
return this.xterm.element.querySelector(".xterm-viewport");
|
||||
}
|
||||
|
||||
@@ -53,21 +53,21 @@ export class Terminal {
|
||||
this.init();
|
||||
}
|
||||
|
||||
get isActive() {
|
||||
get isActive(): boolean {
|
||||
const { isOpen, selectedTabId } = dockStore;
|
||||
return isOpen && selectedTabId === this.tabId;
|
||||
}
|
||||
|
||||
attachTo(parentElem: HTMLElement) {
|
||||
attachTo(parentElem: HTMLElement): void {
|
||||
parentElem.appendChild(this.elem);
|
||||
this.onActivate();
|
||||
}
|
||||
|
||||
detach() {
|
||||
detach(): void {
|
||||
Terminal.spawningPool.appendChild(this.elem);
|
||||
}
|
||||
|
||||
init() {
|
||||
init(): void {
|
||||
if (this.xterm) {
|
||||
return;
|
||||
}
|
||||
@@ -106,15 +106,17 @@ export class Terminal {
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.xterm) return;
|
||||
destroy(): void {
|
||||
if (!this.xterm) {
|
||||
return;
|
||||
}
|
||||
this.disposers.forEach(dispose => dispose());
|
||||
this.disposers = [];
|
||||
this.xterm.dispose();
|
||||
this.xterm = null;
|
||||
}
|
||||
|
||||
fit = () => {
|
||||
fit = (): void => {
|
||||
this.fitAddon.fit();
|
||||
const { cols, rows } = this.xterm;
|
||||
this.api.sendTerminalSize(cols, rows);
|
||||
@@ -122,39 +124,43 @@ export class Terminal {
|
||||
|
||||
fitLazy = debounce(this.fit, 250);
|
||||
|
||||
focus = () => {
|
||||
focus = (): void => {
|
||||
this.xterm.focus();
|
||||
}
|
||||
|
||||
onApiData = (data: string) => {
|
||||
onApiData = (data: string): void => {
|
||||
this.xterm.write(data);
|
||||
}
|
||||
|
||||
onData = (data: string) => {
|
||||
if (!this.api.isReady) return;
|
||||
onData = (data: string): void => {
|
||||
if (!this.api.isReady) {
|
||||
return;
|
||||
}
|
||||
this.api.sendCommand(data);
|
||||
}
|
||||
|
||||
onScroll = () => {
|
||||
onScroll = (): void => {
|
||||
this.scrollPos = this.viewport.scrollTop;
|
||||
}
|
||||
|
||||
onClear = () => {
|
||||
onClear = (): void => {
|
||||
this.xterm.clear();
|
||||
}
|
||||
|
||||
onResize = () => {
|
||||
if (!this.isActive) return;
|
||||
onResize = (): void => {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
this.fitLazy();
|
||||
}
|
||||
|
||||
onActivate = () => {
|
||||
onActivate = (): void => {
|
||||
this.fit();
|
||||
setTimeout(() => this.focus(), 250); // delay used to prevent focus on active tab
|
||||
this.viewport.scrollTop = this.scrollPos; // restore last scroll position
|
||||
}
|
||||
|
||||
onClickLink = (evt: MouseEvent, link: string) => {
|
||||
onClickLink = (evt: MouseEvent, link: string): void => {
|
||||
window.open(link, "_blank");
|
||||
}
|
||||
|
||||
@@ -166,7 +172,9 @@ export class Terminal {
|
||||
switch (code) {
|
||||
// Ctrl+C: prevent terminal exit on windows / linux (?)
|
||||
case "KeyC":
|
||||
if (this.xterm.hasSelection()) return false;
|
||||
if (this.xterm.hasSelection()) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
// Ctrl+W: prevent unexpected terminal tab closing, e.g. editing file in vim
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { action, autorun, IReactionDisposer, reaction } from "mobx";
|
||||
import { t } from "@lingui/macro";
|
||||
import { dockStore, IDockTab, TabId, TabKind } from "./dock.store";
|
||||
import { dockStore, DockTabData, TabId, TabKind } from "./dock.store";
|
||||
import { DockTabStore } from "./dock-tab.store";
|
||||
import { HelmRelease, helmReleasesApi } from "../../api/endpoints/helm-releases.api";
|
||||
import { releaseStore } from "../+apps-releases/release.store";
|
||||
import { _i18n } from "../../i18n";
|
||||
|
||||
export interface IChartUpgradeData {
|
||||
export interface ChartUpgradeData {
|
||||
releaseName: string;
|
||||
releaseNamespace: string;
|
||||
}
|
||||
|
||||
export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
export function isUpgradeChartTab(tab: DockTabData): boolean {
|
||||
return tab?.kind === TabKind.UPGRADE_CHART;
|
||||
}
|
||||
|
||||
export class UpgradeChartStore extends DockTabStore<ChartUpgradeData> {
|
||||
private watchers = new Map<string, IReactionDisposer>();
|
||||
|
||||
values = new DockTabStore<string>();
|
||||
@@ -23,7 +27,9 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
|
||||
autorun(() => {
|
||||
const { selectedTab, isOpen } = dockStore;
|
||||
if (!isUpgradeChartTab(selectedTab)) return;
|
||||
if (!isUpgradeChartTab(selectedTab)) {
|
||||
return;
|
||||
}
|
||||
if (isOpen) {
|
||||
this.loadData(selectedTab.id);
|
||||
}
|
||||
@@ -35,27 +41,28 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
});
|
||||
}
|
||||
|
||||
private createReleaseWatcher(releaseName: string) {
|
||||
private createReleaseWatcher(releaseName: string): void {
|
||||
if (this.watchers.get(releaseName)) {
|
||||
return;
|
||||
}
|
||||
const dispose = reaction(() => {
|
||||
const release = releaseStore.getByName(releaseName);
|
||||
if (release) return release.getRevision(); // watch changes only by revision
|
||||
if (release) {
|
||||
return release.revision;
|
||||
} // watch changes only by revision
|
||||
},
|
||||
release => {
|
||||
const releaseTab = this.getTabByRelease(releaseName);
|
||||
if (!releaseStore.isLoaded || !releaseTab) {
|
||||
return;
|
||||
}
|
||||
// auto-reload values if was loaded before
|
||||
if (release) {
|
||||
// auto-reload values if was loaded before
|
||||
if (dockStore.selectedTab === releaseTab && this.values.getData(releaseTab.id)) {
|
||||
this.loadValues(releaseTab.id);
|
||||
}
|
||||
}
|
||||
// clean up watcher, close tab if release not exists / was removed
|
||||
else {
|
||||
} else {
|
||||
// clean up watcher, close tab if release not exists / was removed
|
||||
dispose();
|
||||
this.watchers.delete(releaseName);
|
||||
dockStore.closeTab(releaseTab.id);
|
||||
@@ -64,13 +71,13 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
this.watchers.set(releaseName, dispose);
|
||||
}
|
||||
|
||||
isLoading(tabId = dockStore.selectedTabId) {
|
||||
isLoading(tabId = dockStore.selectedTabId): boolean {
|
||||
const values = this.values.getData(tabId);
|
||||
return !releaseStore.isLoaded || values === undefined;
|
||||
}
|
||||
|
||||
@action
|
||||
async loadData(tabId: TabId) {
|
||||
async loadData(tabId: TabId): Promise<void> {
|
||||
const values = this.values.getData(tabId);
|
||||
await Promise.all([
|
||||
!releaseStore.isLoaded && releaseStore.loadAll(),
|
||||
@@ -79,25 +86,25 @@ export class UpgradeChartStore extends DockTabStore<IChartUpgradeData> {
|
||||
}
|
||||
|
||||
@action
|
||||
async loadValues(tabId: TabId) {
|
||||
async loadValues(tabId: TabId): Promise<void> {
|
||||
this.values.clearData(tabId); // reset
|
||||
const { releaseName, releaseNamespace } = this.getData(tabId);
|
||||
const values = await helmReleasesApi.getValues(releaseName, releaseNamespace);
|
||||
this.values.setData(tabId, values);
|
||||
}
|
||||
|
||||
getTabByRelease(releaseName: string): IDockTab {
|
||||
getTabByRelease(releaseName: string): DockTabData {
|
||||
const item = [...this.data].find(item => item[1].releaseName === releaseName);
|
||||
if (item) {
|
||||
const [tabId] = item;
|
||||
return dockStore.getTabById(tabId)
|
||||
return dockStore.getTabById(tabId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const upgradeChartStore = new UpgradeChartStore();
|
||||
|
||||
export function createUpgradeChartTab(release: HelmRelease, tabParams: Partial<IDockTab> = {}) {
|
||||
export function createUpgradeChartTab(release: HelmRelease, tabParams: Partial<DockTabData> = {}): DockTabData {
|
||||
let tab = upgradeChartStore.getTabByRelease(release.getName());
|
||||
if (tab) {
|
||||
dockStore.open();
|
||||
@@ -112,12 +119,8 @@ export function createUpgradeChartTab(release: HelmRelease, tabParams: Partial<I
|
||||
|
||||
upgradeChartStore.setData(tab.id, {
|
||||
releaseName: release.getName(),
|
||||
releaseNamespace: release.getNs()
|
||||
})
|
||||
releaseNamespace: release.namespace
|
||||
});
|
||||
}
|
||||
return tab;
|
||||
}
|
||||
|
||||
export function isUpgradeChartTab(tab: IDockTab) {
|
||||
return tab && tab.kind === TabKind.UPGRADE_CHART;
|
||||
}
|
||||
@@ -1,34 +1,34 @@
|
||||
import "./upgrade-chart.scss"
|
||||
import "./upgrade-chart.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { cssNames } from "../../utils";
|
||||
import { IDockTab } from "./dock.store";
|
||||
import { DockTabData } from "./dock.store";
|
||||
import { InfoPanel } from "./info-panel";
|
||||
import { upgradeChartStore } from "./upgrade-chart.store";
|
||||
import { Spinner } from "../spinner";
|
||||
import { releaseStore } from "../+apps-releases/release.store";
|
||||
import { Badge } from "../badge";
|
||||
import { EditorPanel } from "./editor-panel";
|
||||
import { helmChartStore, IChartVersion } from "../+apps-helm-charts/helm-chart.store";
|
||||
import { helmChartStore, ChartVersion } from "../+apps-helm-charts/helm-chart.store";
|
||||
import { HelmRelease } from "../../api/endpoints/helm-releases.api";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { _i18n } from "../../i18n";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
tab: IDockTab;
|
||||
tab: DockTabData;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class UpgradeChart extends React.Component<Props> {
|
||||
@observable error: string;
|
||||
@observable versions = observable.array<IChartVersion>();
|
||||
@observable version: IChartVersion;
|
||||
@observable versions = observable.array<ChartVersion>();
|
||||
@observable version: ChartVersion;
|
||||
|
||||
componentDidMount() {
|
||||
componentDidMount(): void {
|
||||
this.loadVersions();
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
@@ -36,22 +36,26 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
]);
|
||||
}
|
||||
|
||||
get tabId() {
|
||||
return this.props.tab.id;
|
||||
get tabId(): string {
|
||||
return this.props.tab.id || "";
|
||||
}
|
||||
|
||||
get release(): HelmRelease {
|
||||
const tabData = upgradeChartStore.getData(this.tabId);
|
||||
if (!tabData) return;
|
||||
if (!tabData) {
|
||||
return;
|
||||
}
|
||||
return releaseStore.getByName(tabData.releaseName);
|
||||
}
|
||||
|
||||
get value() {
|
||||
get value(): string {
|
||||
return upgradeChartStore.values.getData(this.tabId);
|
||||
}
|
||||
|
||||
async loadVersions() {
|
||||
if (!this.release) return;
|
||||
async loadVersions(): Promise<void> {
|
||||
if (!this.release) {
|
||||
return;
|
||||
}
|
||||
this.version = null;
|
||||
this.versions.clear();
|
||||
const versions = await helmChartStore.getVersions(this.release.getChart());
|
||||
@@ -59,16 +63,18 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
this.version = this.versions[0];
|
||||
}
|
||||
|
||||
onChange = (value: string, error?: string) => {
|
||||
onChange = (value: string, error?: string): void => {
|
||||
upgradeChartStore.values.setData(this.tabId, value);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
upgrade = async () => {
|
||||
if (this.error) return;
|
||||
upgrade = async (): Promise<JSX.Element> => {
|
||||
if (this.error) {
|
||||
return;
|
||||
}
|
||||
const { version, repo } = this.version;
|
||||
const releaseName = this.release.getName();
|
||||
const releaseNs = this.release.getNs()
|
||||
const releaseNs = this.release.namespace;
|
||||
await releaseStore.update(releaseName, releaseNs, {
|
||||
chart: this.release.getChart(),
|
||||
values: this.value,
|
||||
@@ -78,16 +84,16 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
<p>
|
||||
<Trans>Release <b>{releaseName}</b> successfully upgraded to version <b>{version}</b></Trans>
|
||||
</p>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
formatVersionLabel = ({ value }: SelectOption<IChartVersion>) => {
|
||||
formatVersionLabel = ({ value }: SelectOption<ChartVersion>): string => {
|
||||
const chartName = this.release.getChart();
|
||||
const { repo, version } = value;
|
||||
return `${repo}/${chartName}-${version}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
render(): JSX.Element {
|
||||
const { tabId, release, value, error, onChange, upgrade, versions, version } = this;
|
||||
const { className } = this.props;
|
||||
if (!release || upgradeChartStore.isLoading() || !version) {
|
||||
@@ -97,7 +103,7 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
const controlsAndInfo = (
|
||||
<div className="upgrade flex gaps align-center">
|
||||
<span><Trans>Release</Trans></span> <Badge label={release.getName()}/>
|
||||
<span><Trans>Namespace</Trans></span> <Badge label={release.getNs()}/>
|
||||
<span><Trans>Namespace</Trans></span> <Badge label={release.namespace}/>
|
||||
<span><Trans>Version</Trans></span> <Badge label={currentVersion}/>
|
||||
<span><Trans>Upgrade version</Trans></span>
|
||||
<Select
|
||||
@@ -107,10 +113,12 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
value={version}
|
||||
options={versions}
|
||||
formatOptionLabel={this.formatVersionLabel}
|
||||
onChange={({ value }: SelectOption) => this.version = value}
|
||||
onChange={({ value }: SelectOption): void => {
|
||||
this.version = value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
return (
|
||||
<div className={cssNames("UpgradeChart flex column", className)}>
|
||||
<EditorPanel
|
||||
@@ -127,6 +135,6 @@ export class UpgradeChart extends React.Component<Props> {
|
||||
controls={controlsAndInfo}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user