mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Remove global shared state usages of hot bar store
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
a1a088c655
commit
ad7369988a
@ -7,11 +7,13 @@ import { anyObject } from "jest-mock-extended";
|
||||
import mockFs from "mock-fs";
|
||||
import logger from "../../main/logger";
|
||||
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
|
||||
import { HotbarStore } from "../hotbar-store";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import hotbarStoreInjectable from "../hotbar-store.injectable";
|
||||
import { HotbarStore } from "../hotbar-store";
|
||||
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
|
||||
|
||||
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
|
||||
catalogEntityRegistry: {
|
||||
@ -123,12 +125,21 @@ const awsCluster = getMockCatalogEntity({
|
||||
|
||||
describe("HotbarStore", () => {
|
||||
let di: DiContainer;
|
||||
let hotbarStore: HotbarStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
di.permitSideEffects(appVersionInjectable);
|
||||
|
||||
di.override(hotbarStoreInjectable, () => {
|
||||
HotbarStore.resetInstance();
|
||||
|
||||
return HotbarStore.createInstance({
|
||||
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -140,18 +151,18 @@ describe("HotbarStore", () => {
|
||||
mockFs();
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
hotbarStore = di.inject(hotbarStoreInjectable);
|
||||
});
|
||||
|
||||
describe("load", () => {
|
||||
it("loads one hotbar by default", () => {
|
||||
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
|
||||
expect(hotbarStore.hotbars.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("add", () => {
|
||||
it("adds a hotbar", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.add({ name: "hottest" });
|
||||
expect(hotbarStore.hotbars.length).toEqual(2);
|
||||
});
|
||||
@ -159,20 +170,14 @@ describe("HotbarStore", () => {
|
||||
|
||||
describe("hotbar items", () => {
|
||||
it("initially creates 12 empty cells", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
expect(hotbarStore.getActive().items.length).toEqual(12);
|
||||
});
|
||||
|
||||
it("initially adds catalog entity as first item", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
|
||||
});
|
||||
|
||||
it("adds items", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||
|
||||
@ -180,8 +185,6 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("removes items", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.removeFromHotbar("test");
|
||||
hotbarStore.removeFromHotbar("catalog-entity");
|
||||
@ -191,8 +194,6 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("does nothing if removing with invalid uid", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.removeFromHotbar("invalid uid");
|
||||
const items = hotbarStore.getActive().items.filter(Boolean);
|
||||
@ -201,8 +202,6 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("moves item to empty cell", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.addToHotbar(minikubeCluster);
|
||||
hotbarStore.addToHotbar(awsCluster);
|
||||
@ -216,8 +215,6 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("moves items down", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.addToHotbar(minikubeCluster);
|
||||
hotbarStore.addToHotbar(awsCluster);
|
||||
@ -231,8 +228,6 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("moves items up", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.addToHotbar(minikubeCluster);
|
||||
hotbarStore.addToHotbar(awsCluster);
|
||||
@ -246,8 +241,6 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("logs an error if cellIndex is out of bounds", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.add({ name: "hottest", id: "hottest" });
|
||||
hotbarStore.setActiveHotbar("hottest");
|
||||
|
||||
@ -269,22 +262,16 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("throws an error if getId is invalid or returns not a string", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
expect(() => hotbarStore.addToHotbar({} as any)).toThrowError(TypeError);
|
||||
expect(() => hotbarStore.addToHotbar({ getId: () => true } as any)).toThrowError(TypeError);
|
||||
});
|
||||
|
||||
it("throws an error if getName is invalid or returns not a string", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
expect(() => hotbarStore.addToHotbar({ getId: () => "" } as any)).toThrowError(TypeError);
|
||||
expect(() => hotbarStore.addToHotbar({ getId: () => "", getName: () => 4 } as any)).toThrowError(TypeError);
|
||||
});
|
||||
|
||||
it("does nothing when item moved to same cell", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.restackItems(1, 1);
|
||||
|
||||
@ -292,8 +279,6 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("new items takes first empty cell", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
hotbarStore.addToHotbar(awsCluster);
|
||||
hotbarStore.restackItems(0, 3);
|
||||
@ -309,8 +294,6 @@ describe("HotbarStore", () => {
|
||||
console.error = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
|
||||
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
|
||||
@ -324,8 +307,6 @@ describe("HotbarStore", () => {
|
||||
});
|
||||
|
||||
it("checks if entity already pinned to hotbar", () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
|
||||
hotbarStore.addToHotbar(testCluster);
|
||||
|
||||
expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy();
|
||||
@ -401,22 +382,24 @@ describe("HotbarStore", () => {
|
||||
mockFs(configurationToBeMigrated);
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
hotbarStore = di.inject(hotbarStoreInjectable);
|
||||
});
|
||||
|
||||
it("allows to retrieve a hotbar", () => {
|
||||
const hotbar = HotbarStore.getInstance().getById("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
const hotbar = hotbarStore.getById("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
|
||||
expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
|
||||
});
|
||||
|
||||
it("clears cells without entity", () => {
|
||||
const items = HotbarStore.getInstance().hotbars[0].items;
|
||||
const items = hotbarStore.hotbars[0].items;
|
||||
|
||||
expect(items[2]).toBeNull();
|
||||
});
|
||||
|
||||
it("adds extra data to cells with according entity", () => {
|
||||
const items = HotbarStore.getInstance().hotbars[0].items;
|
||||
const items = hotbarStore.hotbars[0].items;
|
||||
|
||||
expect(items[0]).toEqual({
|
||||
entity: {
|
||||
|
||||
@ -2,366 +2,9 @@
|
||||
* 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 catalogCatalogEntityInjectable from "./catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
|
||||
import { action, comparer, observable, makeObservable, computed } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/hotbar-store";
|
||||
import { toJS } from "./utils";
|
||||
import { CatalogEntity } from "./catalog";
|
||||
import logger from "../main/logger";
|
||||
import { broadcastMessage } from "./ipc";
|
||||
import {
|
||||
defaultHotbarCells,
|
||||
getEmptyHotbar,
|
||||
Hotbar,
|
||||
CreateHotbarData,
|
||||
CreateHotbarOptions,
|
||||
} from "./hotbar-types";
|
||||
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
|
||||
import type { GeneralEntity } from "./catalog-entities";
|
||||
|
||||
export interface HotbarStoreModel {
|
||||
hotbars: Hotbar[];
|
||||
activeHotbarId: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
catalogCatalogEntity: GeneralEntity;
|
||||
}
|
||||
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
readonly displayName = "HotbarStore";
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
@observable private _activeHotbarId: string;
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
super({
|
||||
configName: "lens-hotbar-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
syncOptions: {
|
||||
equals: comparer.structural,
|
||||
},
|
||||
migrations,
|
||||
});
|
||||
makeObservable(this);
|
||||
this.load();
|
||||
}
|
||||
|
||||
@computed get activeHotbarId() {
|
||||
return this._activeHotbarId;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
|
||||
* @param hotbar The hotbar instance, or the index, or its ID
|
||||
*/
|
||||
setActiveHotbar(hotbar: Hotbar | number | string) {
|
||||
if (typeof hotbar === "number") {
|
||||
if (hotbar >= 0 && hotbar < this.hotbars.length) {
|
||||
this._activeHotbarId = this.hotbars[hotbar].id;
|
||||
}
|
||||
} else if (typeof hotbar === "string") {
|
||||
if (this.getById(hotbar)) {
|
||||
this._activeHotbarId = hotbar;
|
||||
}
|
||||
} else {
|
||||
if (this.hotbars.indexOf(hotbar) >= 0) {
|
||||
this._activeHotbarId = hotbar.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hotbarIndexById(id: string) {
|
||||
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||
}
|
||||
|
||||
private hotbarIndex(hotbar: Hotbar) {
|
||||
return this.hotbars.indexOf(hotbar);
|
||||
}
|
||||
|
||||
@computed get activeHotbarIndex() {
|
||||
return this.hotbarIndexById(this.activeHotbarId);
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||
if (!data.hotbars || !data.hotbars.length) {
|
||||
const hotbar = getEmptyHotbar("Default");
|
||||
const {
|
||||
metadata: { uid, name, source },
|
||||
} = this.dependencies.catalogCatalogEntity;
|
||||
const initialItem = { entity: { uid, name, source }};
|
||||
|
||||
hotbar.items[0] = initialItem;
|
||||
|
||||
this.hotbars = [hotbar];
|
||||
} else {
|
||||
this.hotbars = data.hotbars;
|
||||
}
|
||||
|
||||
this.hotbars.forEach(ensureExactHotbarItemLength);
|
||||
|
||||
if (data.activeHotbarId) {
|
||||
this.setActiveHotbar(data.activeHotbarId);
|
||||
}
|
||||
|
||||
if (!this.activeHotbarId) {
|
||||
this.setActiveHotbar(0);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): HotbarStoreModel {
|
||||
const model: HotbarStoreModel = {
|
||||
hotbars: this.hotbars,
|
||||
activeHotbarId: this.activeHotbarId,
|
||||
};
|
||||
|
||||
return toJS(model);
|
||||
}
|
||||
|
||||
getActive() {
|
||||
return this.getById(this.activeHotbarId);
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.name === name);
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.id === id);
|
||||
}
|
||||
|
||||
add = action(
|
||||
(
|
||||
data: CreateHotbarData,
|
||||
{ setActive = false }: CreateHotbarOptions = {},
|
||||
) => {
|
||||
const hotbar = getEmptyHotbar(data.name, data.id);
|
||||
|
||||
this.hotbars.push(hotbar);
|
||||
|
||||
if (setActive) {
|
||||
this._activeHotbarId = hotbar.id;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setHotbarName = action((id: string, name: string) => {
|
||||
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||
|
||||
if (index < 0) {
|
||||
return void console.warn(
|
||||
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
this.hotbars[index].name = name;
|
||||
});
|
||||
|
||||
remove = action((hotbar: Hotbar) => {
|
||||
if (this.hotbars.length <= 1) {
|
||||
throw new Error("Cannot remove the last hotbar");
|
||||
}
|
||||
|
||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||
|
||||
if (this.activeHotbarId === hotbar.id) {
|
||||
this.setActiveHotbar(0);
|
||||
}
|
||||
});
|
||||
|
||||
@action
|
||||
addToHotbar(item: CatalogEntity, cellIndex?: number) {
|
||||
const hotbar = this.getActive();
|
||||
const uid = item.getId();
|
||||
const name = item.getName();
|
||||
|
||||
if (typeof uid !== "string") {
|
||||
throw new TypeError("CatalogEntity's ID must be a string");
|
||||
}
|
||||
|
||||
if (typeof name !== "string") {
|
||||
throw new TypeError("CatalogEntity's NAME must be a string");
|
||||
}
|
||||
|
||||
if (this.isAddedToActive(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = {
|
||||
uid,
|
||||
name,
|
||||
source: item.metadata.source,
|
||||
};
|
||||
const newItem = { entity };
|
||||
|
||||
if (cellIndex === undefined) {
|
||||
// Add item to empty cell
|
||||
const emptyCellIndex = hotbar.items.indexOf(null);
|
||||
|
||||
if (emptyCellIndex != -1) {
|
||||
hotbar.items[emptyCellIndex] = newItem;
|
||||
} else {
|
||||
broadcastMessage(hotbarTooManyItemsChannel);
|
||||
}
|
||||
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
|
||||
hotbar.items[cellIndex] = newItem;
|
||||
} else {
|
||||
logger.error(
|
||||
`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`,
|
||||
{ entityId: uid, hotbarId: hotbar.id, cellIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
removeFromHotbar(uid: string): void {
|
||||
const hotbar = this.getActive();
|
||||
const index = hotbar.items.findIndex((item) => item?.entity.uid === uid);
|
||||
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
hotbar.items[index] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all hotbar items that reference the `uid`.
|
||||
* @param uid The `EntityId` that each hotbar item refers to
|
||||
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
||||
*/
|
||||
@action
|
||||
removeAllHotbarItems(uid: string) {
|
||||
for (const hotbar of this.hotbars) {
|
||||
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
|
||||
|
||||
if (index >= 0) {
|
||||
hotbar.items[index] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findClosestEmptyIndex(from: number, direction = 1) {
|
||||
let index = from;
|
||||
|
||||
while (this.getActive().items[index] != null) {
|
||||
index += direction;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
@action
|
||||
restackItems(from: number, to: number): void {
|
||||
const { items } = this.getActive();
|
||||
const source = items[from];
|
||||
const moveDown = from < to;
|
||||
|
||||
if (
|
||||
from < 0 ||
|
||||
to < 0 ||
|
||||
from >= items.length ||
|
||||
to >= items.length ||
|
||||
isNaN(from) ||
|
||||
isNaN(to)
|
||||
) {
|
||||
throw new Error("Invalid 'from' or 'to' arguments");
|
||||
}
|
||||
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.splice(from, 1, null);
|
||||
|
||||
if (items[to] == null) {
|
||||
items.splice(to, 1, source);
|
||||
} else {
|
||||
// Move cells up or down to closes empty cell
|
||||
items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1);
|
||||
items.splice(to, 0, source);
|
||||
}
|
||||
}
|
||||
|
||||
switchToPrevious() {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
let index = hotbarStore.activeHotbarIndex - 1;
|
||||
|
||||
if (index < 0) {
|
||||
index = hotbarStore.hotbars.length - 1;
|
||||
}
|
||||
|
||||
hotbarStore.setActiveHotbar(index);
|
||||
}
|
||||
|
||||
switchToNext() {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
let index = hotbarStore.activeHotbarIndex + 1;
|
||||
|
||||
if (index >= hotbarStore.hotbars.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
hotbarStore.setActiveHotbar(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if entity already pinned to the active hotbar
|
||||
*/
|
||||
isAddedToActive(entity: CatalogEntity | null | undefined): boolean {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.getActive().items.findIndex(
|
||||
(item) => item?.entity.uid === entity.getId(),
|
||||
) >= 0
|
||||
);
|
||||
}
|
||||
|
||||
getDisplayLabel(hotbar: Hotbar): string {
|
||||
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
|
||||
}
|
||||
|
||||
getDisplayIndex(hotbar: Hotbar): string {
|
||||
const index = this.hotbarIndex(hotbar);
|
||||
|
||||
if (index < 0) {
|
||||
return "??";
|
||||
}
|
||||
|
||||
return `${index + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function ensures that there are always exactly `defaultHotbarCells`
|
||||
* worth of items in the hotbar.
|
||||
* @param hotbar The hotbar to modify
|
||||
*/
|
||||
function ensureExactHotbarItemLength(hotbar: Hotbar) {
|
||||
// if there are not enough items
|
||||
while (hotbar.items.length < defaultHotbarCells) {
|
||||
hotbar.items.push(null);
|
||||
}
|
||||
|
||||
// if for some reason the hotbar was overfilled before, remove as many entries
|
||||
// as needed, but prefer empty slots and items at the end first.
|
||||
while (hotbar.items.length > defaultHotbarCells) {
|
||||
const lastNull = hotbar.items.lastIndexOf(null);
|
||||
|
||||
if (lastNull >= 0) {
|
||||
hotbar.items.splice(lastNull, 1);
|
||||
} else {
|
||||
hotbar.items.length = defaultHotbarCells;
|
||||
}
|
||||
}
|
||||
}
|
||||
import { HotbarStore } from "./hotbar-store";
|
||||
|
||||
const hotbarStoreInjectable = getInjectable({
|
||||
id: "hotbar-store",
|
||||
@ -373,6 +16,8 @@ const hotbarStoreInjectable = getInjectable({
|
||||
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
causesSideEffects: true,
|
||||
});
|
||||
|
||||
export default hotbarStoreInjectable;
|
||||
|
||||
@ -3,13 +3,358 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import hotbarStoreInjectable from "./hotbar-store.injectable";
|
||||
|
||||
import { action, comparer, observable, makeObservable, computed } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/hotbar-store";
|
||||
import { toJS } from "./utils";
|
||||
import { CatalogEntity } from "./catalog";
|
||||
import logger from "../main/logger";
|
||||
import { broadcastMessage } from "./ipc";
|
||||
import {
|
||||
asLegacyGlobalSingletonForExtensionApi,
|
||||
} from "../extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api";
|
||||
defaultHotbarCells,
|
||||
getEmptyHotbar,
|
||||
Hotbar,
|
||||
CreateHotbarData,
|
||||
CreateHotbarOptions,
|
||||
} from "./hotbar-types";
|
||||
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
|
||||
import type { GeneralEntity } from "./catalog-entities";
|
||||
|
||||
export interface HotbarStoreModel {
|
||||
hotbars: Hotbar[];
|
||||
activeHotbarId: string;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
catalogCatalogEntity: GeneralEntity;
|
||||
}
|
||||
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
readonly displayName = "HotbarStore";
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
@observable private _activeHotbarId: string;
|
||||
|
||||
constructor(private dependencies: Dependencies) {
|
||||
super({
|
||||
configName: "lens-hotbar-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
syncOptions: {
|
||||
equals: comparer.structural,
|
||||
},
|
||||
migrations,
|
||||
});
|
||||
makeObservable(this);
|
||||
this.load();
|
||||
}
|
||||
|
||||
@computed get activeHotbarId() {
|
||||
return this._activeHotbarId;
|
||||
}
|
||||
|
||||
/**
|
||||
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
|
||||
* @param hotbar The hotbar instance, or the index, or its ID
|
||||
*/
|
||||
setActiveHotbar(hotbar: Hotbar | number | string) {
|
||||
if (typeof hotbar === "number") {
|
||||
if (hotbar >= 0 && hotbar < this.hotbars.length) {
|
||||
this._activeHotbarId = this.hotbars[hotbar].id;
|
||||
}
|
||||
} else if (typeof hotbar === "string") {
|
||||
if (this.getById(hotbar)) {
|
||||
this._activeHotbarId = hotbar;
|
||||
}
|
||||
} else {
|
||||
if (this.hotbars.indexOf(hotbar) >= 0) {
|
||||
this._activeHotbarId = hotbar.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private hotbarIndexById(id: string) {
|
||||
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||
}
|
||||
|
||||
private hotbarIndex(hotbar: Hotbar) {
|
||||
return this.hotbars.indexOf(hotbar);
|
||||
}
|
||||
|
||||
@computed get activeHotbarIndex() {
|
||||
return this.hotbarIndexById(this.activeHotbarId);
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||
if (!data.hotbars || !data.hotbars.length) {
|
||||
const hotbar = getEmptyHotbar("Default");
|
||||
const {
|
||||
metadata: { uid, name, source },
|
||||
} = this.dependencies.catalogCatalogEntity;
|
||||
const initialItem = { entity: { uid, name, source }};
|
||||
|
||||
hotbar.items[0] = initialItem;
|
||||
|
||||
this.hotbars = [hotbar];
|
||||
} else {
|
||||
this.hotbars = data.hotbars;
|
||||
}
|
||||
|
||||
this.hotbars.forEach(ensureExactHotbarItemLength);
|
||||
|
||||
if (data.activeHotbarId) {
|
||||
this.setActiveHotbar(data.activeHotbarId);
|
||||
}
|
||||
|
||||
if (!this.activeHotbarId) {
|
||||
this.setActiveHotbar(0);
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): HotbarStoreModel {
|
||||
const model: HotbarStoreModel = {
|
||||
hotbars: this.hotbars,
|
||||
activeHotbarId: this.activeHotbarId,
|
||||
};
|
||||
|
||||
return toJS(model);
|
||||
}
|
||||
|
||||
getActive() {
|
||||
return this.getById(this.activeHotbarId);
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.name === name);
|
||||
}
|
||||
|
||||
getById(id: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.id === id);
|
||||
}
|
||||
|
||||
add = action(
|
||||
(
|
||||
data: CreateHotbarData,
|
||||
{ setActive = false }: CreateHotbarOptions = {},
|
||||
) => {
|
||||
const hotbar = getEmptyHotbar(data.name, data.id);
|
||||
|
||||
this.hotbars.push(hotbar);
|
||||
|
||||
if (setActive) {
|
||||
this._activeHotbarId = hotbar.id;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setHotbarName = action((id: string, name: string) => {
|
||||
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||
|
||||
if (index < 0) {
|
||||
return void console.warn(
|
||||
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
this.hotbars[index].name = name;
|
||||
});
|
||||
|
||||
remove = action((hotbar: Hotbar) => {
|
||||
if (this.hotbars.length <= 1) {
|
||||
throw new Error("Cannot remove the last hotbar");
|
||||
}
|
||||
|
||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||
|
||||
if (this.activeHotbarId === hotbar.id) {
|
||||
this.setActiveHotbar(0);
|
||||
}
|
||||
});
|
||||
|
||||
@action
|
||||
addToHotbar(item: CatalogEntity, cellIndex?: number) {
|
||||
const hotbar = this.getActive();
|
||||
const uid = item.getId();
|
||||
const name = item.getName();
|
||||
|
||||
if (typeof uid !== "string") {
|
||||
throw new TypeError("CatalogEntity's ID must be a string");
|
||||
}
|
||||
|
||||
if (typeof name !== "string") {
|
||||
throw new TypeError("CatalogEntity's NAME must be a string");
|
||||
}
|
||||
|
||||
if (this.isAddedToActive(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entity = {
|
||||
uid,
|
||||
name,
|
||||
source: item.metadata.source,
|
||||
};
|
||||
const newItem = { entity };
|
||||
|
||||
if (cellIndex === undefined) {
|
||||
// Add item to empty cell
|
||||
const emptyCellIndex = hotbar.items.indexOf(null);
|
||||
|
||||
if (emptyCellIndex != -1) {
|
||||
hotbar.items[emptyCellIndex] = newItem;
|
||||
} else {
|
||||
broadcastMessage(hotbarTooManyItemsChannel);
|
||||
}
|
||||
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
|
||||
hotbar.items[cellIndex] = newItem;
|
||||
} else {
|
||||
logger.error(
|
||||
`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`,
|
||||
{ entityId: uid, hotbarId: hotbar.id, cellIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
removeFromHotbar(uid: string): void {
|
||||
const hotbar = this.getActive();
|
||||
const index = hotbar.items.findIndex((item) => item?.entity.uid === uid);
|
||||
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
hotbar.items[index] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all hotbar items that reference the `uid`.
|
||||
* @param uid The `EntityId` that each hotbar item refers to
|
||||
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
||||
*/
|
||||
@action
|
||||
removeAllHotbarItems(uid: string) {
|
||||
for (const hotbar of this.hotbars) {
|
||||
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
|
||||
|
||||
if (index >= 0) {
|
||||
hotbar.items[index] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findClosestEmptyIndex(from: number, direction = 1) {
|
||||
let index = from;
|
||||
|
||||
while (this.getActive().items[index] != null) {
|
||||
index += direction;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
@action
|
||||
restackItems(from: number, to: number): void {
|
||||
const { items } = this.getActive();
|
||||
const source = items[from];
|
||||
const moveDown = from < to;
|
||||
|
||||
if (
|
||||
from < 0 ||
|
||||
to < 0 ||
|
||||
from >= items.length ||
|
||||
to >= items.length ||
|
||||
isNaN(from) ||
|
||||
isNaN(to)
|
||||
) {
|
||||
throw new Error("Invalid 'from' or 'to' arguments");
|
||||
}
|
||||
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.splice(from, 1, null);
|
||||
|
||||
if (items[to] == null) {
|
||||
items.splice(to, 1, source);
|
||||
} else {
|
||||
// Move cells up or down to closes empty cell
|
||||
items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1);
|
||||
items.splice(to, 0, source);
|
||||
}
|
||||
}
|
||||
|
||||
switchToPrevious() {
|
||||
let index = this.activeHotbarIndex - 1;
|
||||
|
||||
if (index < 0) {
|
||||
index = this.hotbars.length - 1;
|
||||
}
|
||||
|
||||
this.setActiveHotbar(index);
|
||||
}
|
||||
|
||||
switchToNext() {
|
||||
let index = this.activeHotbarIndex + 1;
|
||||
|
||||
if (index >= this.hotbars.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
this.setActiveHotbar(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if entity already pinned to the active hotbar
|
||||
*/
|
||||
isAddedToActive(entity: CatalogEntity | null | undefined): boolean {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
this.getActive().items.findIndex(
|
||||
(item) => item?.entity.uid === entity.getId(),
|
||||
) >= 0
|
||||
);
|
||||
}
|
||||
|
||||
getDisplayLabel(hotbar: Hotbar): string {
|
||||
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
|
||||
}
|
||||
|
||||
getDisplayIndex(hotbar: Hotbar): string {
|
||||
const index = this.hotbarIndex(hotbar);
|
||||
|
||||
if (index < 0) {
|
||||
return "??";
|
||||
}
|
||||
|
||||
return `${index + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use di.inject(hotbarStoreInjectable) instead.
|
||||
* This function ensures that there are always exactly `defaultHotbarCells`
|
||||
* worth of items in the hotbar.
|
||||
* @param hotbar The hotbar to modify
|
||||
*/
|
||||
export const HotbarStore = asLegacyGlobalSingletonForExtensionApi(hotbarStoreInjectable);
|
||||
function ensureExactHotbarItemLength(hotbar: Hotbar) {
|
||||
// if there are not enough items
|
||||
while (hotbar.items.length < defaultHotbarCells) {
|
||||
hotbar.items.push(null);
|
||||
}
|
||||
|
||||
// if for some reason the hotbar was overfilled before, remove as many entries
|
||||
// as needed, but prefer empty slots and items at the end first.
|
||||
while (hotbar.items.length > defaultHotbarCells) {
|
||||
const lastNull = hotbar.items.lastIndexOf(null);
|
||||
|
||||
if (lastNull >= 0) {
|
||||
hotbar.items.splice(lastNull, 1);
|
||||
} else {
|
||||
hotbar.items.length = defaultHotbarCells;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectab
|
||||
import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake";
|
||||
import joinPathsInjectable from "../common/path/join-paths.injectable";
|
||||
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
|
||||
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
|
||||
|
||||
export const getDiForUnitTesting = (
|
||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||
@ -65,6 +66,8 @@ export const getDiForUnitTesting = (
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
|
||||
|
||||
di.override(hotbarStoreInjectable, () => ({}));
|
||||
|
||||
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
|
||||
@ -41,6 +41,8 @@ import navigateToAddClusterInjectable from "../common/front-end-routing/routes/
|
||||
import addSyncEntriesInjectable from "./initializers/add-sync-entries.injectable";
|
||||
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
|
||||
import { bindEvents } from "./navigation/events";
|
||||
import deleteClusterDialogModelInjectable
|
||||
from "./components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.injectable";
|
||||
|
||||
if (process.isMainFrame) {
|
||||
SentryInit();
|
||||
@ -99,6 +101,7 @@ export async function bootstrap(di: DiContainer) {
|
||||
logger.info(`${logPrefix} initializing Catalog`);
|
||||
initializers.initCatalog({
|
||||
openCommandDialog: di.inject(commandOverlayInjectable).open,
|
||||
deleteClusterDialogModel: di.inject(deleteClusterDialogModelInjectable),
|
||||
});
|
||||
|
||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||
|
||||
@ -12,6 +12,7 @@ import { CatalogCategory } from "../../../api/catalog-entity";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns";
|
||||
import getCategoryColumnsInjectable, { CategoryColumns, GetCategoryColumnsParams } from "../get-category-columns.injectable";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
|
||||
|
||||
class TestCategory extends CatalogCategory {
|
||||
apiVersion = "catalog.k8slens.dev/v1alpha1";
|
||||
@ -39,6 +40,8 @@ describe("Custom Category Columns", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
di = getDiForUnitTesting();
|
||||
|
||||
di.override(hotbarStoreInjectable, () => ({}));
|
||||
});
|
||||
|
||||
describe("without extensions", () => {
|
||||
|
||||
@ -13,7 +13,6 @@ import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.s
|
||||
import { navigate } from "../../navigation";
|
||||
import { MenuItem, MenuActions } from "../menu";
|
||||
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
|
||||
import { CatalogAddButton } from "./catalog-add-button";
|
||||
@ -39,6 +38,8 @@ import catalogRouteParametersInjectable from "./catalog-route-parameters.injecta
|
||||
import { browseCatalogTab } from "./catalog-browse-tab";
|
||||
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
|
||||
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
|
||||
interface Dependencies {
|
||||
catalogPreviousActiveTabStorage: { set: (value: string ) => void };
|
||||
@ -53,6 +54,7 @@ interface Dependencies {
|
||||
};
|
||||
|
||||
navigateToCatalog: NavigateToCatalog;
|
||||
hotbarStore: HotbarStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -125,11 +127,11 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
}
|
||||
|
||||
addToHotbar(entity: CatalogEntity): void {
|
||||
HotbarStore.getInstance().addToHotbar(entity);
|
||||
this.props.hotbarStore.addToHotbar(entity);
|
||||
}
|
||||
|
||||
removeFromHotbar(entity: CatalogEntity): void {
|
||||
HotbarStore.getInstance().removeFromHotbar(entity.getId());
|
||||
this.props.hotbarStore.removeFromHotbar(entity.getId());
|
||||
}
|
||||
|
||||
onDetails = (entity: CatalogEntity) => {
|
||||
@ -216,7 +218,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
|
||||
};
|
||||
|
||||
renderName(entity: CatalogEntity) {
|
||||
const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(entity);
|
||||
const isItemInHotbar = this.props.hotbarStore.isAddedToActive(entity);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -341,5 +343,6 @@ export const Catalog = withInjectables<Dependencies>( NonInjectedCatalog, {
|
||||
routeParameters: di.inject(catalogRouteParametersInjectable),
|
||||
navigateToCatalog: di.inject(navigateToCatalogInjectable),
|
||||
emitEvent: di.inject(appEventBusInjectable).emit,
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -9,10 +9,12 @@ import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
|
||||
import type { ItemListLayoutProps } from "../item-object-list";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
import categoryColumnsInjectable from "./custom-category-columns.injectable";
|
||||
import { defaultCategoryColumns, browseAllColumns, nameCategoryColumn } from "./internal-category-columns";
|
||||
import { defaultCategoryColumns, browseAllColumns } from "./internal-category-columns";
|
||||
import nameCategoryColumnInjectable from "./name-category-column.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>;
|
||||
nameCategoryColumn: RegisteredAdditionalCategoryColumn;
|
||||
}
|
||||
|
||||
export interface GetCategoryColumnsParams {
|
||||
@ -21,7 +23,7 @@ export interface GetCategoryColumnsParams {
|
||||
|
||||
export type CategoryColumns = Required<Pick<ItemListLayoutProps<CatalogEntity>, "sortingCallbacks" | "searchFilters" | "renderTableContents" | "renderTableHeader">>;
|
||||
|
||||
function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>): RegisteredAdditionalCategoryColumn[] {
|
||||
function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>, nameCategoryColumn: RegisteredAdditionalCategoryColumn): RegisteredAdditionalCategoryColumn[] {
|
||||
const fromExtensions = (
|
||||
extensionColumns
|
||||
.get()
|
||||
@ -41,7 +43,7 @@ function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionCo
|
||||
];
|
||||
}
|
||||
|
||||
function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] {
|
||||
function getBrowseAllColumns(nameCategoryColumn: RegisteredAdditionalCategoryColumn): RegisteredAdditionalCategoryColumn[] {
|
||||
return [
|
||||
...browseAllColumns,
|
||||
nameCategoryColumn,
|
||||
@ -49,11 +51,11 @@ function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] {
|
||||
];
|
||||
}
|
||||
|
||||
const getCategoryColumns = ({ extensionColumns }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => {
|
||||
const getCategoryColumns = ({ extensionColumns, nameCategoryColumn }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => {
|
||||
const allRegistrations = orderBy(
|
||||
activeCategory
|
||||
? getSpecificCategoryColumns(activeCategory, extensionColumns)
|
||||
: getBrowseAllColumns(),
|
||||
? getSpecificCategoryColumns(activeCategory, extensionColumns, nameCategoryColumn)
|
||||
: getBrowseAllColumns(nameCategoryColumn),
|
||||
"priority",
|
||||
"asc",
|
||||
);
|
||||
@ -89,6 +91,7 @@ const getCategoryColumnsInjectable = getInjectable({
|
||||
|
||||
instantiate: (di) => getCategoryColumns({
|
||||
extensionColumns: di.inject(categoryColumnsInjectable),
|
||||
nameCategoryColumn: di.inject(nameCategoryColumnInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -4,26 +4,54 @@
|
||||
*/
|
||||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { MenuItem } from "../menu";
|
||||
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
|
||||
export function HotbarToggleMenuItem(props: { entity: CatalogEntity; addContent: ReactNode; removeContent: ReactNode }) {
|
||||
const store = HotbarStore.getInstance();
|
||||
const [itemInHotbar, setItemInHotbar] = useState(store.isAddedToActive(props.entity));
|
||||
interface Dependencies {
|
||||
hotbarStore: HotbarStore;
|
||||
}
|
||||
|
||||
interface HotbarToggleMenuItemProps {
|
||||
entity: CatalogEntity;
|
||||
addContent: ReactNode;
|
||||
removeContent: ReactNode;
|
||||
}
|
||||
|
||||
function NonInjectedHotbarToggleMenuItem({
|
||||
addContent,
|
||||
entity,
|
||||
hotbarStore,
|
||||
removeContent,
|
||||
}: Dependencies & HotbarToggleMenuItemProps) {
|
||||
const [itemInHotbar, setItemInHotbar] = useState(hotbarStore.isAddedToActive(entity));
|
||||
|
||||
return (
|
||||
<MenuItem onClick={() => {
|
||||
if (itemInHotbar) {
|
||||
store.removeFromHotbar(props.entity.getId());
|
||||
hotbarStore.removeFromHotbar(entity.getId());
|
||||
setItemInHotbar(false);
|
||||
} else {
|
||||
store.addToHotbar(props.entity);
|
||||
hotbarStore.addToHotbar(entity);
|
||||
setItemInHotbar(true);
|
||||
}
|
||||
}}>
|
||||
{itemInHotbar ? props.removeContent : props.addContent }
|
||||
{itemInHotbar ? removeContent : addContent }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export const HotbarToggleMenuItem = withInjectables<Dependencies, HotbarToggleMenuItemProps>(
|
||||
NonInjectedHotbarToggleMenuItem,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -6,49 +6,10 @@
|
||||
import styles from "./catalog.module.scss";
|
||||
|
||||
import React from "react";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { Avatar } from "../avatar";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
import { Icon } from "../icon";
|
||||
import { prevDefault } from "../../utils";
|
||||
import { getLabelBadges } from "./helpers";
|
||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||
|
||||
function renderEntityName(entity: CatalogEntity) {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
const isItemInHotbar = hotbarStore.isAddedToActive(entity);
|
||||
const onClick = prevDefault(
|
||||
isItemInHotbar
|
||||
? () => hotbarStore.removeFromHotbar(entity.getId())
|
||||
: () => hotbarStore.addToHotbar(entity),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Avatar
|
||||
title={entity.getName()}
|
||||
colorHash={`${entity.getName()}-${entity.getSource()}`}
|
||||
src={entity.spec.icon?.src}
|
||||
background={entity.spec.icon?.background}
|
||||
className={styles.catalogAvatar}
|
||||
size={24}
|
||||
>
|
||||
{entity.spec.icon?.material && <Icon material={entity.spec.icon?.material} small/>}
|
||||
</Avatar>
|
||||
<span>{entity.getName()}</span>
|
||||
<Icon
|
||||
small
|
||||
className={styles.pinIcon}
|
||||
material={!isItemInHotbar && "push_pin"}
|
||||
svg={isItemInHotbar ? "push_off" : "push_pin"}
|
||||
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||
{
|
||||
id: "kind",
|
||||
@ -63,20 +24,6 @@ export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const nameCategoryColumn: RegisteredAdditionalCategoryColumn = {
|
||||
id: "name",
|
||||
priority: 0,
|
||||
renderCell: renderEntityName,
|
||||
titleProps: {
|
||||
title: "Name",
|
||||
className: styles.entityName,
|
||||
id: "name",
|
||||
sortBy: "name",
|
||||
},
|
||||
searchFilter: entity => entity.getName(),
|
||||
sortCallback: entity => `name=${entity.getName()}`,
|
||||
};
|
||||
|
||||
export const defaultCategoryColumns: RegisteredAdditionalCategoryColumn[] = [
|
||||
{
|
||||
id: "source",
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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 styles from "./catalog.module.scss";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { prevDefault } from "../../utils";
|
||||
import { Avatar } from "../avatar";
|
||||
import { Icon } from "../icon";
|
||||
import React from "react";
|
||||
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
|
||||
const renderEntityName = (hotbarStore: HotbarStore) => (entity: CatalogEntity) => {
|
||||
const isItemInHotbar = hotbarStore.isAddedToActive(entity);
|
||||
const onClick = prevDefault(
|
||||
isItemInHotbar
|
||||
? () => hotbarStore.removeFromHotbar(entity.getId())
|
||||
: () => hotbarStore.addToHotbar(entity),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Avatar
|
||||
title={entity.getName()}
|
||||
colorHash={`${entity.getName()}-${entity.getSource()}`}
|
||||
src={entity.spec.icon?.src}
|
||||
background={entity.spec.icon?.background}
|
||||
className={styles.catalogAvatar}
|
||||
size={24}
|
||||
>
|
||||
{entity.spec.icon?.material && <Icon material={entity.spec.icon?.material} small/>}
|
||||
</Avatar>
|
||||
<span>{entity.getName()}</span>
|
||||
<Icon
|
||||
small
|
||||
className={styles.pinIcon}
|
||||
material={!isItemInHotbar && "push_pin"}
|
||||
svg={isItemInHotbar ? "push_off" : "push_pin"}
|
||||
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const nameCategoryColumnInjectable = getInjectable({
|
||||
id: "name-category-column",
|
||||
instantiate: (di): RegisteredAdditionalCategoryColumn => ({
|
||||
id: "name",
|
||||
priority: 0,
|
||||
renderCell: renderEntityName(di.inject(hotbarStoreInjectable)),
|
||||
titleProps: {
|
||||
title: "Name",
|
||||
className: styles.entityName,
|
||||
id: "name",
|
||||
sortBy: "name",
|
||||
},
|
||||
searchFilter: (entity) => entity.getName(),
|
||||
sortCallback: (entity) => `name=${entity.getName()}`,
|
||||
}),
|
||||
});
|
||||
|
||||
export default nameCategoryColumnInjectable;
|
||||
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import mockFs from "mock-fs";
|
||||
import React from "react";
|
||||
import * as selectEvent from "react-select-event";
|
||||
@ -16,6 +16,11 @@ import type { ClusterModel } from "../../../../common/cluster-types";
|
||||
import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing";
|
||||
import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token";
|
||||
import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable";
|
||||
import deleteClusterDialogModelInjectable from "../delete-cluster-dialog-model/delete-cluster-dialog-model.injectable";
|
||||
import type { DeleteClusterDialogModel } from "../delete-cluster-dialog-model/delete-cluster-dialog-model";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
import { renderFor } from "../../test-utils/renderFor";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@ -88,16 +93,23 @@ let config: KubeConfig;
|
||||
|
||||
describe("<DeleteClusterDialog />", () => {
|
||||
let createCluster: (model: ClusterModel) => Cluster;
|
||||
let deleteClusterDialogModel: DeleteClusterDialogModel;
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
const { mainDi, rendererDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
render = renderFor(rendererDi);
|
||||
|
||||
mainDi.override(createContextHandlerInjectable, () => () => undefined);
|
||||
|
||||
mockFs();
|
||||
|
||||
rendererDi.override(hotbarStoreInjectable, () => ({}));
|
||||
|
||||
await runSetups();
|
||||
|
||||
deleteClusterDialogModel = rendererDi.inject(deleteClusterDialogModelInjectable);
|
||||
createCluster = mainDi.inject(createClusterInjectionToken);
|
||||
});
|
||||
|
||||
@ -137,7 +149,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
});
|
||||
|
||||
DeleteClusterDialog.open({ cluster, config });
|
||||
deleteClusterDialogModel.open({ cluster, config });
|
||||
const { getByText } = render(<DeleteClusterDialog />);
|
||||
|
||||
const message = "The contents of kubeconfig file will be changed!";
|
||||
@ -155,7 +167,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
});
|
||||
|
||||
DeleteClusterDialog.open({ cluster, config });
|
||||
deleteClusterDialogModel.open({ cluster, config });
|
||||
|
||||
const { getByTestId } = render(<DeleteClusterDialog />);
|
||||
|
||||
@ -172,7 +184,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
});
|
||||
|
||||
DeleteClusterDialog.open({ cluster, config });
|
||||
deleteClusterDialogModel.open({ cluster, config });
|
||||
|
||||
const { getByText } = render(<DeleteClusterDialog />);
|
||||
|
||||
@ -193,7 +205,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
});
|
||||
|
||||
DeleteClusterDialog.open({ cluster, config });
|
||||
deleteClusterDialogModel.open({ cluster, config });
|
||||
|
||||
const { getByText, getByTestId } = render(<DeleteClusterDialog />);
|
||||
const link = getByTestId("context-switch");
|
||||
@ -220,7 +232,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
|
||||
const spy = jest.spyOn(cluster, "isInLocalKubeconfig").mockImplementation(() => true);
|
||||
|
||||
DeleteClusterDialog.open({ cluster, config });
|
||||
deleteClusterDialogModel.open({ cluster, config });
|
||||
|
||||
const { getByTestId } = render(<DeleteClusterDialog />);
|
||||
|
||||
@ -256,7 +268,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
kubeConfigPath: "./temp-kube-config",
|
||||
});
|
||||
|
||||
DeleteClusterDialog.open({ cluster, config });
|
||||
deleteClusterDialogModel.open({ cluster, config });
|
||||
|
||||
const { getByTestId } = render(<DeleteClusterDialog />);
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 { DeleteClusterDialogModel } from "./delete-cluster-dialog-model";
|
||||
|
||||
const deleteClusterDialogModelInjectable = getInjectable({
|
||||
id: "delete-cluster-dialog-model",
|
||||
instantiate: () => new DeleteClusterDialogModel(),
|
||||
});
|
||||
|
||||
export default deleteClusterDialogModelInjectable;
|
||||
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { observable, makeObservable, action } from "mobx";
|
||||
import type { Cluster } from "../../../../common/cluster/cluster";
|
||||
|
||||
export class DeleteClusterDialogModel {
|
||||
isOpen = false;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
isOpen: observable,
|
||||
open: action,
|
||||
close: action,
|
||||
});
|
||||
}
|
||||
|
||||
cluster: Cluster;
|
||||
config: KubeConfig;
|
||||
|
||||
open = ({ cluster, config }: { cluster: Cluster; config: KubeConfig }) => {
|
||||
this.isOpen = true;
|
||||
|
||||
this.cluster = cluster;
|
||||
this.config = config;
|
||||
};
|
||||
|
||||
close = () => {
|
||||
this.isOpen = false;
|
||||
};
|
||||
}
|
||||
@ -9,52 +9,35 @@ import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
|
||||
import { Button } from "../button";
|
||||
import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { saveKubeconfig } from "./save-config";
|
||||
import { Notifications } from "../notifications";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Icon } from "../icon";
|
||||
import { Select } from "../select";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { requestClearClusterAsDeleting, requestDeleteCluster, requestSetClusterAsDeleting } from "../../ipc";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { DeleteClusterDialogModel } from "./delete-cluster-dialog-model/delete-cluster-dialog-model";
|
||||
import deleteClusterDialogModelInjectable from "./delete-cluster-dialog-model/delete-cluster-dialog-model.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
|
||||
interface DialogState {
|
||||
isOpen: boolean;
|
||||
config?: KubeConfig;
|
||||
cluster?: Cluster;
|
||||
interface Dependencies {
|
||||
hotbarStore: HotbarStore;
|
||||
model: DeleteClusterDialogModel;
|
||||
}
|
||||
|
||||
const dialogState: DialogState = observable({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
export interface DeleteClusterDialogProps {}
|
||||
|
||||
@observer
|
||||
export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProps> {
|
||||
class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
|
||||
@observable showContextSwitch = false;
|
||||
@observable newCurrentContext = "";
|
||||
|
||||
constructor(props: DeleteClusterDialogProps) {
|
||||
constructor(props: Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
static open({ config, cluster }: Partial<DialogState>) {
|
||||
dialogState.isOpen = true;
|
||||
dialogState.config = config;
|
||||
dialogState.cluster = cluster;
|
||||
}
|
||||
|
||||
static close() {
|
||||
dialogState.isOpen = false;
|
||||
dialogState.cluster = null;
|
||||
dialogState.config = null;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onOpen() {
|
||||
this.newCurrentContext = "";
|
||||
@ -66,25 +49,25 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
|
||||
|
||||
@boundMethod
|
||||
onClose() {
|
||||
DeleteClusterDialog.close();
|
||||
this.props.model.close();
|
||||
this.showContextSwitch = false;
|
||||
}
|
||||
|
||||
removeContext() {
|
||||
dialogState.config.contexts = dialogState.config.contexts.filter(item =>
|
||||
item.name !== dialogState.cluster.contextName,
|
||||
this.props.model.config.contexts = this.props.model.config.contexts.filter(item =>
|
||||
item.name !== this.props.model.cluster.contextName,
|
||||
);
|
||||
}
|
||||
|
||||
changeCurrentContext() {
|
||||
if (this.newCurrentContext && this.showContextSwitch) {
|
||||
dialogState.config.currentContext = this.newCurrentContext;
|
||||
this.props.model.config.currentContext = this.newCurrentContext;
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
async onDelete() {
|
||||
const { cluster, config } = dialogState;
|
||||
const { cluster, config } = this.props.model;
|
||||
|
||||
await requestSetClusterAsDeleting(cluster.id);
|
||||
this.removeContext();
|
||||
@ -92,7 +75,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
|
||||
|
||||
try {
|
||||
await saveKubeconfig(config, cluster.kubeConfigPath);
|
||||
HotbarStore.getInstance().removeAllHotbarItems(cluster.id);
|
||||
this.props.hotbarStore.removeAllHotbarItems(cluster.id);
|
||||
await requestDeleteCluster(cluster.id);
|
||||
} catch(error) {
|
||||
Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`);
|
||||
@ -104,7 +87,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
|
||||
}
|
||||
|
||||
@computed get disableDelete() {
|
||||
const { cluster, config } = dialogState;
|
||||
const { cluster, config } = this.props.model;
|
||||
const noContextsAvailable = config.contexts.filter(context => context.name !== cluster.contextName).length == 0;
|
||||
const newContextNotSelected = this.newCurrentContext === "";
|
||||
|
||||
@ -116,12 +99,12 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
|
||||
}
|
||||
|
||||
isCurrentContext() {
|
||||
return dialogState.config.currentContext == dialogState.cluster.contextName;
|
||||
return this.props.model.config.currentContext == this.props.model.cluster.contextName;
|
||||
}
|
||||
|
||||
renderCurrentContextSwitch() {
|
||||
if (!this.showContextSwitch) return null;
|
||||
const { cluster, config } = dialogState;
|
||||
const { cluster, config } = this.props.model;
|
||||
const contexts = config.contexts.filter(context => context.name !== cluster.contextName);
|
||||
|
||||
const options = [
|
||||
@ -146,7 +129,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
|
||||
}
|
||||
|
||||
renderDeleteMessage() {
|
||||
const { cluster } = dialogState;
|
||||
const { cluster } = this.props.model;
|
||||
|
||||
if (cluster.isInLocalKubeconfig()) {
|
||||
return (
|
||||
@ -164,7 +147,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
|
||||
}
|
||||
|
||||
getWarningMessage() {
|
||||
const { cluster, config } = dialogState;
|
||||
const { cluster, config } = this.props.model;
|
||||
const contexts = config.contexts.filter(context => context.name !== cluster.contextName);
|
||||
|
||||
if (!contexts.length) {
|
||||
@ -206,7 +189,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
|
||||
}
|
||||
|
||||
render() {
|
||||
const { cluster, config, isOpen } = dialogState;
|
||||
const { cluster, config, isOpen } = this.props.model;
|
||||
|
||||
if (!cluster || !config) return null;
|
||||
|
||||
@ -258,3 +241,14 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const DeleteClusterDialog = withInjectables<Dependencies>(
|
||||
NonInjectedDeleteClusterDialog,
|
||||
|
||||
{
|
||||
getProps: (di) => ({
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
model: di.inject(deleteClusterDialogModelInjectable),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -10,7 +10,7 @@ import React from "react";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||
import hotbarStoreInjectable, { HotbarStore } from "../../../../common/hotbar-store.injectable";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import { ConfirmDialog } from "../../confirm-dialog";
|
||||
import { UserStore } from "../../../../common/user-store";
|
||||
@ -18,6 +18,7 @@ import mockFs from "mock-fs";
|
||||
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
|
||||
import appVersionInjectable from "../../../../common/get-configuration-file-model/app-version/app-version.injectable";
|
||||
import type { HotbarStore } from "../../../../common/hotbar-store";
|
||||
|
||||
const mockHotbars: { [id: string]: any } = {
|
||||
"1": {
|
||||
|
||||
@ -10,7 +10,6 @@ import { observer } from "mobx-react";
|
||||
import { HotbarEntityIcon } from "./hotbar-entity-icon";
|
||||
import { cssNames, IClassName } from "../../utils";
|
||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||
import { DragDropContext, Draggable, Droppable, type DropResult } from "react-beautiful-dnd";
|
||||
import { HotbarSelector } from "./hotbar-selector";
|
||||
@ -18,26 +17,33 @@ import { HotbarCell } from "./hotbar-cell";
|
||||
import { HotbarIcon } from "./hotbar-icon";
|
||||
import { defaultHotbarCells, HotbarItem } from "../../../common/hotbar-types";
|
||||
import { action, makeObservable, observable } from "mobx";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
|
||||
export interface HotbarMenuProps {
|
||||
className?: IClassName;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
hotbarStore: HotbarStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class HotbarMenu extends React.Component<HotbarMenuProps> {
|
||||
class NonInjectedHotbarMenu extends React.Component<Dependencies & HotbarMenuProps> {
|
||||
@observable draggingOver = false;
|
||||
|
||||
constructor(props: HotbarMenuProps) {
|
||||
constructor(props: Dependencies & HotbarMenuProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get hotbar() {
|
||||
return HotbarStore.getInstance().getActive();
|
||||
return this.props.hotbarStore.getActive();
|
||||
}
|
||||
|
||||
getEntity(item: HotbarItem) {
|
||||
const hotbar = HotbarStore.getInstance().getActive();
|
||||
const hotbar = this.props.hotbarStore.getActive();
|
||||
|
||||
if (!hotbar) {
|
||||
return null;
|
||||
@ -64,17 +70,17 @@ export class HotbarMenu extends React.Component<HotbarMenuProps> {
|
||||
const from = parseInt(source.droppableId);
|
||||
const to = parseInt(destination.droppableId);
|
||||
|
||||
HotbarStore.getInstance().restackItems(from, to);
|
||||
this.props.hotbarStore.restackItems(from, to);
|
||||
}
|
||||
|
||||
removeItem(uid: string) {
|
||||
const hotbar = HotbarStore.getInstance();
|
||||
const hotbar = this.props.hotbarStore;
|
||||
|
||||
hotbar.removeFromHotbar(uid);
|
||||
}
|
||||
|
||||
addItem(entity: CatalogEntity, index = -1) {
|
||||
const hotbar = HotbarStore.getInstance();
|
||||
const hotbar = this.props.hotbarStore;
|
||||
|
||||
hotbar.addToHotbar(entity, index);
|
||||
}
|
||||
@ -160,8 +166,7 @@ export class HotbarMenu extends React.Component<HotbarMenuProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
const { className, hotbarStore } = this.props;
|
||||
const hotbar = hotbarStore.getActive();
|
||||
|
||||
return (
|
||||
@ -176,3 +181,14 @@ export class HotbarMenu extends React.Component<HotbarMenuProps> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const HotbarMenu = withInjectables<Dependencies, HotbarMenuProps>(
|
||||
NonInjectedHotbarMenu,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -6,13 +6,14 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Select } from "../select";
|
||||
import hotbarStoreInjectable, { HotbarStore } from "../../../common/hotbar-store.injectable";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { CommandOverlay } from "../command-palette";
|
||||
import { HotbarAddCommand } from "./hotbar-add-command";
|
||||
import { HotbarRemoveCommand } from "./hotbar-remove-command";
|
||||
import { HotbarRenameCommand } from "./hotbar-rename-command";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
|
||||
const addActionId = "__add__";
|
||||
const removeActionId = "__remove__";
|
||||
|
||||
@ -5,17 +5,14 @@
|
||||
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
import { render, fireEvent } from "@testing-library/react";
|
||||
import { fireEvent } from "@testing-library/react";
|
||||
import { SidebarCluster } from "../sidebar-cluster";
|
||||
import { KubernetesCluster } from "../../../../common/catalog-entities";
|
||||
|
||||
jest.mock("../../../../common/hotbar-store", () => ({
|
||||
HotbarStore: {
|
||||
getInstance: () => ({
|
||||
isAddedToActive: jest.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||
import type { DiRender } from "../../test-utils/renderFor";
|
||||
import { renderFor } from "../../test-utils/renderFor";
|
||||
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../../common/hotbar-store";
|
||||
|
||||
const clusterEntity = new KubernetesCluster({
|
||||
metadata: {
|
||||
@ -34,6 +31,18 @@ const clusterEntity = new KubernetesCluster({
|
||||
});
|
||||
|
||||
describe("<SidebarCluster/>", () => {
|
||||
let render: DiRender;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(hotbarStoreInjectable, () => ({
|
||||
isAddedToActive: () => {},
|
||||
}) as unknown as HotbarStore);
|
||||
|
||||
render = renderFor(di);
|
||||
});
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = render(<SidebarCluster clusterEntity={clusterEntity}/>);
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
import styles from "./sidebar-cluster.module.scss";
|
||||
import { observable } from "mobx";
|
||||
import React, { useState } from "react";
|
||||
import { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||
import { IpcRendererNavigationEvents } from "../../navigation/events";
|
||||
@ -16,6 +15,9 @@ import { navigate } from "../../navigation";
|
||||
import { Menu, MenuItem } from "../menu";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { Tooltip } from "../tooltip";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
|
||||
import type { HotbarStore } from "../../../common/hotbar-store";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
const contextMenu: CatalogEntityContextMenuContext = observable({
|
||||
@ -60,7 +62,15 @@ function renderLoadingSidebarCluster() {
|
||||
);
|
||||
}
|
||||
|
||||
export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: CatalogEntity }) => {
|
||||
interface Dependencies {
|
||||
hotbarStore: HotbarStore;
|
||||
}
|
||||
|
||||
interface SidebarClusterProps {
|
||||
clusterEntity: CatalogEntity;
|
||||
}
|
||||
|
||||
const NonInjectedSidebarCluster = observer(({ clusterEntity, hotbarStore }: Dependencies & SidebarClusterProps) => {
|
||||
const [opened, setOpened] = useState(false);
|
||||
|
||||
if (!clusterEntity) {
|
||||
@ -68,8 +78,7 @@ export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: Cata
|
||||
}
|
||||
|
||||
const onMenuOpen = () => {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
const isAddedToActive = HotbarStore.getInstance().isAddedToActive(clusterEntity);
|
||||
const isAddedToActive = hotbarStore.isAddedToActive(clusterEntity);
|
||||
const title = isAddedToActive
|
||||
? "Remove from Hotbar"
|
||||
: "Add to Hotbar";
|
||||
@ -140,3 +149,14 @@ export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: Cata
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const SidebarCluster = withInjectables<Dependencies, SidebarClusterProps>(
|
||||
NonInjectedSidebarCluster,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
hotbarStore: di.inject(hotbarStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -32,6 +32,7 @@ import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectab
|
||||
import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake";
|
||||
import joinPathsInjectable from "../common/path/join-paths.injectable";
|
||||
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
|
||||
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
|
||||
|
||||
export const getDiForUnitTesting = (
|
||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||
@ -62,6 +63,8 @@ export const getDiForUnitTesting = (
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
|
||||
|
||||
di.override(hotbarStoreInjectable, () => ({}));
|
||||
|
||||
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
|
||||
@ -10,9 +10,9 @@ import { ClusterStore } from "../../common/cluster-store/cluster-store";
|
||||
import { catalogCategoryRegistry } from "../api/catalog-category-registry";
|
||||
import { WeblinkAddCommand } from "../components/catalog-entities/weblink-add-command";
|
||||
import { loadConfigFromString } from "../../common/kube-helpers";
|
||||
import { DeleteClusterDialog } from "../components/delete-cluster-dialog";
|
||||
import type { DeleteClusterDialogModel } from "../components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model";
|
||||
|
||||
async function onClusterDelete(clusterId: string) {
|
||||
async function onClusterDelete(clusterId: string, deleteClusterDialogModel: DeleteClusterDialogModel) {
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (!cluster) {
|
||||
@ -25,14 +25,15 @@ async function onClusterDelete(clusterId: string) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
DeleteClusterDialog.open({ cluster, config });
|
||||
deleteClusterDialogModel.open({ cluster, config });
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
openCommandDialog: (component: React.ReactElement) => void;
|
||||
deleteClusterDialogModel: DeleteClusterDialogModel;
|
||||
}
|
||||
|
||||
export function initCatalog({ openCommandDialog }: Dependencies) {
|
||||
export function initCatalog({ openCommandDialog, deleteClusterDialogModel }: Dependencies) {
|
||||
catalogCategoryRegistry
|
||||
.getForGroupKind("entity.k8slens.dev", "WebLink")
|
||||
.on("catalogAddMenu", ctx => {
|
||||
@ -50,7 +51,7 @@ export function initCatalog({ openCommandDialog }: Dependencies) {
|
||||
context.menuItems.push({
|
||||
title: "Remove",
|
||||
icon: "delete",
|
||||
onClick: () => onClusterDelete(entity.getId()),
|
||||
onClick: () => onClusterDelete(entity.getId(), deleteClusterDialogModel),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user