1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fully split apart the hotbar storage

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-03-17 11:29:26 -04:00
parent f1f8e198a2
commit ae32375beb
56 changed files with 1904 additions and 1478 deletions

View File

@ -1,356 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { anyObject } from "jest-mock-extended";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { DiContainer } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../hotbars/store.injectable";
import type { HotbarStore } from "../hotbars/store";
import catalogEntityRegistryInjectable from "../../main/catalog/entity-registry.injectable";
import { computed } from "mobx";
import hasCategoryForEntityInjectable from "../catalog/has-category-for-entity.injectable";
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import loggerInjectable from "../logger.injectable";
import type { Logger } from "../logger";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import writeJsonSyncInjectable from "../fs/write-json-sync.injectable";
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return {
getName: jest.fn(() => data.metadata?.name),
getId: jest.fn(() => data.metadata?.uid),
getSource: jest.fn(() => data.metadata?.source ?? "unknown"),
isEnabled: jest.fn(() => data.status?.enabled ?? true),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {},
spec: {},
status: {},
...data,
} as CatalogEntity;
}
describe("HotbarStore", () => {
let di: DiContainer;
let hotbarStore: HotbarStore;
let testCluster: CatalogEntity;
let minikubeCluster: CatalogEntity;
let awsCluster: CatalogEntity;
let loggerMock: jest.Mocked<Logger>;
beforeEach(async () => {
di = getDiForUnitTesting();
testCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-test-id",
name: "my-test-cluster",
source: "local",
labels: {},
},
});
minikubeCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-minikube-id",
name: "my-minikube-cluster",
source: "local",
labels: {},
},
});
awsCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-aws-id",
name: "my-aws-cluster",
source: "local",
labels: {},
},
});
di.override(hasCategoryForEntityInjectable, () => () => true);
loggerMock = {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
silly: jest.fn(),
};
di.override(loggerInjectable, () => loggerMock);
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
catalogEntityRegistry.addComputedSource("some-id", computed(() => [
testCluster,
minikubeCluster,
awsCluster,
catalogCatalogEntity,
]));
});
describe("given no previous data in store, running all migrations", () => {
beforeEach(() => {
hotbarStore = di.inject(hotbarStoreInjectable);
hotbarStore.load();
});
describe("load", () => {
it("loads one hotbar by default", () => {
expect(hotbarStore.hotbars.length).toEqual(1);
});
});
describe("add", () => {
it("adds a hotbar", () => {
hotbarStore.add({ name: "hottest" });
expect(hotbarStore.hotbars.length).toEqual(2);
});
});
describe("hotbar items", () => {
it("initially creates 12 empty cells", () => {
expect(hotbarStore.getActive().items.length).toEqual(12);
});
it("initially adds catalog entity as first item", () => {
expect(hotbarStore.getActive().items[0]?.entity.name).toEqual("Catalog");
});
it("adds items", () => {
hotbarStore.addToHotbar(testCluster);
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items.length).toEqual(2);
});
it("removes items", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("some-test-id");
hotbarStore.removeFromHotbar("catalog-entity");
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items).toStrictEqual([]);
});
it("does nothing if removing with invalid uid", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("invalid uid");
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items.length).toEqual(2);
});
it("moves item to empty cell", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
expect(hotbarStore.getActive().items[6]).toBeNull();
hotbarStore.restackItems(1, 5);
expect(hotbarStore.getActive().items[5]).toBeTruthy();
expect(hotbarStore.getActive().items[5]?.entity.uid).toEqual("some-test-id");
});
it("moves items down", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
// aws -> catalog
hotbarStore.restackItems(3, 0);
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
expect(items.slice(0, 4)).toEqual(["some-aws-id", "catalog-entity", "some-test-id", "some-minikube-id"]);
});
it("moves items up", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
// test -> aws
hotbarStore.restackItems(1, 3);
const items = hotbarStore.getActive().items.map(item => item?.entity.uid || null);
expect(items.slice(0, 4)).toEqual(["catalog-entity", "some-minikube-id", "some-aws-id", "some-test-id"]);
});
it("logs an error if cellIndex is out of bounds", () => {
hotbarStore.add({ name: "hottest", id: "hottest" });
hotbarStore.setActiveHotbar("hottest");
hotbarStore.addToHotbar(testCluster, -1);
expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 12);
expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 13);
expect(loggerMock.error).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
});
it("throws an error if getId is invalid or returns not a string", () => {
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", () => {
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", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.restackItems(1, 1);
expect(hotbarStore.getActive().items[1]?.entity.uid).toEqual("some-test-id");
});
it("new items takes first empty cell", () => {
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(awsCluster);
hotbarStore.restackItems(0, 3);
hotbarStore.addToHotbar(minikubeCluster);
expect(hotbarStore.getActive().items[0]?.entity.uid).toEqual("some-minikube-id");
});
it("throws if invalid arguments provided", () => {
hotbarStore.addToHotbar(testCluster);
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
expect(() => hotbarStore.restackItems(2, -1)).toThrow();
expect(() => hotbarStore.restackItems(14, 1)).toThrow();
expect(() => hotbarStore.restackItems(11, 112)).toThrow();
});
it("checks if entity already pinned to hotbar", () => {
hotbarStore.addToHotbar(testCluster);
expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy();
expect(hotbarStore.isAddedToActive(awsCluster)).toBeFalsy();
});
});
});
describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => {
beforeEach(() => {
const writeJsonSync = di.inject(writeJsonSyncInjectable);
writeJsonSync("/some-directory-for-user-data/lens-hotbar-store.json", {
__internal__: {
migrations: {
version: "5.0.0-beta.3",
},
},
hotbars: [
{
id: "3caac17f-aec2-4723-9694-ad204465d935",
name: "myhotbar",
items: [
{
entity: {
uid: "some-aws-id",
},
},
{
entity: {
uid: "55b42c3c7ba3b04193416cda405269a5",
},
},
{
entity: {
uid: "176fd331968660832f62283219d7eb6e",
},
},
{
entity: {
uid: "61c4fb45528840ebad1badc25da41d14",
name: "user1-context",
source: "local",
},
},
{
entity: {
uid: "27d6f99fe9e7548a6e306760bfe19969",
name: "foo2",
source: "local",
},
},
null,
{
entity: {
uid: "c0b20040646849bb4dcf773e43a0bf27",
name: "multinode-demo",
source: "local",
},
},
null,
null,
null,
null,
null,
],
},
],
});
di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10");
hotbarStore = di.inject(hotbarStoreInjectable);
hotbarStore.load();
});
it("allows to retrieve a hotbar", () => {
const hotbar = hotbarStore.findById("3caac17f-aec2-4723-9694-ad204465d935");
expect(hotbar?.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
});
it("clears cells without entity", () => {
const items = hotbarStore.hotbars[0].items;
expect(items[2]).toBeNull();
});
it("adds extra data to cells with according entity", () => {
const items = hotbarStore.hotbars[0].items;
expect(items[0]).toEqual({
entity: {
name: "my-aws-cluster",
source: "local",
uid: "some-aws-id",
},
});
});
});
});

View File

