diff --git a/package.json b/package.json index f9b538cf91..a7c3083fd5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "open-lens", "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", - "version": "5.0.0-alpha.1", + "version": "5.0.0-alpha.2", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index b656f08043..615ddc45d7 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -21,4 +21,14 @@ describe("HotbarStore", () => { expect(HotbarStore.getInstance().hotbars.length).toEqual(1); }); }); + + describe("add", () => { + it("adds a hotbar", () => { + const hotbarStore = HotbarStore.getInstanceOrCreate(); + + hotbarStore.load(); + hotbarStore.add({ name: "hottest" }); + expect(hotbarStore.hotbars.length).toEqual(2); + }); + }); }); diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index 88bdff67d9..478279d9c0 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -1,6 +1,7 @@ import { action, comparer, observable, toJS } from "mobx"; import { BaseStore } from "./base-store"; import migrations from "../migrations/hotbar-store"; +import * as uuid from "uuid"; export interface HotbarItem { entity: { @@ -12,16 +13,25 @@ export interface HotbarItem { } export interface Hotbar { + id: string; name: string; items: HotbarItem[]; } +export interface HotbarCreateOptions { + id?: string; + name: string; + items?: HotbarItem[]; +} + export interface HotbarStoreModel { hotbars: Hotbar[]; + activeHotbarId: string; } export class HotbarStore extends BaseStore { @observable hotbars: Hotbar[] = []; + @observable private _activeHotbarId: string; constructor() { super({ @@ -34,32 +44,103 @@ export class HotbarStore extends BaseStore { }); } + get activeHotbarId() { + return this._activeHotbarId; + } + + set activeHotbarId(id: string) { + if (this.getById(id)) { + this._activeHotbarId = id; + } + } + + get activeHotbarIndex() { + return this.hotbars.findIndex((hotbar) => hotbar.id === this.activeHotbarId); + } + @action protected async fromStore(data: Partial = {}) { if (data.hotbars?.length === 0) { this.hotbars = [{ - name: "default", + id: uuid.v4(), + name: "Default", items: [] }]; } else { this.hotbars = data.hotbars; } + + if (data.activeHotbarId) { + if (this.getById(data.activeHotbarId)) { + this.activeHotbarId = data.activeHotbarId; + } + } + + if (!this.activeHotbarId) { + this.activeHotbarId = this.hotbars[0].id; + } + } + + getActive() { + return this.getById(this.activeHotbarId); } getByName(name: string) { return this.hotbars.find((hotbar) => hotbar.name === name); } - add(hotbar: Hotbar) { - this.hotbars.push(hotbar); + getById(id: string) { + return this.hotbars.find((hotbar) => hotbar.id === id); } + add(data: HotbarCreateOptions) { + const { + id = uuid.v4(), + items = [], + name, + } = data; + + const hotbar = { id, name, items }; + + this.hotbars.push(hotbar as Hotbar); + + return hotbar as Hotbar; + } + + @action remove(hotbar: Hotbar) { this.hotbars = this.hotbars.filter((h) => h !== hotbar); + + if (this.activeHotbarId === hotbar.id) { + this.activeHotbarId = this.hotbars[0].id; + } + } + + switchToPrevious() { + const hotbarStore = HotbarStore.getInstance(); + let index = hotbarStore.activeHotbarIndex - 1; + + if (index < 0) { + index = hotbarStore.hotbars.length - 1; + } + + hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id; + } + + switchToNext() { + const hotbarStore = HotbarStore.getInstance(); + let index = hotbarStore.activeHotbarIndex + 1; + + if (index >= hotbarStore.hotbars.length) { + index = 0; + } + + hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id; } toJSON(): HotbarStoreModel { const model: HotbarStoreModel = { - hotbars: this.hotbars + hotbars: this.hotbars, + activeHotbarId: this.activeHotbarId }; return toJS(model, { diff --git a/src/migrations/hotbar-store/5.0.0-alpha.0.ts b/src/migrations/hotbar-store/5.0.0-alpha.0.ts index 7f2866b193..08d3e9785c 100644 --- a/src/migrations/hotbar-store/5.0.0-alpha.0.ts +++ b/src/migrations/hotbar-store/5.0.0-alpha.0.ts @@ -2,6 +2,7 @@ import { Hotbar } from "../../common/hotbar-store"; import { ClusterStore } from "../../common/cluster-store"; import { migration } from "../migration-wrapper"; +import { v4 as uuid } from "uuid"; export default migration({ version: "5.0.0-alpha.0", @@ -9,11 +10,14 @@ export default migration({ const hotbars: Hotbar[] = []; ClusterStore.getInstance().enabledClustersList.forEach((cluster: any) => { - const name = cluster.workspace || "default"; + const name = cluster.workspace; + + if (!name) return; + let hotbar = hotbars.find((h) => h.name === name); if (!hotbar) { - hotbar = { name, items: [] }; + hotbar = { id: uuid(), name, items: [] }; hotbars.push(hotbar); } diff --git a/src/migrations/hotbar-store/5.0.0-alpha.2.ts b/src/migrations/hotbar-store/5.0.0-alpha.2.ts new file mode 100644 index 0000000000..060fa9f542 --- /dev/null +++ b/src/migrations/hotbar-store/5.0.0-alpha.2.ts @@ -0,0 +1,16 @@ +// Cleans up a store that had the state related data stored +import { Hotbar } from "../../common/hotbar-store"; +import { migration } from "../migration-wrapper"; +import * as uuid from "uuid"; + +export default migration({ + version: "5.0.0-alpha.2", + run(store) { + const hotbars = (store.get("hotbars") || []) as Hotbar[]; + + store.set("hotbars", hotbars.map((hotbar) => ({ + id: uuid.v4(), + ...hotbar + }))); + } +}); diff --git a/src/migrations/hotbar-store/index.ts b/src/migrations/hotbar-store/index.ts index ae9d4bc125..842f144a18 100644 --- a/src/migrations/hotbar-store/index.ts +++ b/src/migrations/hotbar-store/index.ts @@ -1,7 +1,9 @@ // Hotbar store migrations import version500alpha0 from "./5.0.0-alpha.0"; +import version500alpha2 from "./5.0.0-alpha.2"; export default { ...version500alpha0, + ...version500alpha2 }; diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index ded47abb6c..478a282698 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -57,7 +57,7 @@ export class Catalog extends React.Component { } addToHotbar(item: CatalogEntityItem) { - const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME + const hotbar = HotbarStore.getInstance().getActive(); if (!hotbar) { return; @@ -66,16 +66,6 @@ export class Catalog extends React.Component { hotbar.items.push({ entity: { uid: item.id }}); } - removeFromHotbar(item: CatalogEntityItem) { - const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME - - if (!hotbar) { - return; - } - - hotbar.items = hotbar.items.filter((i) => i.entity.uid !== item.id); - } - onDetails(item: CatalogEntityItem) { item.onRun(catalogEntityRunContext); } @@ -137,9 +127,6 @@ export class Catalog extends React.Component { this.addToHotbar(item) }> Add to Hotbar - this.removeFromHotbar(item) }> - Remove from Hotbar - { menuItems.map((menuItem, index) => { return ( this.onMenuItemClick(menuItem)}> diff --git a/src/renderer/components/hotbar/hotbar-add-command.tsx b/src/renderer/components/hotbar/hotbar-add-command.tsx new file mode 100644 index 0000000000..5ec5734219 --- /dev/null +++ b/src/renderer/components/hotbar/hotbar-add-command.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { observer } from "mobx-react"; +import { HotbarStore } from "../../../common/hotbar-store"; +import { CommandOverlay } from "../command-palette"; +import { Input, InputValidator } from "../input"; + +const uniqueHotbarName: InputValidator = { + condition: ({ required }) => required, + message: () => "Hotbar with this name already exists", + validate: value => !HotbarStore.getInstance().getByName(value), +}; + +@observer +export class HotbarAddCommand extends React.Component { + + onSubmit(name: string) { + if (!name.trim()) { + return; + } + + const hotbarStore = HotbarStore.getInstance(); + + const hotbar = hotbarStore.add({ + name + }); + + hotbarStore.activeHotbarId = hotbar.id; + + CommandOverlay.close(); + } + + render() { + return ( + <> + this.onSubmit(v)} + dirty={true} + showValidationLine={true} /> + + Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel) + + + ); + } +} diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index 1a7398c55d..cbf77fba44 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -60,7 +60,7 @@ export class HotbarIcon extends React.Component { } removeFromHotbar(item: CatalogEntity) { - const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME + const hotbar = HotbarStore.getInstance().getActive(); if (!hotbar) { return; diff --git a/src/renderer/components/hotbar/hotbar-menu.scss b/src/renderer/components/hotbar/hotbar-menu.scss index 22af27e486..e8cf6efc1a 100644 --- a/src/renderer/components/hotbar/hotbar-menu.scss +++ b/src/renderer/components/hotbar/hotbar-menu.scss @@ -22,4 +22,14 @@ display: none; } } + + .HotbarSelector { + position: absolute; + bottom: 0; + width: 100%; + + .Badge { + cursor: pointer; + } + } } diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 056494338d..197671b034 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -1,4 +1,5 @@ import "./hotbar-menu.scss"; +import "./hotbar.commands"; import React from "react"; import { observer } from "mobx-react"; @@ -7,6 +8,10 @@ import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; import { HotbarStore } from "../../../common/hotbar-store"; import { catalogEntityRunContext } from "../../api/catalog-entity"; +import { Icon } from "../icon"; +import { Badge } from "../badge"; +import { CommandOverlay } from "../command-palette"; +import { HotbarSwitchCommand } from "./hotbar-switch-command"; interface Props { className?: IClassName; @@ -14,9 +19,8 @@ interface Props { @observer export class HotbarMenu extends React.Component { - get hotbarItems() { - const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME + const hotbar = HotbarStore.getInstance().getActive(); if (!hotbar) { return []; @@ -25,8 +29,21 @@ export class HotbarMenu extends React.Component { return hotbar.items.map((item) => catalogEntityRegistry.items.find((entity) => entity.metadata.uid === item.entity.uid)).filter(Boolean); } + previous() { + HotbarStore.getInstance().switchToPrevious(); + } + + next() { + HotbarStore.getInstance().switchToNext(); + } + + openSelector() { + CommandOverlay.open(); + } + render() { const { className } = this.props; + const hotbarIndex = HotbarStore.getInstance().activeHotbarIndex + 1; return (
@@ -43,6 +60,13 @@ export class HotbarMenu extends React.Component { ); })}
+
+ this.previous()} /> +
+ this.openSelector()} /> +
+ this.next()} /> +
); } diff --git a/src/renderer/components/hotbar/hotbar-remove-command.tsx b/src/renderer/components/hotbar/hotbar-remove-command.tsx new file mode 100644 index 0000000000..185ff65497 --- /dev/null +++ b/src/renderer/components/hotbar/hotbar-remove-command.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { observer } from "mobx-react"; +import { Select } from "../select"; +import { computed } from "mobx"; +import { HotbarStore } from "../../../common/hotbar-store"; +import { CommandOverlay } from "../command-palette"; +import { ConfirmDialog } from "../confirm-dialog"; + +@observer +export class HotbarRemoveCommand extends React.Component { + @computed get options() { + return HotbarStore.getInstance().hotbars.map((hotbar) => { + return { value: hotbar.id, label: hotbar.name }; + }); + } + + onChange(id: string): void { + const hotbarStore = HotbarStore.getInstance(); + const hotbar = hotbarStore.getById(id); + + if (!hotbar) { + return; + } + + CommandOverlay.close(); + ConfirmDialog.open({ + okButtonProps: { + label: `Remove Hotbar`, + primary: false, + accent: true, + }, + ok: () => { + hotbarStore.remove(hotbar); + }, + message: ( +
+

+ Are you sure you want remove hotbar {hotbar.name}? +

+
+ ), + }); + } + + render() { + return ( + this.onChange(v.value)} + components={{ DropdownIndicator: null, IndicatorSeparator: null }} + menuIsOpen={true} + options={this.options} + autoFocus={true} + escapeClearsValue={false} + placeholder="Switch to hotbar" /> + ); + } +} diff --git a/src/renderer/components/hotbar/hotbar.commands.tsx b/src/renderer/components/hotbar/hotbar.commands.tsx new file mode 100644 index 0000000000..c2ee3a05dd --- /dev/null +++ b/src/renderer/components/hotbar/hotbar.commands.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { commandRegistry } from "../../../extensions/registries"; +import { CommandOverlay } from "../command-palette"; +import { HotbarAddCommand } from "./hotbar-add-command"; +import { HotbarRemoveCommand } from "./hotbar-remove-command"; +import { HotbarSwitchCommand } from "./hotbar-switch-command"; + +commandRegistry.add({ + id: "hotbar.switchHotbar", + title: "Hotbar: Switch ...", + scope: "global", + action: () => CommandOverlay.open() +}); + +commandRegistry.add({ + id: "hotbar.addHotbar", + title: "Hotbar: Add Hotbar ...", + scope: "global", + action: () => CommandOverlay.open() +}); + +commandRegistry.add({ + id: "hotbar.removeHotbar", + title: "Hotbar: Remove Hotbar ...", + scope: "global", + action: () => CommandOverlay.open() +});