@ -1,20 +0,0 @@
/**
* 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 hotbarStoreInjectable from "./store.injectable";
import type { CreateHotbarData, CreateHotbarOptions } from "./types";
export type AddHotbar = (data: CreateHotbarData, opts?: CreateHotbarOptions) => void;
const addHotbarInjectable = getInjectable({
id: "add-hotbar",
instantiate: (di): AddHotbar => {
const store = di.inject(hotbarStoreInjectable);
return (data, opts) => store.add(data, opts);
},
});
export default addHotbarInjectable;

View File

@ -1,26 +0,0 @@
/**
* 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 { HotbarStore } from "./store";
import loggerInjectable from "../logger.injectable";
import persistentStorageMigrationsInjectable from "../persistent-storage/migrations.injectable";
import { hotbarStoreMigrationInjectionToken } from "./migrations-token";
import createPersistentStorageInjectable from "../persistent-storage/create.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
const hotbarStoreInjectable = getInjectable({
id: "hotbar-store",
instantiate: (di) => new HotbarStore({
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(persistentStorageMigrationsInjectable, hotbarStoreMigrationInjectionToken),
createPersistentStorage: di.inject(createPersistentStorageInjectable),
}),
});
export default hotbarStoreInjectable;

View File

@ -1,382 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IObservableValue } from "mobx";
import { runInAction, action, comparer, observable } from "mobx";
import type { CatalogEntity } from "../catalog";
import { broadcastMessage } from "../ipc";
import type { Hotbar, CreateHotbarData, CreateHotbarOptions } from "./types";
import { defaultHotbarCells, getEmptyHotbar } from "./types";
import { hotbarTooManyItemsChannel } from "../ipc/hotbar";
import type { GeneralEntity } from "../catalog-entities";
import type { Logger } from "../logger";
import assert from "assert";
import { getShortName } from "../catalog/helpers";
import type { Migrations } from "conf/dist/source/types";
import type { CreatePersistentStorage, PersistentStorage } from "../persistent-storage/create.injectable";
export interface HotbarStoreModel {
hotbars: Hotbar[];
activeHotbarId: string;
}
interface Dependencies {
readonly catalogCatalogEntity: GeneralEntity;
readonly logger: Logger;
readonly storeMigrationVersion: string;
readonly migrations: Migrations<Record<string, unknown>>;
createPersistentStorage: CreatePersistentStorage;
}
export class HotbarStore {
private readonly store: PersistentStorage;
readonly hotbars = observable.array<Hotbar>();
readonly activeHotbarId = observable.box() as IObservableValue<string>;
constructor(protected readonly dependencies: Dependencies) {
this.store = this.dependencies.createPersistentStorage<HotbarStoreModel>({
configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
projectVersion: this.dependencies.storeMigrationVersion,
migrations: this.dependencies.migrations,
fromStore: action((data) => {
if (!data.hotbars || !data.hotbars.length) {
const hotbar = getEmptyHotbar("Default");
const {
metadata: {
uid,
name,
source,
},
} = this.dependencies.catalogCatalogEntity;
hotbar.items[0] = {
entity: {
uid,
name,
source,
},
};
this.hotbars.replace([hotbar]);
} else {
this.hotbars.replace(data.hotbars);
}
for (const hotbar of this.hotbars) {
ensureExactHotbarItemLength(hotbar);
}
if (data.activeHotbarId) {
this.activeHotbarId.set(data.activeHotbarId);
}
if (!this.activeHotbarId.get()) {
this.activeHotbarId.set(this.hotbars[0].id);
}
const activeHotbarExists = this.hotbars.findIndex(hotbar => hotbar.id === this.activeHotbarId.get()) >= 0;
if (!activeHotbarExists) {
this.activeHotbarId.set(this.hotbars[0].id);
}
}),
toJSON: () => ({
hotbars: this.hotbars.toJSON(),
activeHotbarId: this.activeHotbarId.get(),
}),
});
}
load() {
this.store.loadAndStartSyncing();
}
/**
* 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) {
runInAction(() => {
if (typeof hotbar === "number") {
if (hotbar >= 0 && hotbar < this.hotbars.length) {
this.activeHotbarId.set(this.hotbars[hotbar].id);
}
} else if (typeof hotbar === "string") {
if (this.findById(hotbar)) {
this.activeHotbarId.set(hotbar);
}
} else {
if (this.hotbars.indexOf(hotbar) >= 0) {
this.activeHotbarId.set(hotbar.id);
}
}
});
}
private getActiveHotbarIndex() {
return this.hotbars.findIndex((hotbar) => hotbar.id === this.activeHotbarId.get());
}
getActive(): Hotbar {
const hotbar = this.findById(this.activeHotbarId.get());
assert(hotbar, "There MUST always be an active hotbar");
return hotbar;
}
findByName(name: string) {
return this.hotbars.find((hotbar) => hotbar.name === name);
}
findById(id: string) {
return this.hotbars.find((hotbar) => hotbar.id === id);
}
add(data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) {
runInAction(() => {
const hotbar = getEmptyHotbar(data.name, data.id);
this.hotbars.push(hotbar);
if (setActive) {
this.activeHotbarId.set(hotbar.id);
}
});
}
setHotbarName(id: string, name: string): void {
runInAction(() => {
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if (index < 0) {
return this.dependencies.logger.warn(
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
{ id },
);
}
this.hotbars[index].name = name;
});
}
remove(hotbar: Hotbar) {
runInAction(() => {
assert(this.hotbars.length >= 2, "Cannot remove the last hotbar");
this.hotbars.replace(this.hotbars.filter((h) => h.id !== hotbar.id));
if (this.activeHotbarId.get() === hotbar.id) {
this.activeHotbarId.set(this.hotbars[0].id);
}
});
}
addToHotbar(item: CatalogEntity, cellIndex?: number) {
runInAction(() => {
const hotbar = this.getActive();
const uid = item.getId();
const name = item.getName();
const shortName = getShortName(item);
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 (typeof shortName !== "string") {
throw new TypeError("CatalogEntity's SHORT_NAME must be a string");
}
if (this.isAddedToActive(item)) {
return;
}
const entity = {
uid,
name,
source: item.metadata.source,
shortName,
};
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 {
this.dependencies.logger.error(
`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`,
{ entityId: uid, hotbarId: hotbar.id, cellIndex },
);
}
});
}
removeFromHotbar(uid: string): void {
runInAction(() => {
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.
*/
removeAllHotbarItems(uid: string) {
runInAction(() => {
for (const hotbar of this.hotbars) {
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
if (index >= 0) {
hotbar.items[index] = null;
}
}
});
}
private findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
const hotbar = this.getActive();
while (hotbar.items[index] != null) {
index += direction;
}
return index;
}
restackItems(from: number, to: number): void {
runInAction(() => {
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() {
runInAction(() => {
let index = this.getActiveHotbarIndex() - 1;
if (index < 0) {
index = this.hotbars.length - 1;
}
this.setActiveHotbar(index);
});
}
switchToNext() {
runInAction(() => {
let index = this.getActiveHotbarIndex() + 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;
}
const indexInActiveHotbar = this.getActive().items.findIndex(item => item?.entity.uid === entity.getId());
return indexInActiveHotbar >= 0;
}
getDisplayLabel(hotbar: Hotbar): string {
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
}
getDisplayIndex(hotbar: Hotbar): string {
const index = this.hotbars.indexOf(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;
}
}
}

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import activeHotbarIdInjectable from "./active-id.injectable";
import computeHotbarIndexInjectable from "./compute-hotbar-index.injectable";
const activeHotbarIndexInjectable = getInjectable({
id: "active-hotbar-index",
instantiate: (di) => {
const computeHotbarIndex = di.inject(computeHotbarIndexInjectable);
const activeHotbarId = di.inject(activeHotbarIdInjectable);
return computed(() => {
const activeId = activeHotbarId.get();
return (activeId && computeHotbarIndex(activeId)) || 0;
});
},
});
export default activeHotbarIndexInjectable;

View File

@ -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 { observable } from "mobx";
const activeHotbarIdInjectable = getInjectable({
id: "active-hotbar-id",
instantiate: () => observable.box<string>(),
});
export default activeHotbarIdInjectable;

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import activeHotbarIdInjectable from "./active-id.injectable";
import hotbarsStateInjectable from "./state.injectable";
const activeHotbarInjectable = getInjectable({
id: "active-hotbar",
instantiate: (di) => {
const state = di.inject(hotbarsStateInjectable);
const activeHotbarId = di.inject(activeHotbarIdInjectable);
return computed(() => {
const id = activeHotbarId.get();
return (id && state.get(id)) || undefined;
});
},
});
export default activeHotbarInjectable;

View File

@ -0,0 +1,33 @@
/**
* 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 { action } from "mobx";
import type { CreateHotbarData, CreateHotbarOptions } from "./types";
import activeHotbarIdInjectable from "./active-id.injectable";
import hotbarsStateInjectable from "./state.injectable";
import createHotbarInjectable from "./create-hotbar.injectable";
export type AddHotbar = (data: CreateHotbarData, { setActive }?: CreateHotbarOptions) => void;
const addHotbarInjectable = getInjectable({
id: "add-hotbar",
instantiate: (di): AddHotbar => {
const state = di.inject(hotbarsStateInjectable);
const activeHotbarId = di.inject(activeHotbarIdInjectable);
const createHotbar = di.inject(createHotbarInjectable);
return action((data, { setActive = false } = {}) => {
const hotbar = createHotbar(data);
state.set(hotbar.id, hotbar);
if (setActive) {
activeHotbarId.set(hotbar.id);
}
});
},
});
export default addHotbarInjectable;

View File

@ -0,0 +1,19 @@
/**
* 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 computeHotbarIndexInjectable from "./compute-hotbar-index.injectable";
export type ComputeDisplayIndex = (hotbarId: string) => string;
const computeDisplayIndexInjectable = getInjectable({
id: "compute-display-index",
instantiate: (di): ComputeDisplayIndex => {
const computeHotbarIndex = di.inject(computeHotbarIndexInjectable);
return (hotbarId) => `${computeHotbarIndex(hotbarId) + 1}`;
},
});
export default computeDisplayIndexInjectable;

View File

@ -0,0 +1,20 @@
/**
* 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 type { Hotbar } from "./hotbar";
import computeDisplayIndexInjectable from "./compute-display-index.injectable";
export type ComputeHotbarDisplayLabel = (hotbar: Hotbar) => string;
const computeHotbarDisplayLabelInjectable = getInjectable({
id: "compute-hotbar-display-label",
instantiate: (di): ComputeHotbarDisplayLabel => {
const computeDisplayIndex = di.inject(computeDisplayIndexInjectable);
return (hotbar) => `${computeDisplayIndex(hotbar.id)}: ${hotbar.name}`;
},
});
export default computeHotbarDisplayLabelInjectable;

View File

@ -0,0 +1,31 @@
/**
* 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 hotbarsStateInjectable from "./state.injectable";
export type ComputeHotbarIndex = (hotbarId: string) => number;
const computeHotbarIndexInjectable = getInjectable({
id: "compute-hotbar-index",
instantiate: (di): ComputeHotbarIndex => {
const state = di.inject(hotbarsStateInjectable);
return (hotbarId) => {
let i = 0;
for (const hotbar of state.values()) {
if (hotbar.id === hotbarId) {
return i;
}
i += 1;
}
return 0;
};
},
});
export default computeHotbarIndexInjectable;

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { CreateHotbarData } from "./types";
import prefixedLoggerInjectable from "../../../../common/logger/prefixed-logger.injectable";
import type { HotbarDependencies } from "./hotbar";
import { Hotbar } from "./hotbar";
export type CreateHotbar = (data: CreateHotbarData) => Hotbar;
const createHotbarInjectable = getInjectable({
id: "create-hotbar",
instantiate: (di): CreateHotbar => {
const deps: HotbarDependencies = {
logger: di.inject(prefixedLoggerInjectable, "HOTBAR"),
};
return (data) => new Hotbar(deps, data);
},
});
export default createHotbarInjectable;

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { iter } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import type { Hotbar } from "./hotbar";
import hotbarsStateInjectable from "./state.injectable";
export type FindHotbarByName = (name: string) => Hotbar | undefined;
const findHotbarByNameInjectable = getInjectable({
id: "find-hotbar-by-name",
instantiate: (di): FindHotbarByName => {
const state = di.inject(hotbarsStateInjectable);
return (name) => iter.find(state.values(), hotbar => hotbar.name.get() === name);
},
});
export default findHotbarByNameInjectable;

View File

@ -0,0 +1,20 @@
/**
* 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 type { Hotbar } from "./hotbar";
import hotbarsStateInjectable from "./state.injectable";
export type GetHotbarById = (id: string) => Hotbar | undefined;
const getHotbarByIdInjectable = getInjectable({
id: "get-hotbar-by-id",
instantiate: (di): GetHotbarById => {
const state = di.inject(hotbarsStateInjectable);
return (id) => state.get(id);
},
});
export default getHotbarByIdInjectable;

View File

@ -0,0 +1,175 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { type IObservableValue, type IObservableArray, observable, runInAction, toJS } from "mobx";
import type { CatalogEntity } from "../../../../common/catalog";
import { getShortName } from "../../../../common/catalog/helpers";
import type { HotbarItem, CreateHotbarData } from "./types";
import { defaultHotbarCells } from "./types";
import { broadcastMessage } from "../../../../common/ipc";
import { hotbarTooManyItemsChannel } from "../../../../common/ipc/hotbar";
import * as uuid from "uuid";
import type { Logger } from "../../../../common/logger";
import { tuple } from "@k8slens/utilities";
export interface HotbarDependencies {
readonly logger: Logger;
}
export interface HotbarData {
readonly id: string;
readonly name: string;
readonly items: (HotbarItem | null)[];
}
export class Hotbar {
readonly id: string;
readonly name: IObservableValue<string>;
readonly items: IObservableArray<HotbarItem | null>;
constructor(private readonly dependencies: HotbarDependencies, data: CreateHotbarData) {
this.id = data.id ?? uuid.v4();
this.name = observable.box(data.name);
this.items = observable.array(data.items ?? tuple.filled(defaultHotbarCells, null));
}
isFull() {
for (const item of this.items) {
if (!item) {
return false;
}
}
return true;
}
hasEntity(entityId: string) {
return this.items.findIndex(item => item?.entity.uid === entityId) >= 0;
}
private findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
while (this.items[index] != null) {
index += direction;
}
return index;
}
restack(from: number, to: number) {
runInAction(() => {
const source = this.items[from];
const moveDown = from < to;
if (
from < 0 ||
to < 0 ||
from >= this.items.length ||
to >= this.items.length ||
isNaN(from) ||
isNaN(to)
) {
throw new Error("Invalid 'from' or 'to' arguments");
}
if (from == to) {
return;
}
this.items.splice(from, 1, null);
if (this.items[to] == null) {
this.items.splice(to, 1, source);
} else {
// Move cells up or down to closes empty cell
this.items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1);
this.items.splice(to, 0, source);
}
});
}
toggleEntity(item: CatalogEntity) {
runInAction(() => {
if (this.hasEntity(item.getId())) {
this.removeEntity(item.getId());
} else {
this.addEntity(item);
}
});
}
removeEntity(uid: string) {
runInAction(() => {
const index = this.items.findIndex((item) => item?.entity.uid === uid);
if (index < 0) {
return;
}
this.items[index] = null;
});
}
addEntity(item: CatalogEntity, cellIndex?: number) {
const uid = item.getId();
const name = item.getName();
const shortName = getShortName(item);
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 (typeof shortName !== "string") {
throw new TypeError("CatalogEntity's SHORT_NAME must be a string");
}
if (this.hasEntity(item.getId())) {
return;
}
const entity = {
uid,
name,
source: item.metadata.source,
shortName,
};
const newItem = { entity };
if (cellIndex === undefined) {
// Add item to empty cell
const emptyCellIndex = this.items.indexOf(null);
if (emptyCellIndex >= 0) {
runInAction(() => {
this.items[emptyCellIndex] = newItem;
});
} else {
broadcastMessage(hotbarTooManyItemsChannel);
}
} else if (0 <= cellIndex && cellIndex < this.items.length) {
runInAction(() => {
this.items[cellIndex] = newItem;
});
} else {
this.dependencies.logger.error(
"cannot pin entity to hotbar outside of index range",
{ entityId: uid, hotbarId: this.id, cellIndex },
);
}
}
toJSON(): HotbarData {
return {
id: this.id,
items: toJS(this.items),
name: this.name.get(),
};
}
}

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import hotbarsStateInjectable from "./state.injectable";
const hotbarsInjectable = getInjectable({
id: "hotbars",
instantiate: (di) => {
const state = di.inject(hotbarsStateInjectable);
return computed(() => [...state.values()]);
},
});
export default hotbarsInjectable;

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectionToken } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../persistent-storage/migrations.injectable"; import type { MigrationDeclaration } from "../../../../common/persistent-storage/migrations.injectable";
export const hotbarStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({ export const hotbarStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "hotbar-store-migration-token", id: "hotbar-store-migration-token",

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { action } from "mobx";
import hotbarsInjectable from "./hotbars.injectable";
export type RemoveEntityFromAllHotbars = (entityId: string) => void;
const removeEntityFromAllHotbarsInjectable = getInjectable({
id: "remove-entity-from-all-hotbars",
instantiate: (di): RemoveEntityFromAllHotbars => {
const hotbars = di.inject(hotbarsInjectable);
return action((entityId) => {
for (const hotbar of hotbars.get()) {
hotbar.removeEntity(entityId);
}
});
},
});
export default removeEntityFromAllHotbarsInjectable;

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { iter } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import { action } from "mobx";
import activeHotbarIdInjectable from "./active-id.injectable";
import type { Hotbar } from "./hotbar";
import hotbarsStateInjectable from "./state.injectable";
export type RemoveHotbar = (hotbar: Hotbar) => void;
const removeHotbarInjectable = getInjectable({
id: "remove-hotbar",
instantiate: (di): RemoveHotbar => {
const state = di.inject(hotbarsStateInjectable);
const activeHotbarId = di.inject(activeHotbarIdInjectable);
return action((hotbar) => {
assert(state.size >= 2, "Cannot remove the last hotbar");
state.delete(hotbar.id);
if (activeHotbarId.get() === hotbar.id) {
activeHotbarId.set(iter.first(state.values())?.id);
}
});
},
});
export default removeHotbarInjectable;

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { iter } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import { action } from "mobx";
import activeHotbarIdInjectable from "./active-id.injectable";
import type { Hotbar } from "./hotbar";
import hotbarsStateInjectable from "./state.injectable";
export type SetAsActiveHotbar = (desc: Hotbar | number | string) => void;
const setAsActiveHotbarInjectable = getInjectable({
id: "set-as-active-hotbar",
instantiate: (di): SetAsActiveHotbar => {
const hotbarsState = di.inject(hotbarsStateInjectable);
const activeHotbarId = di.inject(activeHotbarIdInjectable);
return action((desc) => {
if (typeof desc === "number") {
const hotbar = iter.nth(hotbarsState.values(), desc);
if (hotbar) {
activeHotbarId.set(hotbar.id);
}
} else if (typeof desc === "string") {
if (hotbarsState.has(desc)) {
activeHotbarId.set(desc);
}
} else {
if (hotbarsState.has(desc.id)) {
activeHotbarId.set(desc.id);
}
}
});
},
});
export default setAsActiveHotbarInjectable;

View File

@ -0,0 +1,14 @@
/**
* 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 { observable } from "mobx";
import type { Hotbar } from "./hotbar";
const hotbarsStateInjectable = getInjectable({
id: "hotbars-state",
instantiate: () => observable.map<string, Hotbar>(),
});
export default hotbarsStateInjectable;

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { iter } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import { action, comparer } from "mobx";
import catalogCatalogEntityInjectable from "../../../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { hotbarStoreMigrationInjectionToken } from "./migrations-token";
import { defaultHotbarCells } from "./types";
import createPersistentStorageInjectable from "../../../../common/persistent-storage/create.injectable";
import persistentStorageMigrationsInjectable from "../../../../common/persistent-storage/migrations.injectable";
import storeMigrationVersionInjectable from "../../../../common/vars/store-migration-version.injectable";
import activeHotbarIdInjectable from "./active-id.injectable";
import createHotbarInjectable from "./create-hotbar.injectable";
import type { Hotbar, HotbarData } from "./hotbar";
import hotbarsStateInjectable from "./state.injectable";
export interface HotbarStoreModel {
hotbars: HotbarData[];
activeHotbarId: string | undefined;
}
const hotbarsPersistentStorageInjectable = getInjectable({
id: "hotbars-persistent-storage",
instantiate: (di) => {
const state = di.inject(hotbarsStateInjectable);
const createPersistentStorage = di.inject(createPersistentStorageInjectable);
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const activeHotbarId = di.inject(activeHotbarIdInjectable);
const createHotbar = di.inject(createHotbarInjectable);
return createPersistentStorage<HotbarStoreModel>({
configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
projectVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(persistentStorageMigrationsInjectable, hotbarStoreMigrationInjectionToken),
fromStore: action((data) => {
if (!data.hotbars || !data.hotbars.length) {
const hotbar = createHotbar({
name: "Default",
});
const {
metadata: {
uid,
name,
source,
},
} = catalogCatalogEntity;
hotbar.items[0] = {
entity: {
uid,
name,
source,
},
};
state.replace([[hotbar.id, hotbar]]);
} else {
state.replace(data.hotbars.map((hotbar) => [hotbar.id, createHotbar(hotbar)]));
}
for (const hotbar of state.values()) {
ensureExactHotbarItemLength(hotbar);
}
if (data.activeHotbarId) {
activeHotbarId.set(data.activeHotbarId);
}
const firstHotbarId = iter.first(state.values())?.id;
if (!activeHotbarId.get()) {
activeHotbarId.set(firstHotbarId);
} else if (!iter.find(state.values(), hotbar => hotbar.id === activeHotbarId.get())) {
activeHotbarId.set(firstHotbarId);
}
}),
toJSON: () => ({
hotbars: iter.chain(state.values())
.map(hotbar => hotbar.toJSON())
.toArray(),
activeHotbarId: activeHotbarId.get(),
}),
});
},
});
export default hotbarsPersistentStorageInjectable;
/**
* 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;
}
}
}

View File

@ -0,0 +1,32 @@
/**
* 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 { action } from "mobx";
import activeHotbarIndexInjectable from "./active-hotbar-index.injectable";
import setAsActiveHotbarInjectable from "./set-as-active.injectable";
import hotbarsStateInjectable from "./state.injectable";
export type SwitchToNextHotbar = () => void;
const switchToNextHotbarInjectable = getInjectable({
id: "switch-to-next-hotbar",
instantiate: (di): SwitchToNextHotbar => {
const setAsActiveHotbar = di.inject(setAsActiveHotbarInjectable);
const activeHotbarIndex = di.inject(activeHotbarIndexInjectable);
const state = di.inject(hotbarsStateInjectable);
return action(() => {
const index = activeHotbarIndex.get() + 1;
if (index >= state.size) {
setAsActiveHotbar(0);
} else {
setAsActiveHotbar(index);
}
});
},
});
export default switchToNextHotbarInjectable;

View File

@ -0,0 +1,32 @@
/**
* 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 { action } from "mobx";
import activeHotbarIndexInjectable from "./active-hotbar-index.injectable";
import setAsActiveHotbarInjectable from "./set-as-active.injectable";
import hotbarsStateInjectable from "./state.injectable";
export type SwitchToPreviousHotbar = () => void;
const switchToPreviousHotbarInjectable = getInjectable({
id: "switch-to-previous-hotbar",
instantiate: (di): SwitchToPreviousHotbar => {
const setAsActiveHotbar = di.inject(setAsActiveHotbarInjectable);
const activeHotbarIndex = di.inject(activeHotbarIndexInjectable);
const state = di.inject(hotbarsStateInjectable);
return action(() => {
const index = activeHotbarIndex.get() - 1;
if (index < 0) {
setAsActiveHotbar(state.size - 1);
} else {
setAsActiveHotbar(index);
}
});
},
});
export default switchToPreviousHotbarInjectable;

View File

@ -0,0 +1,25 @@
/**
* 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 activeHotbarInjectable from "./active.injectable";
import type { Hotbar } from "./hotbar";
export type ActiveHotbarModel = Pick<Hotbar, "hasEntity" | "addEntity" | "removeEntity" | "toggleEntity">;
const activeHotbarModelInjectable = getInjectable({
id: "active-hotbar-model",
instantiate: (di): ActiveHotbarModel => {
const activeHotbar = di.inject(activeHotbarInjectable);
return {
hasEntity: (entityId) => activeHotbar.get()?.hasEntity(entityId) ?? false,
toggleEntity: (entity) => activeHotbar.get()?.toggleEntity(entity),
addEntity: (entity) => activeHotbar.get()?.addEntity(entity),
removeEntity: (entityId) => activeHotbar.get()?.removeEntity(entityId),
};
},
});
export default activeHotbarModelInjectable;

View File

@ -3,9 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import * as uuid from "uuid";
import type { Tuple } from "@k8slens/utilities";
import { tuple } from "@k8slens/utilities";
export interface HotbarItem { export interface HotbarItem {
entity: { entity: {
@ -18,12 +15,10 @@ export interface HotbarItem {
}; };
} }
export type Hotbar = Required<CreateHotbarData>;
export interface CreateHotbarData { export interface CreateHotbarData {
id?: string; id?: string;
name: string; name: string;
items?: Tuple<HotbarItem | null, typeof defaultHotbarCells>; items?: (HotbarItem | null)[];
} }
export interface CreateHotbarOptions { export interface CreateHotbarOptions {
@ -31,11 +26,3 @@ export interface CreateHotbarOptions {
} }
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
export function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar {
return {
id,
items: tuple.filled(defaultHotbarCells, null),
name,
};
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Cleans up a store that had the state related data stored
import catalogCatalogEntityInjectable from "../../../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationInjectionToken } from "../common/migrations-token";
import createHotbarInjectable from "../common/create-hotbar.injectable";
const v500Alpha0HotbarStoreMigrationInjectable = getInjectable({
id: "v5.0.0-alpha.0-hotbar-store-migration",
instantiate: (di) => ({
version: "5.0.0-alpha.0",
run(store) {
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const createHotbar = di.inject(createHotbarInjectable);
const hotbar = createHotbar({ name: "default" });
hotbar.addEntity(catalogCatalogEntity);
store.set("hotbars", [hotbar.toJSON()]);
},
}),
injectionToken: hotbarStoreMigrationInjectionToken,
});
export default v500Alpha0HotbarStoreMigrationInjectable;

View File

@ -4,10 +4,10 @@
*/ */
// Cleans up a store that had the state related data stored // Cleans up a store that had the state related data stored
import type { Hotbar } from "../../../common/hotbars/types";
import * as uuid from "uuid"; import * as uuid from "uuid";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationInjectionToken } from "../../../common/hotbars/migrations-token"; import { hotbarStoreMigrationInjectionToken } from "../common/migrations-token";
import type { HotbarData } from "../common/hotbar";
const v500Alpha2HotbarStoreMigrationInjectable = getInjectable({ const v500Alpha2HotbarStoreMigrationInjectable = getInjectable({
id: "v5.0.0-alpha.2-hotbar-store-migration", id: "v5.0.0-alpha.2-hotbar-store-migration",
@ -15,7 +15,7 @@ const v500Alpha2HotbarStoreMigrationInjectable = getInjectable({
version: "5.0.0-alpha.2", version: "5.0.0-alpha.2",
run(store) { run(store) {
const rawHotbars = store.get("hotbars"); const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars : []; const hotbars: HotbarData[] = Array.isArray(rawHotbars) ? rawHotbars : [];
store.set("hotbars", hotbars.map(({ id, ...rest }) => ({ store.set("hotbars", hotbars.map(({ id, ...rest }) => ({
id: id || uuid.v4(), id: id || uuid.v4(),

View File

@ -0,0 +1,168 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as uuid from "uuid";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import catalogCatalogEntityInjectable from "../../../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { isDefined, isErrnoException } from "@k8slens/utilities";
import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationInjectionToken } from "../common/migrations-token";
import readJsonSyncInjectable from "../../../../common/fs/read-json-sync.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
import { generateNewIdFor } from "../../../../common/utils/generate-new-id-for";
import type { ClusterModel } from "../../../../common/cluster-types";
import { defaultHotbarCells } from "../common/types";
import type { HotbarData } from "../common/hotbar";
import createHotbarInjectable from "../common/create-hotbar.injectable";
interface Pre500WorkspaceStoreModel {
workspaces: {
id: string;
name: string;
}[];
}
interface Pre500ClusterModel extends ClusterModel {
workspace?: string;
workspaces?: string[];
}
interface Pre500ClusterStoreModel {
clusters?: Pre500ClusterModel[];
}
const v500Beta10HotbarStoreMigrationInjectable = getInjectable({
id: "v5.0.0-beta.10-hotbar-store-migration",
instantiate: (di) => ({
version: "5.0.0-beta.10",
run(store) {
const userDataPath = di.inject(directoryForUserDataInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const readJsonSync = di.inject(readJsonSyncInjectable);
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const logger = di.inject(loggerInjectable);
const createHotbar = di.inject(createHotbarInjectable);
const rawHotbars = store.get("hotbars");
const hotbars: HotbarData[] = Array.isArray(rawHotbars) ? rawHotbars.filter(h => h && typeof h === "object") : [];
// Hotbars might be empty, if some of the previous migrations weren't run
if (hotbars.length === 0) {
const hotbar = createHotbar({ name: "default" });
hotbar.addEntity(catalogCatalogEntity);
hotbars.push(hotbar.toJSON());
}
try {
const workspaceStoreData: Pre500WorkspaceStoreModel = readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json"));
const { clusters = [] }: Pre500ClusterStoreModel = readJsonSync(joinPaths(userDataPath, "lens-cluster-store.json"));
const workspaceHotbars = new Map<string, HotbarData>(); // mapping from WorkspaceId to HotBar
for (const { id, name } of workspaceStoreData.workspaces) {
logger.info(`Creating new hotbar for ${name}`);
workspaceHotbars.set(id, {
id: uuid.v4(),
items: [],
name: `Workspace: ${name}`,
});
}
{
// grab the default named hotbar or the first.
const defaultHotbarIndex = Math.max(0, hotbars.findIndex(hotbar => hotbar.name === "default"));
const [{ name, id, items }] = hotbars.splice(defaultHotbarIndex, 1);
workspaceHotbars.set("default", {
name,
id,
items: items.filter(isDefined),
});
}
for (const cluster of clusters) {
const uid = generateNewIdFor(cluster);
for (const workspaceId of cluster.workspaces ?? [cluster.workspace].filter(isDefined)) {
const workspaceHotbar = workspaceHotbars.get(workspaceId);
if (!workspaceHotbar) {
logger.info(`Cluster ${uid} has unknown workspace ID, skipping`);
continue;
}
logger.info(`Adding cluster ${uid} to ${workspaceHotbar.name}`);
if (workspaceHotbar?.items.length < defaultHotbarCells) {
workspaceHotbar.items.push({
entity: {
uid: generateNewIdFor(cluster),
name: cluster.preferences?.clusterName || cluster.contextName,
},
});
}
}
}
for (const hotbar of workspaceHotbars.values()) {
if (hotbar.items.length === 0) {
logger.info(`Skipping ${hotbar.name} due to it being empty`);
continue;
}
while (hotbar.items.length < defaultHotbarCells) {
hotbar.items.push(null);
}
hotbars.push(hotbar);
}
/**
* Finally, make sure that the catalog entity hotbar item is in place.
* Just in case something else removed it.
*
* if every hotbar has elements that all not the `catalog-entity` item
*/
if (hotbars.every(hotbar => hotbar.items.every(item => item?.entity?.uid !== "catalog-entity"))) {
// note, we will add a new whole hotbar here called "default" if that was previously removed
const defaultHotbarIndex = hotbars.findIndex(hotbar => hotbar.name === "default");
if (defaultHotbarIndex >= 0) {
const defaultHotbar = createHotbar(hotbars[defaultHotbarIndex]);
if (defaultHotbar.isFull()) {
// making a new hotbar is less destructive if the first hotbar
// called "default" is full than overriding a hotbar item
const hotbar = createHotbar({ name: "initial" });
hotbar.addEntity(catalogCatalogEntity);
hotbars.unshift(hotbar.toJSON());
} else {
defaultHotbar.addEntity(catalogCatalogEntity);
hotbars[defaultHotbarIndex] = defaultHotbar.toJSON();
}
} else {
const hotbar = createHotbar({ name: "default" });
hotbar.addEntity(catalogCatalogEntity);
hotbars.unshift(hotbar.toJSON());
}
}
} catch (error) {
// ignore files being missing
if (isErrnoException(error) && error.code !== "ENOENT") {
throw error;
}
}
store.set("hotbars", hotbars);
},
}),
injectionToken: hotbarStoreMigrationInjectionToken,
});
export default v500Beta10HotbarStoreMigrationInjectable;

View File

@ -0,0 +1,51 @@
/**
* 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 catalogEntityRegistryInjectable from "../../../../main/catalog/entity-registry.injectable";
import type { HotbarData } from "../common/hotbar";
import { hotbarStoreMigrationInjectionToken } from "../common/migrations-token";
const v500Beta5HotbarStoreMigrationInjectable = getInjectable({
id: "v500-beta5-hotbar-store-migration",
instantiate: (di) => ({
version: "5.0.0-beta.5",
run(store) {
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const rawHotbars = store.get("hotbars");
const hotbars: HotbarData[] = Array.isArray(rawHotbars) ? rawHotbars : [];
for (const hotbar of hotbars) {
for (let i = 0; i < hotbar.items.length; i += 1) {
const item = hotbar.items[i];
if (!item) {
continue;
}
const entity = catalogEntityRegistry.findById(item.entity.uid);
if (!entity) {
// Clear disabled item
hotbar.items[i] = null;
} else {
// Save additional data
item.entity = {
...item.entity,
name: entity.metadata.name,
source: entity.metadata.source,
};
}
}
}
store.set("hotbars", hotbars);
},
}),
injectionToken: hotbarStoreMigrationInjectionToken,
});
export default v500Beta5HotbarStoreMigrationInjectable;

View File

@ -3,21 +3,21 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import { onLoadOfApplicationInjectionToken } from "@k8slens/application"; import { onLoadOfApplicationInjectionToken } from "@k8slens/application";
import setupSyncingOfGeneralCatalogEntitiesInjectable from "../../../../main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable"; import setupSyncingOfGeneralCatalogEntitiesInjectable from "../../../../main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable";
import hotbarsPersistentStorageInjectable from "../common/storage.injectable";
const initHotbarStoreInjectable = getInjectable({ const loadHotbarStorageInjectable = getInjectable({
id: "init-hotbar-store", id: "load-hotbar-storage",
instantiate: (di) => ({ instantiate: (di) => ({
run: () => { run: () => {
const hotbarStore = di.inject(hotbarStoreInjectable); const storage = di.inject(hotbarsPersistentStorageInjectable);
hotbarStore.load(); storage.loadAndStartSyncing();
}, },
runAfter: setupSyncingOfGeneralCatalogEntitiesInjectable, runAfter: setupSyncingOfGeneralCatalogEntitiesInjectable,
}), }),
injectionToken: onLoadOfApplicationInjectionToken, injectionToken: onLoadOfApplicationInjectionToken,
}); });
export default initHotbarStoreInjectable; export default loadHotbarStorageInjectable;

View File

@ -3,21 +3,21 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import { beforeFrameStartsSecondInjectionToken } from "../../../../renderer/before-frame-starts/tokens"; import { beforeFrameStartsSecondInjectionToken } from "../../../../renderer/before-frame-starts/tokens";
import initClusterStoreInjectable from "../../../cluster/storage/renderer/init.injectable"; import initClusterStoreInjectable from "../../../cluster/storage/renderer/init.injectable";
import hotbarsPersistentStorageInjectable from "../common/storage.injectable";
const initHotbarStoreInjectable = getInjectable({ const loadHotbarStorageInjectable = getInjectable({
id: "init-hotbar-store", id: "load-hotbar-storage",
instantiate: (di) => ({ instantiate: (di) => ({
run: () => { run: () => {
const hotbarStore = di.inject(hotbarStoreInjectable); const storage = di.inject(hotbarsPersistentStorageInjectable);
hotbarStore.load(); storage.loadAndStartSyncing();
}, },
runAfter: initClusterStoreInjectable, runAfter: initClusterStoreInjectable,
}), }),
injectionToken: beforeFrameStartsSecondInjectionToken, injectionToken: beforeFrameStartsSecondInjectionToken,
}); });
export default initHotbarStoreInjectable; export default loadHotbarStorageInjectable;

View File

@ -0,0 +1,373 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { anyObject } from "jest-mock-extended";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../../../common/catalog";
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
import type { DiContainer } from "@ogre-tools/injectable";
import catalogEntityRegistryInjectable from "../../../main/catalog/entity-registry.injectable";
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import hasCategoryForEntityInjectable from "../../../common/catalog/has-category-for-entity.injectable";
import catalogCatalogEntityInjectable from "../../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import type { Logger } from "../../../common/logger";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import storeMigrationVersionInjectable from "../../../common/vars/store-migration-version.injectable";
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable";
import type { SetAsActiveHotbar } from "./common/set-as-active.injectable";
import setAsActiveHotbarInjectable from "./common/set-as-active.injectable";
import hotbarsPersistentStorageInjectable from "./common/storage.injectable";
import type { Hotbar } from "./common/hotbar";
import hotbarsInjectable from "./common/hotbars.injectable";
import activeHotbarInjectable from "./common/active.injectable";
import type { AddHotbar } from "./common/add.injectable";
import type { GetHotbarById } from "./common/get-by-id.injectable";
import getHotbarByIdInjectable from "./common/get-by-id.injectable";
import addHotbarInjectable from "./common/add.injectable";
import { defaultHotbarCells } from "./common/types";
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return {
getName: jest.fn(() => data.metadata?.name),
getId: jest.fn(() => data.metadata?.uid),
getSource: jest.fn(() => data.metadata?.source ?? "unknown"),
isEnabled: jest.fn(() => data.status?.enabled ?? true),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {},
spec: {},
status: {},
...data,
} as CatalogEntity;
}
describe("Hotbars technical tests", () => {
let di: DiContainer;
let testCluster: CatalogEntity;
let minikubeCluster: CatalogEntity;
let awsCluster: CatalogEntity;
let loggerMock: jest.Mocked<Logger>;
let setAsActiveHotbar: SetAsActiveHotbar;
let hotbars: IComputedValue<Hotbar[]>;
let activeHotbar: IComputedValue<Hotbar | undefined>;
let addHotbar: AddHotbar;
let getHotbarById: GetHotbarById;
beforeEach(async () => {
di = getDiForUnitTesting();
testCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-test-id",
name: "my-test-cluster",
source: "local",
labels: {},
},
});
minikubeCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-minikube-id",
name: "my-minikube-cluster",
source: "local",
labels: {},
},
});
awsCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
metadata: {
uid: "some-aws-id",
name: "my-aws-cluster",
source: "local",
labels: {},
},
});
di.override(hasCategoryForEntityInjectable, () => () => true);
loggerMock = {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
silly: jest.fn(),
};
di.override(loggerInjectable, () => loggerMock);
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
catalogEntityRegistry.addComputedSource("some-id", computed(() => [
testCluster,
minikubeCluster,
awsCluster,
catalogCatalogEntity,
]));
setAsActiveHotbar = di.inject(setAsActiveHotbarInjectable);
hotbars = di.inject(hotbarsInjectable);
activeHotbar = di.inject(activeHotbarInjectable);
addHotbar = di.inject(addHotbarInjectable);
getHotbarById = di.inject(getHotbarByIdInjectable);
});
describe("given no previous data in store, running all migrations", () => {
beforeEach(() => {
di.override(storeMigrationVersionInjectable, () => "9999.0.0");
di.inject(hotbarsPersistentStorageInjectable).loadAndStartSyncing();
});
describe("load", () => {
it("loads one hotbar by default", () => {
expect(hotbars.get().length).toEqual(1);
});
});
describe("add", () => {
it("adds a hotbar", () => {
addHotbar({ name: "hottest" });
expect(hotbars.get().length).toEqual(2);
});
});
describe("hotbar items", () => {
it("initially creates default number of empty cells", () => {
expect(activeHotbar.get()?.items?.length).toEqual(defaultHotbarCells);
});
it("initially adds catalog entity as first item", () => {
expect(activeHotbar.get()?.items[0]?.entity.name).toEqual("Catalog");
});
it("adds items", () => {
activeHotbar.get()?.addEntity(testCluster);
const items = activeHotbar.get()?.items.filter(Boolean);
expect(items?.length).toEqual(2);
});
it("removes items", () => {
activeHotbar.get()?.addEntity(testCluster);
activeHotbar.get()?.removeEntity("some-test-id");
activeHotbar.get()?.removeEntity("catalog-entity");
const items = activeHotbar.get()?.items.filter(Boolean);
expect(items).toStrictEqual([]);
});
it("does nothing if removing with invalid uid", () => {
activeHotbar.get()?.addEntity(testCluster);
activeHotbar.get()?.removeEntity("invalid uid");
const items = activeHotbar.get()?.items.filter(Boolean);
expect(items?.length).toEqual(2);
});
it("moves item to empty cell", () => {
activeHotbar.get()?.addEntity(testCluster);
activeHotbar.get()?.addEntity(minikubeCluster);
activeHotbar.get()?.addEntity(awsCluster);
expect(activeHotbar.get()?.items[6]).toBeNull();
activeHotbar.get()?.restack(1, 5);
expect(activeHotbar.get()?.items[5]).toBeTruthy();
expect(activeHotbar.get()?.items[5]?.entity.uid).toEqual("some-test-id");
});
it("moves items down", () => {
activeHotbar.get()?.addEntity(testCluster);
activeHotbar.get()?.addEntity(minikubeCluster);
activeHotbar.get()?.addEntity(awsCluster);
// aws -> catalog
activeHotbar.get()?.restack(3, 0);
const items = activeHotbar.get()?.items.map(item => item?.entity.uid || null);
expect(items?.slice(0, 4)).toEqual(["some-aws-id", "catalog-entity", "some-test-id", "some-minikube-id"]);
});
it("moves items up", () => {
activeHotbar.get()?.addEntity(testCluster);
activeHotbar.get()?.addEntity(minikubeCluster);
activeHotbar.get()?.addEntity(awsCluster);
// test -> aws
activeHotbar.get()?.restack(1, 3);
const items = activeHotbar.get()?.items.map(item => item?.entity.uid || null);
expect(items?.slice(0, 4)).toEqual(["catalog-entity", "some-minikube-id", "some-aws-id", "some-test-id"]);
});
it("logs an error if cellIndex is out of bounds", () => {
addHotbar({ name: "hottest", id: "hottest" });
setAsActiveHotbar("hottest");
activeHotbar.get()?.addEntity(testCluster, -1);
expect(loggerMock.error).toBeCalledWith("[HOTBAR]: cannot pin entity to hotbar outside of index range", anyObject());
activeHotbar.get()?.addEntity(testCluster, 12);
expect(loggerMock.error).toBeCalledWith("[HOTBAR]: cannot pin entity to hotbar outside of index range", anyObject());
activeHotbar.get()?.addEntity(testCluster, 13);
expect(loggerMock.error).toBeCalledWith("[HOTBAR]: cannot pin entity to hotbar outside of index range", anyObject());
});
it("throws an error if getId is invalid or returns not a string", () => {
expect(() => activeHotbar.get()?.addEntity({} as any)).toThrowError(TypeError);
expect(() => activeHotbar.get()?.addEntity({ getId: () => true } as any)).toThrowError(TypeError);
});
it("throws an error if getName is invalid or returns not a string", () => {
expect(() => activeHotbar.get()?.addEntity({ getId: () => "" } as any)).toThrowError(TypeError);
expect(() => activeHotbar.get()?.addEntity({ getId: () => "", getName: () => 4 } as any)).toThrowError(TypeError);
});
it("does nothing when item moved to same cell", () => {
activeHotbar.get()?.addEntity(testCluster);
activeHotbar.get()?.restack(1, 1);
expect(activeHotbar.get()?.items[1]?.entity.uid).toEqual("some-test-id");
});
it("new items takes first empty cell", () => {
activeHotbar.get()?.addEntity(testCluster);
activeHotbar.get()?.addEntity(awsCluster);
activeHotbar.get()?.restack(0, 3);
activeHotbar.get()?.addEntity(minikubeCluster);
expect(activeHotbar.get()?.items[0]?.entity.uid).toEqual("some-minikube-id");
});
it("throws if invalid arguments provided", () => {
activeHotbar.get()?.addEntity(testCluster);
expect(() => activeHotbar.get()?.restack(-5, 0)).toThrow();
expect(() => activeHotbar.get()?.restack(2, -1)).toThrow();
expect(() => activeHotbar.get()?.restack(14, 1)).toThrow();
expect(() => activeHotbar.get()?.restack(11, 112)).toThrow();
});
it("checks if entity already pinned to hotbar", () => {
activeHotbar.get()?.addEntity(testCluster);
expect(activeHotbar.get()?.hasEntity(testCluster.getId())).toBeTruthy();
expect(activeHotbar.get()?.hasEntity(awsCluster.getId())).toBeFalsy();
});
});
});
describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => {
beforeEach(() => {
const writeJsonSync = di.inject(writeJsonSyncInjectable);
writeJsonSync("/some-directory-for-user-data/lens-hotbar-store.json", {
__internal__: {
migrations: {
version: "5.0.0-beta.3",
},
},
hotbars: [
{
id: "3caac17f-aec2-4723-9694-ad204465d935",
name: "myhotbar",
items: [
{
entity: {
uid: "some-aws-id",
},
},
{
entity: {
uid: "55b42c3c7ba3b04193416cda405269a5",
},
},
{
entity: {
uid: "176fd331968660832f62283219d7eb6e",
},
},
{
entity: {
uid: "61c4fb45528840ebad1badc25da41d14",
name: "user1-context",
source: "local",
},
},
{
entity: {
uid: "27d6f99fe9e7548a6e306760bfe19969",
name: "foo2",
source: "local",
},
},
null,
{
entity: {
uid: "c0b20040646849bb4dcf773e43a0bf27",
name: "multinode-demo",
source: "local",
},
},
null,
null,
null,
null,
null,
],
},
],
});
di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10");
di.inject(hotbarsPersistentStorageInjectable).loadAndStartSyncing();
});
it("allows to retrieve a hotbar", () => {
const hotbar = getHotbarById("3caac17f-aec2-4723-9694-ad204465d935");
expect(hotbar?.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
});
it("clears cells without entity", () => {
const items = hotbars.get()[0].items;
expect(items[2]).toBeNull();
});
it("adds extra data to cells with according entity", () => {
const items = hotbars.get()[0].items;
expect(items[0]).toEqual({
entity: {
name: "my-aws-cluster",
source: "local",
uid: "some-aws-id",
},
});
});
});
});

View File

@ -1,34 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Cleans up a store that had the state related data stored
import { getEmptyHotbar } from "../../../common/hotbars/types";
import catalogCatalogEntityInjectable from "../../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationInjectionToken } from "../../../common/hotbars/migrations-token";
const v500Alpha0HotbarStoreMigrationInjectable = getInjectable({
id: "v5.0.0-alpha.0-hotbar-store-migration",
instantiate: (di) => {
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
return {
version: "5.0.0-alpha.0",
run(store) {
const hotbar = getEmptyHotbar("default");
const { metadata: { uid, name, source }} = catalogCatalogEntity;
hotbar.items[0] = { entity: { uid, name, source }};
store.set("hotbars", [hotbar]);
},
};
},
injectionToken: hotbarStoreMigrationInjectionToken,
});
export default v500Alpha0HotbarStoreMigrationInjectable;

View File

@ -1,182 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as uuid from "uuid";
import type { Hotbar, HotbarItem } from "../../../common/hotbars/types";
import { defaultHotbarCells, getEmptyHotbar } from "../../../common/hotbars/types";
import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import catalogCatalogEntityInjectable from "../../../common/catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { isDefined, isErrnoException } from "@k8slens/utilities";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationInjectionToken } from "../../../common/hotbars/migrations-token";
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import { generateNewIdFor } from "../../../common/utils/generate-new-id-for";
import type { ClusterModel } from "../../../common/cluster-types";
interface Pre500WorkspaceStoreModel {
workspaces: {
id: string;
name: string;
}[];
}
interface PartialHotbar {
id: string;
name: string;
items: (null | HotbarItem)[];
}
interface Pre500ClusterModel extends ClusterModel {
workspace?: string;
workspaces?: string[];
}
interface Pre500ClusterStoreModel {
clusters?: Pre500ClusterModel[];
}
const v500Beta10HotbarStoreMigrationInjectable = getInjectable({
id: "v5.0.0-beta.10-hotbar-store-migration",
instantiate: (di) => {
const userDataPath = di.inject(directoryForUserDataInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const readJsonSync = di.inject(readJsonSyncInjectable);
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const logger = di.inject(loggerInjectable);
return {
version: "5.0.0-beta.10",
run(store) {
const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars.filter(h => h && typeof h === "object") : [];
// Hotbars might be empty, if some of the previous migrations weren't run
if (hotbars.length === 0) {
const hotbar = getEmptyHotbar("default");
const { metadata: { uid, name, source }} = catalogCatalogEntity;
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.push(hotbar);
}
try {
const workspaceStoreData: Pre500WorkspaceStoreModel = readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json"));
const { clusters = [] }: Pre500ClusterStoreModel = readJsonSync(joinPaths(userDataPath, "lens-cluster-store.json"));
const workspaceHotbars = new Map<string, PartialHotbar>(); // mapping from WorkspaceId to HotBar
for (const { id, name } of workspaceStoreData.workspaces) {
logger.info(`Creating new hotbar for ${name}`);
workspaceHotbars.set(id, {
id: uuid.v4(), // don't use the old IDs as they aren't necessarily UUIDs
items: [],
name: `Workspace: ${name}`,
});
}
{
// grab the default named hotbar or the first.
const defaultHotbarIndex = Math.max(0, hotbars.findIndex(hotbar => hotbar.name === "default"));
const [{ name, id, items }] = hotbars.splice(defaultHotbarIndex, 1);
workspaceHotbars.set("default", {
name,
id,
items: items.filter(isDefined),
});
}
for (const cluster of clusters) {
const uid = generateNewIdFor(cluster);
for (const workspaceId of cluster.workspaces ?? [cluster.workspace].filter(isDefined)) {
const workspaceHotbar = workspaceHotbars.get(workspaceId);
if (!workspaceHotbar) {
logger.info(`Cluster ${uid} has unknown workspace ID, skipping`);
continue;
}
logger.info(`Adding cluster ${uid} to ${workspaceHotbar.name}`);
if (workspaceHotbar?.items.length < defaultHotbarCells) {
workspaceHotbar.items.push({
entity: {
uid: generateNewIdFor(cluster),
name: cluster.preferences?.clusterName || cluster.contextName,
},
});
}
}
}
for (const hotbar of workspaceHotbars.values()) {
if (hotbar.items.length === 0) {
logger.info(`Skipping ${hotbar.name} due to it being empty`);
continue;
}
while (hotbar.items.length < defaultHotbarCells) {
hotbar.items.push(null);
}
hotbars.push(hotbar as Hotbar);
}
/**
* Finally, make sure that the catalog entity hotbar item is in place.
* Just in case something else removed it.
*
* if every hotbar has elements that all not the `catalog-entity` item
*/
if (hotbars.every(hotbar => hotbar.items.every(item => item?.entity?.uid !== "catalog-entity"))) {
// note, we will add a new whole hotbar here called "default" if that was previously removed
const di = getLegacyGlobalDiForExtensionApi();
const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable);
const defaultHotbar = hotbars.find(hotbar => hotbar.name === "default");
const { metadata: { uid, name, source }} = catalogCatalogEntity;
if (defaultHotbar) {
const freeIndex = defaultHotbar.items.findIndex(i => i === null);
if (freeIndex === -1) {
// making a new hotbar is less destructive if the first hotbar
// called "default" is full than overriding a hotbar item
const hotbar = getEmptyHotbar("initial");
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.unshift(hotbar);
} else {
defaultHotbar.items[freeIndex] = { entity: { uid, name, source }};
}
} else {
const hotbar = getEmptyHotbar("default");
hotbar.items[0] = { entity: { uid, name, source }};
hotbars.unshift(hotbar);
}
}
} catch (error) {
// ignore files being missing
if (isErrnoException(error) && error.code !== "ENOENT") {
throw error;
}
}
store.set("hotbars", hotbars);
},
};
},
injectionToken: hotbarStoreMigrationInjectionToken,
});
export default v500Beta10HotbarStoreMigrationInjectable;

View File

@ -1,54 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Hotbar } from "../../../common/hotbars/types";
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { hotbarStoreMigrationInjectionToken } from "../../../common/hotbars/migrations-token";
const v500Beta5HotbarStoreMigrationInjectable = getInjectable({
id: "v500-beta5-hotbar-store-migration",
instantiate: (di) => {
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
return {
version: "5.0.0-beta.5",
run(store) {
const rawHotbars = store.get("hotbars");
const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars : [];
for (const hotbar of hotbars) {
for (let i = 0; i < hotbar.items.length; i += 1) {
const item = hotbar.items[i];
if (!item) {
continue;
}
const entity = catalogEntityRegistry.findById(item.entity.uid);
if (!entity) {
// Clear disabled item
hotbar.items[i] = null;
} else {
// Save additional data
item.entity = {
...item.entity,
name: entity.metadata.name,
source: entity.metadata.source,
};
}
}
}
store.set("hotbars", hotbars);
},
};
},
injectionToken: hotbarStoreMigrationInjectionToken,
});
export default v500Beta5HotbarStoreMigrationInjectable;

View File

@ -11,7 +11,6 @@ import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns"; import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns";
import type { CategoryColumns, GetCategoryColumnsParams } from "../columns/get.injectable"; import type { CategoryColumns, GetCategoryColumnsParams } from "../columns/get.injectable";
import getCategoryColumnsInjectable from "../columns/get.injectable"; import getCategoryColumnsInjectable from "../columns/get.injectable";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import extensionInjectable from "../../../../extensions/extension-loader/extension/extension.injectable"; import extensionInjectable from "../../../../extensions/extension-loader/extension/extension.injectable";
import currentlyInClusterFrameInjectable from "../../../routes/currently-in-cluster-frame.injectable"; import currentlyInClusterFrameInjectable from "../../../routes/currently-in-cluster-frame.injectable";
@ -46,7 +45,6 @@ describe("Custom Category Columns", () => {
beforeEach(() => { beforeEach(() => {
di = getDiForUnitTesting(); di = getDiForUnitTesting();
di.override(hotbarStoreInjectable, () => ({}));
di.override(currentlyInClusterFrameInjectable, () => false); di.override(currentlyInClusterFrameInjectable, () => false);
getCategoryColumns = di.inject(getCategoryColumnsInjectable); getCategoryColumns = di.inject(getCategoryColumnsInjectable);

View File

@ -28,8 +28,6 @@ import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entit
import visitEntityContextMenuInjectable from "../../../common/catalog/visit-entity-context-menu.injectable"; import visitEntityContextMenuInjectable from "../../../common/catalog/visit-entity-context-menu.injectable";
import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import type { HotbarStore } from "../../../common/hotbars/store";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { Logger } from "../../../common/logger"; import type { Logger } from "../../../common/logger";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable"; import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable";
@ -51,6 +49,8 @@ import type { OnCatalogEntityListClick } from "./entity-details/on-catalog-click
import onCatalogEntityListClickInjectable from "./entity-details/on-catalog-click.injectable"; import onCatalogEntityListClickInjectable from "./entity-details/on-catalog-click.injectable";
import type { ShowEntityDetails } from "./entity-details/show.injectable"; import type { ShowEntityDetails } from "./entity-details/show.injectable";
import showEntityDetailsInjectable from "./entity-details/show.injectable"; import showEntityDetailsInjectable from "./entity-details/show.injectable";
import type { Hotbar } from "../../../features/hotbar/storage/common/hotbar";
import activeHotbarInjectable from "../../../features/hotbar/storage/common/active.injectable";
interface Dependencies { interface Dependencies {
catalogPreviousActiveTabStorage: StorageLayer<string | null>; catalogPreviousActiveTabStorage: StorageLayer<string | null>;
@ -65,13 +65,13 @@ interface Dependencies {
kind: IComputedValue<string>; kind: IComputedValue<string>;
}; };
navigateToCatalog: NavigateToCatalog; navigateToCatalog: NavigateToCatalog;
hotbarStore: HotbarStore;
catalogCategoryRegistry: CatalogCategoryRegistry; catalogCategoryRegistry: CatalogCategoryRegistry;
visitEntityContextMenu: VisitEntityContextMenu; visitEntityContextMenu: VisitEntityContextMenu;
navigate: Navigate; navigate: Navigate;
normalizeMenuItem: NormalizeCatalogEntityContextMenu; normalizeMenuItem: NormalizeCatalogEntityContextMenu;
showErrorNotification: ShowNotification; showErrorNotification: ShowNotification;
logger: Logger; logger: Logger;
activeHotbar: IComputedValue<Hotbar | undefined>;
} }
@observer @observer
@ -156,11 +156,11 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
} }
addToHotbar(entity: CatalogEntity): void { addToHotbar(entity: CatalogEntity): void {
this.props.hotbarStore.addToHotbar(entity); this.props.activeHotbar.get()?.addEntity(entity);
} }
removeFromHotbar(entity: CatalogEntity): void { removeFromHotbar(entity: CatalogEntity): void {
this.props.hotbarStore.removeFromHotbar(entity.getId()); this.props.activeHotbar.get()?.removeEntity(entity.getId());
} }
onTabChange = action((tabId: string | null) => { onTabChange = action((tabId: string | null) => {
@ -323,7 +323,7 @@ export const Catalog = withInjectables<Dependencies>(NonInjectedCatalog, {
routeParameters: di.inject(catalogRouteParametersInjectable), routeParameters: di.inject(catalogRouteParametersInjectable),
navigateToCatalog: di.inject(navigateToCatalogInjectable), navigateToCatalog: di.inject(navigateToCatalogInjectable),
emitEvent: di.inject(emitAppEventInjectable), emitEvent: di.inject(emitAppEventInjectable),
hotbarStore: di.inject(hotbarStoreInjectable), activeHotbar: di.inject(activeHotbarInjectable),
catalogCategoryRegistry: di.inject(catalogCategoryRegistryInjectable), catalogCategoryRegistry: di.inject(catalogCategoryRegistryInjectable),
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable), visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
navigate: di.inject(navigateInjectable), navigate: di.inject(navigateInjectable),

View File

@ -0,0 +1,27 @@
/**
* 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 { RegisteredAdditionalCategoryColumn } from "../custom-category-columns";
import renderNamedCategoryColumnCellInjectable from "./render-named-category-column-cell.injectable";
const namedCategoryColumnInjectable = getInjectable({
id: "name-category-column",
instantiate: (di): RegisteredAdditionalCategoryColumn => ({
id: "name",
priority: 0,
renderCell: di.inject(renderNamedCategoryColumnCellInjectable),
titleProps: {
title: "Name",
className: styles.entityName,
id: "name",
sortBy: "name",
},
searchFilter: (entity) => entity.getName(),
sortCallback: (entity) => `name=${entity.getName()}`,
}),
});
export default namedCategoryColumnInjectable;

View File

@ -1,65 +0,0 @@
/**
* 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 "@k8slens/utilities";
import { Avatar } from "../../avatar";
import { Icon } from "../../icon";
import React from "react";
import type { RegisteredAdditionalCategoryColumn } from "../custom-category-columns";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import type { HotbarStore } from "../../../../common/hotbars/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}
svg={isItemInHotbar ? "push_off" : "push_pin"}
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
onClick={onClick}
/>
</>
);
};
const namedCategoryColumnInjectable = 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 namedCategoryColumnInjectable;

View File

@ -0,0 +1,59 @@
/**
* 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 React from "react";
import activeHotbarInjectable from "../../../../features/hotbar/storage/common/active.injectable";
import { Avatar } from "../../avatar";
import type { RegisteredAdditionalCategoryColumn } from "../custom-category-columns";
import { Icon } from "../../icon";
import { prevDefault } from "@k8slens/utilities";
const renderNamedCategoryColumnCellInjectable = getInjectable({
id: "render-named-category-column-cell",
instantiate: (di): RegisteredAdditionalCategoryColumn["renderCell"] => {
const activeHotbar = di.inject(activeHotbarInjectable);
return (entity) => {
const hotbar = activeHotbar.get();
if (!hotbar) {
return null;
}
const isItemInHotbar = hotbar.hasEntity(entity.getId());
const onClick = prevDefault((
isItemInHotbar
? () => hotbar.removeEntity(entity.getId())
: () => hotbar.addEntity(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}
svg={isItemInHotbar ? "push_off" : "push_pin"}
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
onClick={onClick}
/>
</>
);
};
},
});
export default renderNamedCategoryColumnCellInjectable;

View File

@ -9,11 +9,12 @@ import { MenuItem } from "../menu";
import type { CatalogEntity } from "../../api/catalog-entity"; import type { CatalogEntity } from "../../api/catalog-entity";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable"; import type { IComputedValue } from "mobx";
import type { HotbarStore } from "../../../common/hotbars/store"; import type { Hotbar } from "../../../features/hotbar/storage/common/hotbar";
import activeHotbarInjectable from "../../../features/hotbar/storage/common/active.injectable";
interface Dependencies { interface Dependencies {
hotbarStore: HotbarStore; activeHotbar: IComputedValue<Hotbar | undefined>;
} }
interface HotbarToggleMenuItemProps { interface HotbarToggleMenuItemProps {
@ -25,19 +26,19 @@ interface HotbarToggleMenuItemProps {
function NonInjectedHotbarToggleMenuItem({ function NonInjectedHotbarToggleMenuItem({
addContent, addContent,
entity, entity,
hotbarStore, activeHotbar,
removeContent, removeContent,
}: Dependencies & HotbarToggleMenuItemProps) { }: Dependencies & HotbarToggleMenuItemProps) {
const [itemInHotbar, setItemInHotbar] = useState(hotbarStore.isAddedToActive(entity)); const [itemInHotbar, setItemInHotbar] = useState(activeHotbar.get()?.hasEntity(entity.getId()) ?? false);
return ( return (
<MenuItem <MenuItem
onClick={() => { onClick={() => {
if (itemInHotbar) { if (itemInHotbar) {
hotbarStore.removeFromHotbar(entity.getId()); activeHotbar.get()?.removeEntity(entity.getId());
setItemInHotbar(false); setItemInHotbar(false);
} else { } else {
hotbarStore.addToHotbar(entity); activeHotbar.get()?.addEntity(entity);
setItemInHotbar(true); setItemInHotbar(true);
} }
}} }}
@ -47,14 +48,10 @@ function NonInjectedHotbarToggleMenuItem({
); );
} }
export const HotbarToggleMenuItem = withInjectables<Dependencies, HotbarToggleMenuItemProps>( export const HotbarToggleMenuItem = withInjectables<Dependencies, HotbarToggleMenuItemProps>(NonInjectedHotbarToggleMenuItem, {
NonInjectedHotbarToggleMenuItem, getProps: (di, props) => ({
...props,
{ activeHotbar: di.inject(activeHotbarInjectable),
getProps: (di, props) => ({ }),
hotbarStore: di.inject(hotbarStoreInjectable), });
...props,
}),
},
);

View File

@ -15,9 +15,7 @@ import { Dialog } from "../dialog";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Select } from "../select"; import { Select } from "../select";
import { Checkbox } from "../checkbox"; import { Checkbox } from "../checkbox";
import type { HotbarStore } from "../../../common/hotbars/store";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { DeleteClusterDialogState } from "./state.injectable"; import type { DeleteClusterDialogState } from "./state.injectable";
import deleteClusterDialogStateInjectable from "./state.injectable"; import deleteClusterDialogStateInjectable from "./state.injectable";
import type { RequestSetClusterAsDeleting } from "../../../features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable"; import type { RequestSetClusterAsDeleting } from "../../../features/cluster/delete-dialog/renderer/request-set-as-deleting.injectable";
@ -32,16 +30,18 @@ import showErrorNotificationInjectable from "../notifications/show-error-notific
import { isCurrentContext } from "./is-current-context"; import { isCurrentContext } from "./is-current-context";
import type { IsInLocalKubeconfig } from "./is-in-local-kubeconfig.injectable"; import type { IsInLocalKubeconfig } from "./is-in-local-kubeconfig.injectable";
import isInLocalKubeconfigInjectable from "./is-in-local-kubeconfig.injectable"; import isInLocalKubeconfigInjectable from "./is-in-local-kubeconfig.injectable";
import type { RemoveEntityFromAllHotbars } from "../../../features/hotbar/storage/common/remove-entity-from-all.injectable";
import removeEntityFromAllHotbarsInjectable from "../../../features/hotbar/storage/common/remove-entity-from-all.injectable";
interface Dependencies { interface Dependencies {
state: IObservableValue<DeleteClusterDialogState | undefined>; state: IObservableValue<DeleteClusterDialogState | undefined>;
hotbarStore: HotbarStore;
requestSetClusterAsDeleting: RequestSetClusterAsDeleting; requestSetClusterAsDeleting: RequestSetClusterAsDeleting;
requestDeleteCluster: RequestDeleteCluster; requestDeleteCluster: RequestDeleteCluster;
requestClearClusterAsDeleting: RequestClearClusterAsDeleting; requestClearClusterAsDeleting: RequestClearClusterAsDeleting;
showErrorNotification: ShowNotification; showErrorNotification: ShowNotification;
saveKubeconfig: SaveKubeconfig; saveKubeconfig: SaveKubeconfig;
isInLocalKubeconfig: IsInLocalKubeconfig; isInLocalKubeconfig: IsInLocalKubeconfig;
removeEntityFromAllHotbars: RemoveEntityFromAllHotbars;
} }
@observer @observer
@ -65,7 +65,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
try { try {
await this.props.saveKubeconfig(config, cluster.kubeConfigPath.get()); await this.props.saveKubeconfig(config, cluster.kubeConfigPath.get());
this.props.hotbarStore.removeAllHotbarItems(cluster.id); this.props.removeEntityFromAllHotbars(cluster.id);
await this.props.requestDeleteCluster(cluster.id); await this.props.requestDeleteCluster(cluster.id);
} catch(error) { } catch(error) {
this.props.showErrorNotification(`Cannot remove cluster, failed to process config file. ${error}`); this.props.showErrorNotification(`Cannot remove cluster, failed to process config file. ${error}`);
@ -267,7 +267,6 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
export const DeleteClusterDialog = withInjectables<Dependencies>(NonInjectedDeleteClusterDialog, { export const DeleteClusterDialog = withInjectables<Dependencies>(NonInjectedDeleteClusterDialog, {
getProps: (di) => ({ getProps: (di) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
state: di.inject(deleteClusterDialogStateInjectable), state: di.inject(deleteClusterDialogStateInjectable),
requestSetClusterAsDeleting: di.inject(requestSetClusterAsDeletingInjectable), requestSetClusterAsDeleting: di.inject(requestSetClusterAsDeletingInjectable),
requestClearClusterAsDeleting: di.inject(requestClearClusterAsDeletingInjectable), requestClearClusterAsDeleting: di.inject(requestClearClusterAsDeletingInjectable),
@ -275,5 +274,6 @@ export const DeleteClusterDialog = withInjectables<Dependencies>(NonInjectedDele
saveKubeconfig: di.inject(saveKubeconfigInjectable), saveKubeconfig: di.inject(saveKubeconfigInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable), showErrorNotification: di.inject(showErrorNotificationInjectable),
isInLocalKubeconfig: di.inject(isInLocalKubeconfigInjectable), isInLocalKubeconfig: di.inject(isInLocalKubeconfigInjectable),
removeEntityFromAllHotbars: di.inject(removeEntityFromAllHotbarsInjectable),
}), }),
}); });

View File

@ -0,0 +1,97 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<HotbarRemoveCommand /> renders w/o errors 1`] = `
<div>
<div
class="Select theme-dark css-b62m3t-container"
>
<span
class="css-1f43avz-a11yText-A11yText"
id="react-select-2-live-region"
>
<span
id="aria-selection"
>
option , selected.
</span>
<span
id="aria-context"
>
2 results available. Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu.
</span>
</span>
<span
aria-atomic="false"
aria-live="polite"
aria-relevant="additions text"
class="css-1f43avz-a11yText-A11yText"
/>
<div
class="Select__control Select__control--is-focused Select__control--menu-is-open css-t3ipsp-control"
>
<div
class="Select__value-container css-1fdsijx-ValueContainer"
>
<div
class="Select__placeholder css-1jqq78o-placeholder"
id="react-select-2-placeholder"
>
Remove hotbar
</div>
<div
class="Select__input-container css-qbdosj-Input"
data-value=""
>
<input
aria-autocomplete="list"
aria-controls="react-select-2-listbox"
aria-describedby="react-select-2-placeholder"
aria-expanded="true"
aria-haspopup="true"
aria-owns="react-select-2-listbox"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="Select__input"
id="react-select-2-input"
role="combobox"
spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
tabindex="0"
type="text"
value=""
/>
</div>
</div>
<div
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
/>
</div>
<div
class="theme-dark Select__menu css-1nmdiq5-menu"
id="react-select-2-listbox"
>
<div
class="Select__menu-list css-1n6sfyn-MenuList"
>
<div
aria-disabled="false"
class="Select__option css-10wo9uf-option"
id="react-select-2-option-0"
tabindex="-1"
>
1: default
</div>
<div
aria-disabled="false"
class="Select__option css-10wo9uf-option"
id="react-select-2-option-1"
tabindex="-1"
>
2: non-default
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -5,75 +5,56 @@
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { HotbarRemoveCommand } from "../hotbar-remove-command"; import { HotbarRemoveCommand } from "../hotbar-remove-command";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react"; import { fireEvent } from "@testing-library/react";
import React from "react"; import React from "react";
import type { DiContainer } from "@ogre-tools/injectable";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import { ConfirmDialog } from "../../confirm-dialog"; import { ConfirmDialog } from "../../confirm-dialog";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { HotbarStore } from "../../../../common/hotbars/store"; import hotbarsStateInjectable from "../../../../features/hotbar/storage/common/state.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable"; import type { CreateHotbar } from "../../../../features/hotbar/storage/common/create-hotbar.injectable";
import createHotbarInjectable from "../../../../features/hotbar/storage/common/create-hotbar.injectable";
const mockHotbars: Partial<Record<string, any>> = { import type { IComputedValue } from "mobx";
"1": { import type { Hotbar } from "../../../../features/hotbar/storage/common/hotbar";
id: "1", import hotbarsInjectable from "../../../../features/hotbar/storage/common/hotbars.injectable";
name: "Default",
items: [],
},
};
describe("<HotbarRemoveCommand />", () => { describe("<HotbarRemoveCommand />", () => {
let di: DiContainer; let result: RenderResult;
let render: DiRender; let createHotbar: CreateHotbar;
let hotbars: IComputedValue<Hotbar[]>;
beforeEach(() => { beforeEach(() => {
di = getDiForUnitTesting(); const di = getDiForUnitTesting();
const render = renderFor(di);
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
render = renderFor(di); createHotbar = di.inject(createHotbarInjectable);
}); hotbars = di.inject(hotbarsInjectable);
it("renders w/o errors", () => { const hotbarsState = di.inject(hotbarsStateInjectable);
di.override(hotbarStoreInjectable, () => ({ const defaultHotbar = createHotbar({ name: "default" });
hotbars: [mockHotbars["1"]], const nonDefaultHotbar = createHotbar({ name: "non-default" });
getById: (id: string) => mockHotbars[id],
remove: () => {
},
hotbarIndex: () => 0,
getDisplayLabel: () => "1: Default",
}) as unknown as HotbarStore);
const { container } = render(<HotbarRemoveCommand />); hotbarsState.set(defaultHotbar.id, defaultHotbar);
hotbarsState.set(nonDefaultHotbar.id, nonDefaultHotbar);
expect(container).toBeInstanceOf(HTMLElement); result = render((
});
it("calls remove if you click on the entry", () => {
const removeMock = jest.fn();
di.override(hotbarStoreInjectable, () => ({
hotbars: [mockHotbars["1"]],
getById: (id: string) => mockHotbars[id],
remove: removeMock,
hotbarIndex: () => 0,
getDisplayLabel: () => "1: Default",
}) as unknown as HotbarStore);
const { getByText } = render(
<> <>
<HotbarRemoveCommand /> <HotbarRemoveCommand />
<ConfirmDialog /> <ConfirmDialog />
</>, </>
); ));
});
fireEvent.click(getByText("1: Default")); it("renders w/o errors", () => {
fireEvent.click(getByText("Remove Hotbar")); expect(result.container).toMatchSnapshot();
});
expect(removeMock).toHaveBeenCalled(); it("calls remove if you click on the entry", () => {
fireEvent.click(result.getByText("1: default"));
fireEvent.click(result.getByText("Remove Hotbar"));
expect(hotbars.get().length).toBe(1);
}); });
}); });

View File

@ -7,15 +7,15 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { InputValidator } from "../input"; import type { InputValidator } from "../input";
import { Input } from "../input"; import { Input } from "../input";
import type { CreateHotbarData, CreateHotbarOptions } from "../../../common/hotbars/types";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable"; import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable";
import addHotbarInjectable from "../../../common/hotbars/add-hotbar.injectable"; import type { AddHotbar } from "../../../features/hotbar/storage/common/add.injectable";
import addHotbarInjectable from "../../../features/hotbar/storage/common/add.injectable";
interface Dependencies { interface Dependencies {
closeCommandOverlay: () => void; closeCommandOverlay: () => void;
addHotbar: (data: CreateHotbarData, opts: CreateHotbarOptions) => void; addHotbar: AddHotbar;
uniqueHotbarName: InputValidator<boolean>; uniqueHotbarName: InputValidator<boolean>;
} }

View File

@ -5,7 +5,7 @@
import "./hotbar-menu.scss"; import "./hotbar-menu.scss";
import React from "react"; import React, { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { HotbarEntityIcon } from "./hotbar-entity-icon"; import { HotbarEntityIcon } from "./hotbar-entity-icon";
import type { IClassName } from "@k8slens/utilities"; import type { IClassName } from "@k8slens/utilities";
@ -16,57 +16,46 @@ import { DragDropContext, Draggable, Droppable, type DropResult } from "react-be
import { HotbarSelector } from "./hotbar-selector"; import { HotbarSelector } from "./hotbar-selector";
import { HotbarCell } from "./hotbar-cell"; import { HotbarCell } from "./hotbar-cell";
import { HotbarIcon } from "./hotbar-icon"; import { HotbarIcon } from "./hotbar-icon";
import type { HotbarItem } from "../../../common/hotbars/types"; import type { HotbarItem } from "../../../features/hotbar/storage/common/types";
import { defaultHotbarCells } from "../../../common/hotbars/types"; import { defaultHotbarCells } from "../../../features/hotbar/storage/common/types";
import { action, makeObservable, observable } from "mobx"; import type { IComputedValue } from "mobx";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import type { HotbarStore } from "../../../common/hotbars/store";
import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable"; import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable";
import type { Hotbar } from "../../../features/hotbar/storage/common/hotbar";
import activeHotbarInjectable from "../../../features/hotbar/storage/common/active.injectable";
export interface HotbarMenuProps { export interface HotbarMenuProps {
className?: IClassName; className?: IClassName;
} }
interface Dependencies { interface Dependencies {
hotbarStore: HotbarStore; activeHotbar: IComputedValue<Hotbar | undefined>;
entityRegistry: CatalogEntityRegistry; entityRegistry: CatalogEntityRegistry;
} }
@observer const NonInjectedHotbarMenu = observer((props: Dependencies & HotbarMenuProps) => {
class NonInjectedHotbarMenu extends React.Component<Dependencies & HotbarMenuProps> { const {
@observable draggingOver = false; activeHotbar,
entityRegistry,
className,
} = props;
constructor(props: Dependencies & HotbarMenuProps) { const [draggingOver, setDraggingOver] = useState(false);
super(props); const hotbar = activeHotbar.get();
makeObservable(this);
}
get hotbar() { const getEntity = (item: HotbarItem | null) => {
return this.props.hotbarStore.getActive(); if (!item) {
} return undefined;
getEntity(item: HotbarItem | null) {
const hotbar = this.props.hotbarStore.getActive();
if (!hotbar || !item) {
return null;
} }
return this.props.entityRegistry.getById(item.entity.uid) ?? null; return entityRegistry.getById(item.entity.uid);
} };
const onDragStart = () => setDraggingOver(true);
const onDragEnd = (result: DropResult) => {
setDraggingOver(false);
@action
onDragStart() {
this.draggingOver = true;
}
@action
onDragEnd(result: DropResult) {
const { source, destination } = result; const { source, destination } = result;
this.draggingOver = false;
if (!destination) { // Dropped outside of the list if (!destination) { // Dropped outside of the list
return; return;
} }
@ -74,135 +63,115 @@ class NonInjectedHotbarMenu extends React.Component<Dependencies & HotbarMenuPro
const from = parseInt(source.droppableId); const from = parseInt(source.droppableId);
const to = parseInt(destination.droppableId); const to = parseInt(destination.droppableId);
this.props.hotbarStore.restackItems(from, to); hotbar?.restack(from, to);
}
removeItem = (uid: string) => {
const hotbar = this.props.hotbarStore;
hotbar.removeFromHotbar(uid);
}; };
const removeItem = (entityId: string) => {
addItem = (entity: CatalogEntity, index = -1) => { hotbar?.removeEntity(entityId);
const hotbar = this.props.hotbarStore;
hotbar.addToHotbar(entity, index);
}; };
const addItem = (entity: CatalogEntity) => {
getMoveAwayDirection(entityId: string | undefined | null, cellIndex: number) { hotbar?.addEntity(entity);
if (!entityId) { };
const getMoveAwayDirection = (entityId: string | undefined | null, cellIndex: number) => {
if (!entityId || !hotbar) {
return "animateDown"; return "animateDown";
} }
const draggableItemIndex = this.hotbar.items.findIndex(item => item?.entity.uid == entityId); const draggableItemIndex = hotbar.items.findIndex(item => item?.entity.uid == entityId);
return draggableItemIndex > cellIndex ? "animateDown" : "animateUp"; return draggableItemIndex > cellIndex ? "animateDown" : "animateUp";
} };
renderGrid() { const renderGrid = () => hotbar?.items.map((item, index) => {
return this.hotbar.items.map((item, index) => { const entity = getEntity(item);
const entity = this.getEntity(item);
return (
<Droppable droppableId={`${index}`} key={index}>
{(provided, snapshot) => (
<HotbarCell
index={index}
key={entity ? entity.getId() : `cell${index}`}
innerRef={provided.innerRef}
className={cssNames(
{
isDraggingOver: snapshot.isDraggingOver,
isDraggingOwner: snapshot.draggingOverWith == entity?.getId(),
},
this.getMoveAwayDirection(snapshot.draggingOverWith, index),
)}
{...provided.droppableProps}
>
{item && (
<Draggable
draggableId={item.entity.uid}
key={item.entity.uid}
index={0}
>
{(provided, snapshot) => {
const style = {
zIndex: defaultHotbarCells - index,
position: "absolute",
...provided.draggableProps.style,
} as React.CSSProperties;
return (
<div
key={item.entity.uid}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={style}
>
{entity ? (
<HotbarEntityIcon
key={index}
index={index}
entity={entity}
onClick={() => this.props.entityRegistry.onRun(entity)}
className={cssNames({ isDragging: snapshot.isDragging })}
remove={this.removeItem}
add={this.addItem}
size={40}
/>
) : (
<HotbarIcon
uid={`hotbar-icon-${item.entity.uid}`}
title={item.entity.name}
source={item.entity.source ?? "local"}
tooltip={`${item.entity.name} (${item.entity.source})`}
menuItems={[
{
title: "Remove from Hotbar",
onClick: () => this.removeItem(item.entity.uid),
},
]}
disabled
size={40}
/>
)}
</div>
);
}}
</Draggable>
)}
{provided.placeholder}
</HotbarCell>
)}
</Droppable>
);
});
}
render() {
const { className, hotbarStore } = this.props;
const hotbar = hotbarStore.getActive();
return ( return (
<div className={cssNames("HotbarMenu flex column", { draggingOver: this.draggingOver }, className)}> <Droppable droppableId={`${index}`} key={index}>
<div className="HotbarItems flex column gaps"> {(provided, snapshot) => (
<DragDropContext <HotbarCell
onDragStart={() => this.onDragStart()} index={index}
onDragEnd={(result) => this.onDragEnd(result)}> key={entity ? entity.getId() : `cell${index}`}
{this.renderGrid()} innerRef={provided.innerRef}
</DragDropContext> className={cssNames(
</div> {
<HotbarSelector hotbar={hotbar} /> isDraggingOver: snapshot.isDraggingOver,
</div> isDraggingOwner: snapshot.draggingOverWith == entity?.getId(),
},
getMoveAwayDirection(snapshot.draggingOverWith, index),
)}
{...provided.droppableProps}
>
{item && (
<Draggable
draggableId={item.entity.uid}
key={item.entity.uid}
index={0}
>
{(provided, snapshot) => (
<div
key={item.entity.uid}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
zIndex: defaultHotbarCells - index,
position: "absolute" as const,
...provided.draggableProps.style,
}}
>
{entity ? (
<HotbarEntityIcon
key={index}
index={index}
entity={entity}
onClick={() => entityRegistry.onRun(entity)}
className={cssNames({ isDragging: snapshot.isDragging })}
remove={removeItem}
add={addItem}
size={40} />
) : (
<HotbarIcon
uid={`hotbar-icon-${item.entity.uid}`}
title={item.entity.name}
source={item.entity.source ?? "local"}
tooltip={`${item.entity.name} (${item.entity.source})`}
menuItems={[
{
title: "Remove from Hotbar",
onClick: () => removeItem(item.entity.uid),
},
]}
disabled
size={40} />
)}
</div>
)}
</Draggable>
)}
{provided.placeholder}
</HotbarCell>
)}
</Droppable>
); );
} });
}
return (
<div className={cssNames("HotbarMenu flex column", { draggingOver }, className)}>
<div className="HotbarItems flex column gaps">
<DragDropContext
onDragStart={() => onDragStart()}
onDragEnd={(result) => onDragEnd(result)}>
{renderGrid()}
</DragDropContext>
</div>
<HotbarSelector />
</div>
);
});
export const HotbarMenu = withInjectables<Dependencies, HotbarMenuProps>(NonInjectedHotbarMenu, { export const HotbarMenu = withInjectables<Dependencies, HotbarMenuProps>(NonInjectedHotbarMenu, {
getProps: (di, props) => ({ getProps: (di, props) => ({
...props, ...props,
hotbarStore: di.inject(hotbarStoreInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable), entityRegistry: di.inject(catalogEntityRegistryInjectable),
activeHotbar: di.inject(activeHotbarInjectable),
}), }),
}); });

View File

@ -6,23 +6,32 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Select } from "../select"; import { Select } from "../select";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import type { HotbarStore } from "../../../common/hotbars/store";
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable"; import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable"; import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
import type { IComputedValue } from "mobx";
import type { Hotbar } from "../../../features/hotbar/storage/common/hotbar";
import type { ComputeHotbarDisplayLabel } from "../../../features/hotbar/storage/common/compute-display-label.injectable";
import computeHotbarDisplayLabelInjectable from "../../../features/hotbar/storage/common/compute-display-label.injectable";
import hotbarsInjectable from "../../../features/hotbar/storage/common/hotbars.injectable";
import type { RemoveHotbar } from "../../../features/hotbar/storage/common/remove.injectable";
import removeHotbarInjectable from "../../../features/hotbar/storage/common/remove.injectable";
interface Dependencies { interface Dependencies {
closeCommandOverlay: () => void; closeCommandOverlay: () => void;
openConfirmDialog: OpenConfirmDialog; openConfirmDialog: OpenConfirmDialog;
hotbarStore: HotbarStore; hotbars: IComputedValue<Hotbar[]>;
computeHotbarDisplayLabel: ComputeHotbarDisplayLabel;
removeHotbar: RemoveHotbar;
} }
const NonInjectedHotbarRemoveCommand = observer(({ const NonInjectedHotbarRemoveCommand = observer(({
closeCommandOverlay, closeCommandOverlay,
hotbarStore,
openConfirmDialog, openConfirmDialog,
hotbars,
computeHotbarDisplayLabel,
removeHotbar,
}: Dependencies) => ( }: Dependencies) => (
<Select <Select
menuPortalTarget={null} menuPortalTarget={null}
@ -38,28 +47,28 @@ const NonInjectedHotbarRemoveCommand = observer(({
primary: false, primary: false,
accent: true, accent: true,
}, },
ok: () => hotbarStore.remove(option.value), ok: () => removeHotbar(option.value),
message: ( message: (
<div className="confirm flex column gaps"> <div className="confirm flex column gaps">
<p> <p>
Are you sure you want remove hotbar Are you sure you want remove hotbar
{" "} {" "}
<b> <b>
{option.value.name} {option.value.name.get()}
</b> </b>
? ?
</p> </p>
</div> </div>
), ),
}); });
} } }}
components={{ DropdownIndicator: null, IndicatorSeparator: null }} components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true} menuIsOpen={true}
options={( options={(
hotbarStore.hotbars hotbars.get()
.map(hotbar => ({ .map(hotbar => ({
value: hotbar, value: hotbar,
label: hotbarStore.getDisplayLabel(hotbar), label: computeHotbarDisplayLabel(hotbar),
})) }))
)} )}
autoFocus={true} autoFocus={true}
@ -72,7 +81,9 @@ export const HotbarRemoveCommand = withInjectables<Dependencies>(NonInjectedHotb
getProps: (di, props) => ({ getProps: (di, props) => ({
...props, ...props,
closeCommandOverlay: di.inject(commandOverlayInjectable).close, closeCommandOverlay: di.inject(commandOverlayInjectable).close,
hotbarStore: di.inject(hotbarStoreInjectable),
openConfirmDialog: di.inject(openConfirmDialogInjectable), openConfirmDialog: di.inject(openConfirmDialogInjectable),
computeHotbarDisplayLabel: di.inject(computeHotbarDisplayLabelInjectable),
hotbars: di.inject(hotbarsInjectable),
removeHotbar: di.inject(removeHotbarInjectable),
}), }),
}); });

View File

@ -6,36 +6,46 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Select } from "../select"; import { Select } from "../select";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { InputValidator } from "../input"; import type { InputValidator } from "../input";
import { Input } from "../input"; import { Input } from "../input";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable"; import uniqueHotbarNameInjectable from "../input/validators/unique-hotbar-name.injectable";
import type { HotbarStore } from "../../../common/hotbars/store"; import type { IComputedValue } from "mobx";
import { action } from "mobx";
import type { Hotbar } from "../../../features/hotbar/storage/common/hotbar";
import type { GetHotbarById } from "../../../features/hotbar/storage/common/get-by-id.injectable";
import getHotbarByIdInjectable from "../../../features/hotbar/storage/common/get-by-id.injectable";
import hotbarsInjectable from "../../../features/hotbar/storage/common/hotbars.injectable";
import type { ComputeHotbarDisplayLabel } from "../../../features/hotbar/storage/common/compute-display-label.injectable";
import computeHotbarDisplayLabelInjectable from "../../../features/hotbar/storage/common/compute-display-label.injectable";
interface Dependencies { interface Dependencies {
closeCommandOverlay: () => void; closeCommandOverlay: () => void;
hotbarStore: HotbarStore; getHotbarById: GetHotbarById;
computeHotbarDisplayLabel: ComputeHotbarDisplayLabel;
uniqueHotbarName: InputValidator<false>; uniqueHotbarName: InputValidator<false>;
hotbars: IComputedValue<Hotbar[]>;
} }
const NonInjectedHotbarRenameCommand = observer(({ const NonInjectedHotbarRenameCommand = observer(({
closeCommandOverlay, closeCommandOverlay,
hotbarStore, getHotbarById,
computeHotbarDisplayLabel,
uniqueHotbarName, uniqueHotbarName,
hotbars,
}: Dependencies) => { }: Dependencies) => {
const [hotbarId, setHotbarId] = useState(""); const [hotbarId, setHotbarId] = useState("");
const [hotbarName, setHotbarName] = useState(""); const [hotbarName, setHotbarName] = useState("");
const onSubmit = (name: string) => { const onSubmit = action((name: string) => {
if (!name.trim()) { if (!name.trim()) {
return; return;
} }
hotbarStore.setHotbarName(hotbarId, name); getHotbarById(hotbarId)?.name.set(name);
closeCommandOverlay(); closeCommandOverlay();
}; });
if (hotbarId) { if (hotbarId) {
return ( return (
@ -65,16 +75,16 @@ const NonInjectedHotbarRenameCommand = observer(({
onChange={(option) => { onChange={(option) => {
if (option) { if (option) {
setHotbarId(option.value.id); setHotbarId(option.value.id);
setHotbarName(option.value.name); setHotbarName(option.value.name.get());
} }
}} }}
components={{ DropdownIndicator: null, IndicatorSeparator: null }} components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true} menuIsOpen={true}
options={( options={(
hotbarStore.hotbars hotbars.get()
.map(hotbar => ({ .map(hotbar => ({
value: hotbar, value: hotbar,
label: hotbarStore.getDisplayLabel(hotbar), label: computeHotbarDisplayLabel(hotbar),
})) }))
)} )}
autoFocus={true} autoFocus={true}
@ -86,9 +96,11 @@ const NonInjectedHotbarRenameCommand = observer(({
export const HotbarRenameCommand = withInjectables<Dependencies>(NonInjectedHotbarRenameCommand, { export const HotbarRenameCommand = withInjectables<Dependencies>(NonInjectedHotbarRenameCommand, {
getProps: (di, props) => ({ getProps: (di, props) => ({
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
hotbarStore: di.inject(hotbarStoreInjectable),
uniqueHotbarName: di.inject(uniqueHotbarNameInjectable),
...props, ...props,
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
uniqueHotbarName: di.inject(uniqueHotbarNameInjectable),
getHotbarById: di.inject(getHotbarByIdInjectable),
hotbars: di.inject(hotbarsInjectable),
computeHotbarDisplayLabel: di.inject(computeHotbarDisplayLabelInjectable),
}), }),
}); });

View File

@ -7,28 +7,40 @@ import styles from "./hotbar-selector.module.scss";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Badge } from "../badge"; import { Badge } from "../badge";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import { HotbarSwitchCommand } from "./hotbar-switch-command"; import { HotbarSwitchCommand } from "./hotbar-switch-command";
import { Tooltip, TooltipPosition } from "../tooltip"; import { Tooltip, TooltipPosition } from "../tooltip";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { Hotbar } from "../../../common/hotbars/types";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import { cssNames } from "@k8slens/utilities"; import { cssNames } from "@k8slens/utilities";
import type { HotbarStore } from "../../../common/hotbars/store"; import type { IComputedValue } from "mobx";
import activeHotbarInjectable from "../../../features/hotbar/storage/common/active.injectable";
import type { SwitchToPreviousHotbar } from "../../../features/hotbar/storage/common/switch-to-previous.injectable";
import type { SwitchToNextHotbar } from "../../../features/hotbar/storage/common/switch-to-next.injectable";
import switchToNextHotbarInjectable from "../../../features/hotbar/storage/common/switch-to-next.injectable";
import switchToPreviousHotbarInjectable from "../../../features/hotbar/storage/common/switch-to-previous.injectable";
import type { Hotbar } from "../../../features/hotbar/storage/common/hotbar";
import type { ComputeDisplayIndex } from "../../../features/hotbar/storage/common/compute-display-index.injectable";
import computeDisplayIndexInjectable from "../../../features/hotbar/storage/common/compute-display-index.injectable";
interface Dependencies { interface Dependencies {
hotbarStore: HotbarStore; activeHotbar: IComputedValue<Hotbar | undefined>;
openCommandOverlay: (component: React.ReactElement) => void; openCommandOverlay: (component: React.ReactElement) => void;
switchToPreviousHotbar: SwitchToPreviousHotbar;
switchToNextHotbar: SwitchToNextHotbar;
computeDisplayIndex: ComputeDisplayIndex;
} }
export interface HotbarSelectorProps extends Partial<Dependencies> { const NonInjectedHotbarSelector = observer(({
hotbar: Hotbar; activeHotbar,
} openCommandOverlay,
switchToNextHotbar,
const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOverlay }: HotbarSelectorProps & Dependencies) => { switchToPreviousHotbar,
computeDisplayIndex,
}: Dependencies) => {
const [tooltipVisible, setTooltipVisible] = useState(false); const [tooltipVisible, setTooltipVisible] = useState(false);
const tooltipTimeout = useRef<number>(); const tooltipTimeout = useRef<number>();
const hotbar = activeHotbar.get();
function clearTimer() { function clearTimer() {
clearTimeout(tooltipTimeout.current); clearTimeout(tooltipTimeout.current);
@ -42,12 +54,12 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOv
function onPrevClick() { function onPrevClick() {
onTooltipShow(); onTooltipShow();
hotbarStore.switchToPrevious(); switchToPreviousHotbar();
} }
function onNextClick() { function onNextClick() {
onTooltipShow(); onTooltipShow();
hotbarStore.switchToNext(); switchToNextHotbar();
} }
function onMouseEvent(event: React.MouseEvent) { function onMouseEvent(event: React.MouseEvent) {
@ -65,7 +77,7 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOv
<Badge <Badge
id="hotbarIndex" id="hotbarIndex"
small small
label={hotbarStore.getDisplayIndex(hotbarStore.getActive())} label={hotbar ? computeDisplayIndex(hotbar.id) : "??"}
onClick={() => openCommandOverlay(<HotbarSwitchCommand />)} onClick={() => openCommandOverlay(<HotbarSwitchCommand />)}
className={styles.Badge} className={styles.Badge}
onMouseEnter={onMouseEvent} onMouseEnter={onMouseEvent}
@ -76,7 +88,7 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOv
targetId="hotbarIndex" targetId="hotbarIndex"
preferredPositions={[TooltipPosition.TOP, TooltipPosition.TOP_LEFT]} preferredPositions={[TooltipPosition.TOP, TooltipPosition.TOP_LEFT]}
> >
{hotbar.name} {hotbar?.name}
</Tooltip> </Tooltip>
</div> </div>
<Icon <Icon
@ -88,10 +100,13 @@ const NonInjectedHotbarSelector = observer(({ hotbar, hotbarStore, openCommandOv
); );
}); });
export const HotbarSelector = withInjectables<Dependencies, HotbarSelectorProps>(NonInjectedHotbarSelector, { export const HotbarSelector = withInjectables<Dependencies>(NonInjectedHotbarSelector, {
getProps: (di, props) => ({ getProps: (di, props) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
openCommandOverlay: di.inject(commandOverlayInjectable).open,
...props, ...props,
openCommandOverlay: di.inject(commandOverlayInjectable).open,
activeHotbar: di.inject(activeHotbarInjectable),
switchToNextHotbar: di.inject(switchToNextHotbarInjectable),
switchToPreviousHotbar: di.inject(switchToPreviousHotbarInjectable),
computeDisplayIndex: di.inject(computeDisplayIndexInjectable),
}), }),
}); });

View File

@ -6,21 +6,28 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Select } from "../select"; import { Select } from "../select";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { CommandOverlay } from "../command-palette"; import type { CommandOverlay } from "../command-palette";
import { HotbarAddCommand } from "./hotbar-add-command"; import { HotbarAddCommand } from "./hotbar-add-command";
import { HotbarRemoveCommand } from "./hotbar-remove-command"; import { HotbarRemoveCommand } from "./hotbar-remove-command";
import { HotbarRenameCommand } from "./hotbar-rename-command"; import { HotbarRenameCommand } from "./hotbar-rename-command";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import type { HotbarStore } from "../../../common/hotbars/store"; import type { SetAsActiveHotbar } from "../../../features/hotbar/storage/common/set-as-active.injectable";
import type { IComputedValue } from "mobx";
import type { Hotbar } from "../../../features/hotbar/storage/common/hotbar";
import type { ComputeHotbarDisplayLabel } from "../../../features/hotbar/storage/common/compute-display-label.injectable";
import computeHotbarDisplayLabelInjectable from "../../../features/hotbar/storage/common/compute-display-label.injectable";
import hotbarsInjectable from "../../../features/hotbar/storage/common/hotbars.injectable";
import setAsActiveHotbarInjectable from "../../../features/hotbar/storage/common/set-as-active.injectable";
const hotbarAddAction = Symbol("hotbar-add"); const hotbarAddAction = Symbol("hotbar-add");
const hotbarRemoveAction = Symbol("hotbar-remove"); const hotbarRemoveAction = Symbol("hotbar-remove");
const hotbarRenameAction = Symbol("hotbar-rename"); const hotbarRenameAction = Symbol("hotbar-rename");
interface Dependencies { interface Dependencies {
hotbarStore: HotbarStore; setAsActiveHotbar: SetAsActiveHotbar;
computeHotbarDisplayLabel: ComputeHotbarDisplayLabel;
hotbars: IComputedValue<Hotbar[]>;
commandOverlay: CommandOverlay; commandOverlay: CommandOverlay;
} }
@ -29,7 +36,9 @@ function ignoreIf<T>(check: boolean, menuItems: T[]): T[] {
} }
const NonInjectedHotbarSwitchCommand = observer(({ const NonInjectedHotbarSwitchCommand = observer(({
hotbarStore, setAsActiveHotbar,
computeHotbarDisplayLabel,
hotbars,
commandOverlay, commandOverlay,
}: Dependencies) => ( }: Dependencies) => (
<Select <Select
@ -50,22 +59,22 @@ const NonInjectedHotbarSwitchCommand = observer(({
return commandOverlay.open(<HotbarRenameCommand />); return commandOverlay.open(<HotbarRenameCommand />);
} }
} else { } else {
hotbarStore.setActiveHotbar(option.value); setAsActiveHotbar(option.value);
commandOverlay.close(); commandOverlay.close();
} }
}} }}
components={{ DropdownIndicator: null, IndicatorSeparator: null }} components={{ DropdownIndicator: null, IndicatorSeparator: null }}
menuIsOpen={true} menuIsOpen={true}
options={[ options={[
...hotbarStore.hotbars.map(hotbar => ({ ...hotbars.get().map(hotbar => ({
value: hotbar, value: hotbar,
label: hotbarStore.getDisplayLabel(hotbar), label: computeHotbarDisplayLabel(hotbar),
})), })),
{ {
value: hotbarAddAction, value: hotbarAddAction,
label: "Add hotbar ...", label: "Add hotbar ...",
}, },
...ignoreIf(hotbarStore.hotbars.length > 1, [ ...ignoreIf(hotbars.get().length > 1, [
{ {
value: hotbarRemoveAction, value: hotbarRemoveAction,
label: "Remove hotbar ...", label: "Remove hotbar ...",
@ -85,8 +94,10 @@ const NonInjectedHotbarSwitchCommand = observer(({
export const HotbarSwitchCommand = withInjectables<Dependencies>(NonInjectedHotbarSwitchCommand, { export const HotbarSwitchCommand = withInjectables<Dependencies>(NonInjectedHotbarSwitchCommand, {
getProps: (di, props) => ({ getProps: (di, props) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
commandOverlay: di.inject(commandOverlayInjectable),
...props, ...props,
commandOverlay: di.inject(commandOverlayInjectable),
computeHotbarDisplayLabel: di.inject(computeHotbarDisplayLabelInjectable),
hotbars: di.inject(hotbarsInjectable),
setAsActiveHotbar: di.inject(setAsActiveHotbarInjectable),
}), }),
}); });

View File

@ -4,19 +4,19 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable"; import findHotbarByNameInjectable from "../../../../features/hotbar/storage/common/find-by-name.injectable";
import { inputValidator } from "../input_validators"; import { inputValidator } from "../input_validators";
const uniqueHotbarNameInjectable = getInjectable({ const uniqueHotbarNameInjectable = getInjectable({
id: "unique-hotbar-name", id: "unique-hotbar-name",
instantiate: di => { instantiate: di => {
const store = di.inject(hotbarStoreInjectable); const findHotbarByName = di.inject(findHotbarByNameInjectable);
return inputValidator({ return inputValidator({
condition: ({ required }) => required, condition: ({ required }) => required,
message: () => "Hotbar with this name already exists", message: () => "Hotbar with this name already exists",
validate: value => !store.findByName(value), validate: value => !findHotbarByName(value),
}); });
}, },
}); });

View File

@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SidebarCluster/> renders w/o errors 1`] = `
<div>
<div
class="SidebarCluster"
data-testid="sidebar-cluster-dropdown"
id="cluster-test-uid"
role="menubar"
tabindex="0"
>
<div
class="Avatar rounded avatar"
style="width: 40px; height: 40px; background: rgb(96, 130, 10);"
>
tc
</div>
<div
class="clusterName"
id="tooltip-cluster-test-uid"
>
test-cluster
</div>
<i
class="Icon dropdown material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
</div>
`;

View File

@ -5,56 +5,47 @@
import React from "react"; import React from "react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react"; import { fireEvent } from "@testing-library/react";
import { SidebarCluster } from "../sidebar-cluster"; import { SidebarCluster } from "../sidebar-cluster";
import { KubernetesCluster } from "../../../../common/catalog-entities"; import { KubernetesCluster } from "../../../../common/catalog-entities";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor"; import { renderFor } from "../../test-utils/renderFor";
import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import type { HotbarStore } from "../../../../common/hotbars/store";
const clusterEntity = new KubernetesCluster({
metadata: {
uid: "test-uid",
name: "test-cluster",
source: "local",
labels: {},
},
spec: {
kubeconfigPath: "",
kubeconfigContext: "",
},
status: {
phase: "connected",
},
});
describe("<SidebarCluster/>", () => { describe("<SidebarCluster/>", () => {
let render: DiRender; let result: RenderResult;
beforeEach(() => { beforeEach(() => {
const di = getDiForUnitTesting(); const di = getDiForUnitTesting();
const render = renderFor(di);
di.override(hotbarStoreInjectable, () => ({ const clusterEntity = new KubernetesCluster({
isAddedToActive: () => {}, metadata: {
}) as unknown as HotbarStore); uid: "test-uid",
name: "test-cluster",
source: "local",
labels: {},
},
spec: {
kubeconfigPath: "",
kubeconfigContext: "",
},
status: {
phase: "connected",
},
});
render = renderFor(di); result = render(<SidebarCluster clusterEntity={clusterEntity}/>);
}); });
it("renders w/o errors", () => { it("renders w/o errors", () => {
const { container } = render(<SidebarCluster clusterEntity={clusterEntity}/>); expect(result.container).toMatchSnapshot();
expect(container).toBeInstanceOf(HTMLElement);
}); });
it("renders cluster avatar and name", () => { it("renders cluster avatar and name", () => {
const { getByText, getAllByText } = render(<SidebarCluster clusterEntity={clusterEntity}/>); expect(result.getByText("tc")).toBeInTheDocument();
expect(getByText("tc")).toBeInTheDocument(); const v = result.getAllByText("test-cluster");
const v = getAllByText("test-cluster");
expect(v.length).toBeGreaterThan(0); expect(v.length).toBeGreaterThan(0);
@ -64,11 +55,8 @@ describe("<SidebarCluster/>", () => {
}); });
it("renders cluster menu", () => { it("renders cluster menu", () => {
const { getByTestId, getByText } = render(<SidebarCluster clusterEntity={clusterEntity}/>); fireEvent.click(result.getByTestId("sidebar-cluster-dropdown"));
const link = getByTestId("sidebar-cluster-dropdown"); expect(result.getByText("Add to Hotbar")).toBeInTheDocument();
fireEvent.click(link);
expect(getByText("Add to Hotbar")).toBeInTheDocument();
}); });
}); });

View File

@ -14,8 +14,6 @@ import { Icon } from "../icon";
import { Menu, MenuItem } from "../menu"; import { Menu, MenuItem } from "../menu";
import { Tooltip } from "../tooltip"; import { Tooltip } from "../tooltip";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import hotbarStoreInjectable from "../../../common/hotbars/store.injectable";
import type { HotbarStore } from "../../../common/hotbars/store";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entity-context-menu.injectable"; import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entity-context-menu.injectable";
import visitEntityContextMenuInjectable from "../../../common/catalog/visit-entity-context-menu.injectable"; import visitEntityContextMenuInjectable from "../../../common/catalog/visit-entity-context-menu.injectable";
@ -23,6 +21,8 @@ import type { Navigate } from "../../navigation/navigate.injectable";
import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable"; import type { NormalizeCatalogEntityContextMenu } from "../../catalog/normalize-menu-item.injectable";
import navigateInjectable from "../../navigation/navigate.injectable"; import navigateInjectable from "../../navigation/navigate.injectable";
import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable"; import normalizeCatalogEntityContextMenuInjectable from "../../catalog/normalize-menu-item.injectable";
import type { ActiveHotbarModel } from "../../../features/hotbar/storage/common/toggling.injectable";
import activeHotbarInjectable from "../../../features/hotbar/storage/common/toggling.injectable";
export interface SidebarClusterProps { export interface SidebarClusterProps {
clusterEntity: CatalogEntity | null | undefined; clusterEntity: CatalogEntity | null | undefined;
@ -31,16 +31,16 @@ export interface SidebarClusterProps {
interface Dependencies { interface Dependencies {
navigate: Navigate; navigate: Navigate;
normalizeMenuItem: NormalizeCatalogEntityContextMenu; normalizeMenuItem: NormalizeCatalogEntityContextMenu;
hotbarStore: HotbarStore;
visitEntityContextMenu: VisitEntityContextMenu; visitEntityContextMenu: VisitEntityContextMenu;
entityInActiveHotbar: ActiveHotbarModel;
} }
const NonInjectedSidebarCluster = observer(({ const NonInjectedSidebarCluster = observer(({
clusterEntity, clusterEntity,
hotbarStore,
visitEntityContextMenu: onContextMenuOpen, visitEntityContextMenu: onContextMenuOpen,
navigate, navigate,
normalizeMenuItem, normalizeMenuItem,
entityInActiveHotbar,
}: Dependencies & SidebarClusterProps) => { }: Dependencies & SidebarClusterProps) => {
const [menuItems] = useState(observable.array<CatalogEntityContextMenu>()); const [menuItems] = useState(observable.array<CatalogEntityContextMenu>());
const [opened, setOpened] = useState(false ); const [opened, setOpened] = useState(false );
@ -61,13 +61,10 @@ const NonInjectedSidebarCluster = observer(({
} }
const onMenuOpen = () => { const onMenuOpen = () => {
const isAddedToActive = hotbarStore.isAddedToActive(clusterEntity); const title = entityInActiveHotbar.hasEntity(clusterEntity.getId())
const title = isAddedToActive
? "Remove from Hotbar" ? "Remove from Hotbar"
: "Add to Hotbar"; : "Add to Hotbar";
const onClick = isAddedToActive const onClick = () => entityInActiveHotbar.toggleEntity(clusterEntity);
? () => hotbarStore.removeFromHotbar(clusterEntity.getId())
: () => hotbarStore.addToHotbar(clusterEntity);
menuItems.replace([{ title, onClick }]); menuItems.replace([{ title, onClick }]);
onContextMenuOpen(clusterEntity, { onContextMenuOpen(clusterEntity, {
@ -148,9 +145,9 @@ const NonInjectedSidebarCluster = observer(({
export const SidebarCluster = withInjectables<Dependencies, SidebarClusterProps>(NonInjectedSidebarCluster, { export const SidebarCluster = withInjectables<Dependencies, SidebarClusterProps>(NonInjectedSidebarCluster, {
getProps: (di, props) => ({ getProps: (di, props) => ({
...props, ...props,
hotbarStore: di.inject(hotbarStoreInjectable),
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable), visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
navigate: di.inject(navigateInjectable), navigate: di.inject(navigateInjectable),
normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable), normalizeMenuItem: di.inject(normalizeCatalogEntityContextMenuInjectable),
entityInActiveHotbar: di.inject(activeHotbarInjectable),
}), }),
}); });

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { defaultHotbarCells } from "../../common/hotbars/types"; import { defaultHotbarCells } from "../../features/hotbar/storage/common/types";
import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster"; import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster";
import { hotbarTooManyItemsChannel } from "../../common/ipc/hotbar"; import { hotbarTooManyItemsChannel } from "../../common/ipc/hotbar";
import showErrorNotificationInjectable from "../components/notifications/show-error-notification.injectable"; import showErrorNotificationInjectable from "../components/notifications/show-error-notification.injectable";