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

Remove global shared state usages of hot bar store

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-03-31 09:50:30 +03:00
parent a1a088c655
commit ad7369988a
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
22 changed files with 681 additions and 548 deletions

View File

@ -7,11 +7,13 @@ import { anyObject } from "jest-mock-extended";
import mockFs from "mock-fs";
import logger from "../../main/logger";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { HotbarStore } from "../hotbar-store";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../hotbar-store.injectable";
import { HotbarStore } from "../hotbar-store";
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
catalogEntityRegistry: {
@ -123,12 +125,21 @@ const awsCluster = getMockCatalogEntity({
describe("HotbarStore", () => {
let di: DiContainer;
let hotbarStore: HotbarStore;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
di.override(hotbarStoreInjectable, () => {
HotbarStore.resetInstance();
return HotbarStore.createInstance({
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
});
});
});
afterEach(() => {
@ -140,18 +151,18 @@ describe("HotbarStore", () => {
mockFs();
await di.runSetups();
hotbarStore = di.inject(hotbarStoreInjectable);
});
describe("load", () => {
it("loads one hotbar by default", () => {
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
expect(hotbarStore.hotbars.length).toEqual(1);
});
});
describe("add", () => {
it("adds a hotbar", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.add({ name: "hottest" });
expect(hotbarStore.hotbars.length).toEqual(2);
});
@ -159,20 +170,14 @@ describe("HotbarStore", () => {
describe("hotbar items", () => {
it("initially creates 12 empty cells", () => {
const hotbarStore = HotbarStore.getInstance();
expect(hotbarStore.getActive().items.length).toEqual(12);
});
it("initially adds catalog entity as first item", () => {
const hotbarStore = HotbarStore.getInstance();
expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog");
});
it("adds items", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
const items = hotbarStore.getActive().items.filter(Boolean);
@ -180,8 +185,6 @@ describe("HotbarStore", () => {
});
it("removes items", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("test");
hotbarStore.removeFromHotbar("catalog-entity");
@ -191,8 +194,6 @@ describe("HotbarStore", () => {
});
it("does nothing if removing with invalid uid", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("invalid uid");
const items = hotbarStore.getActive().items.filter(Boolean);
@ -201,8 +202,6 @@ describe("HotbarStore", () => {
});
it("moves item to empty cell", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
@ -216,8 +215,6 @@ describe("HotbarStore", () => {
});
it("moves items down", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
@ -231,8 +228,6 @@ describe("HotbarStore", () => {
});
it("moves items up", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
@ -246,8 +241,6 @@ describe("HotbarStore", () => {
});
it("logs an error if cellIndex is out of bounds", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.add({ name: "hottest", id: "hottest" });
hotbarStore.setActiveHotbar("hottest");
@ -269,22 +262,16 @@ describe("HotbarStore", () => {
});
it("throws an error if getId is invalid or returns not a string", () => {
const hotbarStore = HotbarStore.getInstance();
expect(() => hotbarStore.addToHotbar({} as any)).toThrowError(TypeError);
expect(() => hotbarStore.addToHotbar({ getId: () => true } as any)).toThrowError(TypeError);
});
it("throws an error if getName is invalid or returns not a string", () => {
const hotbarStore = HotbarStore.getInstance();
expect(() => hotbarStore.addToHotbar({ getId: () => "" } as any)).toThrowError(TypeError);
expect(() => hotbarStore.addToHotbar({ getId: () => "", getName: () => 4 } as any)).toThrowError(TypeError);
});
it("does nothing when item moved to same cell", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.restackItems(1, 1);
@ -292,8 +279,6 @@ describe("HotbarStore", () => {
});
it("new items takes first empty cell", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(awsCluster);
hotbarStore.restackItems(0, 3);
@ -309,8 +294,6 @@ describe("HotbarStore", () => {
console.error = jest.fn();
console.warn = jest.fn();
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
@ -324,8 +307,6 @@ describe("HotbarStore", () => {
});
it("checks if entity already pinned to hotbar", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.addToHotbar(testCluster);
expect(hotbarStore.isAddedToActive(testCluster)).toBeTruthy();
@ -401,22 +382,24 @@ describe("HotbarStore", () => {
mockFs(configurationToBeMigrated);
await di.runSetups();
hotbarStore = di.inject(hotbarStoreInjectable);
});
it("allows to retrieve a hotbar", () => {
const hotbar = HotbarStore.getInstance().getById("3caac17f-aec2-4723-9694-ad204465d935");
const hotbar = hotbarStore.getById("3caac17f-aec2-4723-9694-ad204465d935");
expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
});
it("clears cells without entity", () => {
const items = HotbarStore.getInstance().hotbars[0].items;
const items = hotbarStore.hotbars[0].items;
expect(items[2]).toBeNull();
});
it("adds extra data to cells with according entity", () => {
const items = HotbarStore.getInstance().hotbars[0].items;
const items = hotbarStore.hotbars[0].items;
expect(items[0]).toEqual({
entity: {

View File

@ -2,366 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import catalogCatalogEntityInjectable from "./catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { action, comparer, observable, makeObservable, computed } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store";
import { toJS } from "./utils";
import { CatalogEntity } from "./catalog";
import logger from "../main/logger";
import { broadcastMessage } from "./ipc";
import {
defaultHotbarCells,
getEmptyHotbar,
Hotbar,
CreateHotbarData,
CreateHotbarOptions,
} from "./hotbar-types";
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
import type { GeneralEntity } from "./catalog-entities";
export interface HotbarStoreModel {
hotbars: Hotbar[];
activeHotbarId: string;
}
interface Dependencies {
catalogCatalogEntity: GeneralEntity;
}
export class HotbarStore extends BaseStore<HotbarStoreModel> {
readonly displayName = "HotbarStore";
@observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId: string;
constructor(private dependencies: Dependencies) {
super({
configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
migrations,
});
makeObservable(this);
this.load();
}
@computed get activeHotbarId() {
return this._activeHotbarId;
}
/**
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
* @param hotbar The hotbar instance, or the index, or its ID
*/
setActiveHotbar(hotbar: Hotbar | number | string) {
if (typeof hotbar === "number") {
if (hotbar >= 0 && hotbar < this.hotbars.length) {
this._activeHotbarId = this.hotbars[hotbar].id;
}
} else if (typeof hotbar === "string") {
if (this.getById(hotbar)) {
this._activeHotbarId = hotbar;
}
} else {
if (this.hotbars.indexOf(hotbar) >= 0) {
this._activeHotbarId = hotbar.id;
}
}
}
private hotbarIndexById(id: string) {
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
}
private hotbarIndex(hotbar: Hotbar) {
return this.hotbars.indexOf(hotbar);
}
@computed get activeHotbarIndex() {
return this.hotbarIndexById(this.activeHotbarId);
}
@action
protected fromStore(data: Partial<HotbarStoreModel> = {}) {
if (!data.hotbars || !data.hotbars.length) {
const hotbar = getEmptyHotbar("Default");
const {
metadata: { uid, name, source },
} = this.dependencies.catalogCatalogEntity;
const initialItem = { entity: { uid, name, source }};
hotbar.items[0] = initialItem;
this.hotbars = [hotbar];
} else {
this.hotbars = data.hotbars;
}
this.hotbars.forEach(ensureExactHotbarItemLength);
if (data.activeHotbarId) {
this.setActiveHotbar(data.activeHotbarId);
}
if (!this.activeHotbarId) {
this.setActiveHotbar(0);
}
}
toJSON(): HotbarStoreModel {
const model: HotbarStoreModel = {
hotbars: this.hotbars,
activeHotbarId: this.activeHotbarId,
};
return toJS(model);
}
getActive() {
return this.getById(this.activeHotbarId);
}
getByName(name: string) {
return this.hotbars.find((hotbar) => hotbar.name === name);
}
getById(id: string) {
return this.hotbars.find((hotbar) => hotbar.id === id);
}
add = action(
(
data: CreateHotbarData,
{ setActive = false }: CreateHotbarOptions = {},
) => {
const hotbar = getEmptyHotbar(data.name, data.id);
this.hotbars.push(hotbar);
if (setActive) {
this._activeHotbarId = hotbar.id;
}
},
);
setHotbarName = action((id: string, name: string) => {
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if (index < 0) {
return void console.warn(
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
{ id },
);
}
this.hotbars[index].name = name;
});
remove = action((hotbar: Hotbar) => {
if (this.hotbars.length <= 1) {
throw new Error("Cannot remove the last hotbar");
}
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
if (this.activeHotbarId === hotbar.id) {
this.setActiveHotbar(0);
}
});
@action
addToHotbar(item: CatalogEntity, cellIndex?: number) {
const hotbar = this.getActive();
const uid = item.getId();
const name = item.getName();
if (typeof uid !== "string") {
throw new TypeError("CatalogEntity's ID must be a string");
}
if (typeof name !== "string") {
throw new TypeError("CatalogEntity's NAME must be a string");
}
if (this.isAddedToActive(item)) {
return;
}
const entity = {
uid,
name,
source: item.metadata.source,
};
const newItem = { entity };
if (cellIndex === undefined) {
// Add item to empty cell
const emptyCellIndex = hotbar.items.indexOf(null);
if (emptyCellIndex != -1) {
hotbar.items[emptyCellIndex] = newItem;
} else {
broadcastMessage(hotbarTooManyItemsChannel);
}
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
hotbar.items[cellIndex] = newItem;
} else {
logger.error(
`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`,
{ entityId: uid, hotbarId: hotbar.id, cellIndex },
);
}
}
@action
removeFromHotbar(uid: string): void {
const hotbar = this.getActive();
const index = hotbar.items.findIndex((item) => item?.entity.uid === uid);
if (index < 0) {
return;
}
hotbar.items[index] = null;
}
/**
* Remove all hotbar items that reference the `uid`.
* @param uid The `EntityId` that each hotbar item refers to
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
*/
@action
removeAllHotbarItems(uid: string) {
for (const hotbar of this.hotbars) {
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
if (index >= 0) {
hotbar.items[index] = null;
}
}
}
findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
while (this.getActive().items[index] != null) {
index += direction;
}
return index;
}
@action
restackItems(from: number, to: number): void {
const { items } = this.getActive();
const source = items[from];
const moveDown = from < to;
if (
from < 0 ||
to < 0 ||
from >= items.length ||
to >= items.length ||
isNaN(from) ||
isNaN(to)
) {
throw new Error("Invalid 'from' or 'to' arguments");
}
if (from == to) {
return;
}
items.splice(from, 1, null);
if (items[to] == null) {
items.splice(to, 1, source);
} else {
// Move cells up or down to closes empty cell
items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1);
items.splice(to, 0, source);
}
}
switchToPrevious() {
const hotbarStore = HotbarStore.getInstance();
let index = hotbarStore.activeHotbarIndex - 1;
if (index < 0) {
index = hotbarStore.hotbars.length - 1;
}
hotbarStore.setActiveHotbar(index);
}
switchToNext() {
const hotbarStore = HotbarStore.getInstance();
let index = hotbarStore.activeHotbarIndex + 1;
if (index >= hotbarStore.hotbars.length) {
index = 0;
}
hotbarStore.setActiveHotbar(index);
}
/**
* Checks if entity already pinned to the active hotbar
*/
isAddedToActive(entity: CatalogEntity | null | undefined): boolean {
if (!entity) {
return false;
}
return (
this.getActive().items.findIndex(
(item) => item?.entity.uid === entity.getId(),
) >= 0
);
}
getDisplayLabel(hotbar: Hotbar): string {
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
}
getDisplayIndex(hotbar: Hotbar): string {
const index = this.hotbarIndex(hotbar);
if (index < 0) {
return "??";
}
return `${index + 1}`;
}
}
/**
* This function ensures that there are always exactly `defaultHotbarCells`
* worth of items in the hotbar.
* @param hotbar The hotbar to modify
*/
function ensureExactHotbarItemLength(hotbar: Hotbar) {
// if there are not enough items
while (hotbar.items.length < defaultHotbarCells) {
hotbar.items.push(null);
}
// if for some reason the hotbar was overfilled before, remove as many entries
// as needed, but prefer empty slots and items at the end first.
while (hotbar.items.length > defaultHotbarCells) {
const lastNull = hotbar.items.lastIndexOf(null);
if (lastNull >= 0) {
hotbar.items.splice(lastNull, 1);
} else {
hotbar.items.length = defaultHotbarCells;
}
}
}
import { HotbarStore } from "./hotbar-store";
const hotbarStoreInjectable = getInjectable({
id: "hotbar-store",
@ -373,6 +16,8 @@ const hotbarStoreInjectable = getInjectable({
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
});
},
causesSideEffects: true,
});
export default hotbarStoreInjectable;

View File

@ -3,13 +3,358 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import hotbarStoreInjectable from "./hotbar-store.injectable";
import { action, comparer, observable, makeObservable, computed } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store";
import { toJS } from "./utils";
import { CatalogEntity } from "./catalog";
import logger from "../main/logger";
import { broadcastMessage } from "./ipc";
import {
asLegacyGlobalSingletonForExtensionApi,
} from "../extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api";
defaultHotbarCells,
getEmptyHotbar,
Hotbar,
CreateHotbarData,
CreateHotbarOptions,
} from "./hotbar-types";
import { hotbarTooManyItemsChannel } from "./ipc/hotbar";
import type { GeneralEntity } from "./catalog-entities";
export interface HotbarStoreModel {
hotbars: Hotbar[];
activeHotbarId: string;
}
interface Dependencies {
catalogCatalogEntity: GeneralEntity;
}
export class HotbarStore extends BaseStore<HotbarStoreModel> {
readonly displayName = "HotbarStore";
@observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId: string;
constructor(private dependencies: Dependencies) {
super({
configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
migrations,
});
makeObservable(this);
this.load();
}
@computed get activeHotbarId() {
return this._activeHotbarId;
}
/**
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
* @param hotbar The hotbar instance, or the index, or its ID
*/
setActiveHotbar(hotbar: Hotbar | number | string) {
if (typeof hotbar === "number") {
if (hotbar >= 0 && hotbar < this.hotbars.length) {
this._activeHotbarId = this.hotbars[hotbar].id;
}
} else if (typeof hotbar === "string") {
if (this.getById(hotbar)) {
this._activeHotbarId = hotbar;
}
} else {
if (this.hotbars.indexOf(hotbar) >= 0) {
this._activeHotbarId = hotbar.id;
}
}
}
private hotbarIndexById(id: string) {
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
}
private hotbarIndex(hotbar: Hotbar) {
return this.hotbars.indexOf(hotbar);
}
@computed get activeHotbarIndex() {
return this.hotbarIndexById(this.activeHotbarId);
}
@action
protected fromStore(data: Partial<HotbarStoreModel> = {}) {
if (!data.hotbars || !data.hotbars.length) {
const hotbar = getEmptyHotbar("Default");
const {
metadata: { uid, name, source },
} = this.dependencies.catalogCatalogEntity;
const initialItem = { entity: { uid, name, source }};
hotbar.items[0] = initialItem;
this.hotbars = [hotbar];
} else {
this.hotbars = data.hotbars;
}
this.hotbars.forEach(ensureExactHotbarItemLength);
if (data.activeHotbarId) {
this.setActiveHotbar(data.activeHotbarId);
}
if (!this.activeHotbarId) {
this.setActiveHotbar(0);
}
}
toJSON(): HotbarStoreModel {
const model: HotbarStoreModel = {
hotbars: this.hotbars,
activeHotbarId: this.activeHotbarId,
};
return toJS(model);
}
getActive() {
return this.getById(this.activeHotbarId);
}
getByName(name: string) {
return this.hotbars.find((hotbar) => hotbar.name === name);
}
getById(id: string) {
return this.hotbars.find((hotbar) => hotbar.id === id);
}
add = action(
(
data: CreateHotbarData,
{ setActive = false }: CreateHotbarOptions = {},
) => {
const hotbar = getEmptyHotbar(data.name, data.id);
this.hotbars.push(hotbar);
if (setActive) {
this._activeHotbarId = hotbar.id;
}
},
);
setHotbarName = action((id: string, name: string) => {
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if (index < 0) {
return void console.warn(
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
{ id },
);
}
this.hotbars[index].name = name;
});
remove = action((hotbar: Hotbar) => {
if (this.hotbars.length <= 1) {
throw new Error("Cannot remove the last hotbar");
}
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
if (this.activeHotbarId === hotbar.id) {
this.setActiveHotbar(0);
}
});
@action
addToHotbar(item: CatalogEntity, cellIndex?: number) {
const hotbar = this.getActive();
const uid = item.getId();
const name = item.getName();
if (typeof uid !== "string") {
throw new TypeError("CatalogEntity's ID must be a string");
}
if (typeof name !== "string") {
throw new TypeError("CatalogEntity's NAME must be a string");
}
if (this.isAddedToActive(item)) {
return;
}
const entity = {
uid,
name,
source: item.metadata.source,
};
const newItem = { entity };
if (cellIndex === undefined) {
// Add item to empty cell
const emptyCellIndex = hotbar.items.indexOf(null);
if (emptyCellIndex != -1) {
hotbar.items[emptyCellIndex] = newItem;
} else {
broadcastMessage(hotbarTooManyItemsChannel);
}
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
hotbar.items[cellIndex] = newItem;
} else {
logger.error(
`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`,
{ entityId: uid, hotbarId: hotbar.id, cellIndex },
);
}
}
@action
removeFromHotbar(uid: string): void {
const hotbar = this.getActive();
const index = hotbar.items.findIndex((item) => item?.entity.uid === uid);
if (index < 0) {
return;
}
hotbar.items[index] = null;
}
/**
* Remove all hotbar items that reference the `uid`.
* @param uid The `EntityId` that each hotbar item refers to
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
*/
@action
removeAllHotbarItems(uid: string) {
for (const hotbar of this.hotbars) {
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
if (index >= 0) {
hotbar.items[index] = null;
}
}
}
findClosestEmptyIndex(from: number, direction = 1) {
let index = from;
while (this.getActive().items[index] != null) {
index += direction;
}
return index;
}
@action
restackItems(from: number, to: number): void {
const { items } = this.getActive();
const source = items[from];
const moveDown = from < to;
if (
from < 0 ||
to < 0 ||
from >= items.length ||
to >= items.length ||
isNaN(from) ||
isNaN(to)
) {
throw new Error("Invalid 'from' or 'to' arguments");
}
if (from == to) {
return;
}
items.splice(from, 1, null);
if (items[to] == null) {
items.splice(to, 1, source);
} else {
// Move cells up or down to closes empty cell
items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1);
items.splice(to, 0, source);
}
}
switchToPrevious() {
let index = this.activeHotbarIndex - 1;
if (index < 0) {
index = this.hotbars.length - 1;
}
this.setActiveHotbar(index);
}
switchToNext() {
let index = this.activeHotbarIndex + 1;
if (index >= this.hotbars.length) {
index = 0;
}
this.setActiveHotbar(index);
}
/**
* Checks if entity already pinned to the active hotbar
*/
isAddedToActive(entity: CatalogEntity | null | undefined): boolean {
if (!entity) {
return false;
}
return (
this.getActive().items.findIndex(
(item) => item?.entity.uid === entity.getId(),
) >= 0
);
}
getDisplayLabel(hotbar: Hotbar): string {
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
}
getDisplayIndex(hotbar: Hotbar): string {
const index = this.hotbarIndex(hotbar);
if (index < 0) {
return "??";
}
return `${index + 1}`;
}
}
/**
* @deprecated use di.inject(hotbarStoreInjectable) instead.
* This function ensures that there are always exactly `defaultHotbarCells`
* worth of items in the hotbar.
* @param hotbar The hotbar to modify
*/
export const HotbarStore = asLegacyGlobalSingletonForExtensionApi(hotbarStoreInjectable);
function ensureExactHotbarItemLength(hotbar: Hotbar) {
// if there are not enough items
while (hotbar.items.length < defaultHotbarCells) {
hotbar.items.push(null);
}
// if for some reason the hotbar was overfilled before, remove as many entries
// as needed, but prefer empty slots and items at the end first.
while (hotbar.items.length > defaultHotbarCells) {
const lastNull = hotbar.items.lastIndexOf(null);
if (lastNull >= 0) {
hotbar.items.splice(lastNull, 1);
} else {
hotbar.items.length = defaultHotbarCells;
}
}
}

View File

@ -34,6 +34,7 @@ import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectab
import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake";
import joinPathsInjectable from "../common/path/join-paths.injectable";
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false },
@ -65,6 +66,8 @@ export const getDiForUnitTesting = (
// eslint-disable-next-line unused-imports/no-unused-vars-ts
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
di.override(hotbarStoreInjectable, () => ({}));
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
// eslint-disable-next-line unused-imports/no-unused-vars-ts

View File

@ -41,6 +41,8 @@ import navigateToAddClusterInjectable from "../common/front-end-routing/routes/
import addSyncEntriesInjectable from "./initializers/add-sync-entries.injectable";
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
import { bindEvents } from "./navigation/events";
import deleteClusterDialogModelInjectable
from "./components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model.injectable";
if (process.isMainFrame) {
SentryInit();
@ -99,6 +101,7 @@ export async function bootstrap(di: DiContainer) {
logger.info(`${logPrefix} initializing Catalog`);
initializers.initCatalog({
openCommandDialog: di.inject(commandOverlayInjectable).open,
deleteClusterDialogModel: di.inject(deleteClusterDialogModelInjectable),
});
const extensionLoader = di.inject(extensionLoaderInjectable);

View File

@ -12,6 +12,7 @@ import { CatalogCategory } from "../../../api/catalog-entity";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { AdditionalCategoryColumnRegistration, CategoryColumnRegistration } from "../custom-category-columns";
import getCategoryColumnsInjectable, { CategoryColumns, GetCategoryColumnsParams } from "../get-category-columns.injectable";
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
class TestCategory extends CatalogCategory {
apiVersion = "catalog.k8slens.dev/v1alpha1";
@ -39,6 +40,8 @@ describe("Custom Category Columns", () => {
beforeEach(() => {
di = getDiForUnitTesting();
di.override(hotbarStoreInjectable, () => ({}));
});
describe("without extensions", () => {

View File

@ -13,7 +13,6 @@ import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.s
import { navigate } from "../../navigation";
import { MenuItem, MenuActions } from "../menu";
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
import { HotbarStore } from "../../../common/hotbar-store";
import { ConfirmDialog } from "../confirm-dialog";
import { catalogCategoryRegistry, CatalogEntity } from "../../../common/catalog";
import { CatalogAddButton } from "./catalog-add-button";
@ -39,6 +38,8 @@ import catalogRouteParametersInjectable from "./catalog-route-parameters.injecta
import { browseCatalogTab } from "./catalog-browse-tab";
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
import type { HotbarStore } from "../../../common/hotbar-store";
interface Dependencies {
catalogPreviousActiveTabStorage: { set: (value: string ) => void };
@ -53,6 +54,7 @@ interface Dependencies {
};
navigateToCatalog: NavigateToCatalog;
hotbarStore: HotbarStore;
}
@observer
@ -125,11 +127,11 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
}
addToHotbar(entity: CatalogEntity): void {
HotbarStore.getInstance().addToHotbar(entity);
this.props.hotbarStore.addToHotbar(entity);
}
removeFromHotbar(entity: CatalogEntity): void {
HotbarStore.getInstance().removeFromHotbar(entity.getId());
this.props.hotbarStore.removeFromHotbar(entity.getId());
}
onDetails = (entity: CatalogEntity) => {
@ -216,7 +218,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
};
renderName(entity: CatalogEntity) {
const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(entity);
const isItemInHotbar = this.props.hotbarStore.isAddedToActive(entity);
return (
<>
@ -341,5 +343,6 @@ export const Catalog = withInjectables<Dependencies>( NonInjectedCatalog, {
routeParameters: di.inject(catalogRouteParametersInjectable),
navigateToCatalog: di.inject(navigateToCatalogInjectable),
emitEvent: di.inject(appEventBusInjectable).emit,
hotbarStore: di.inject(hotbarStoreInjectable),
}),
});

View File

@ -9,10 +9,12 @@ import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
import type { ItemListLayoutProps } from "../item-object-list";
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
import categoryColumnsInjectable from "./custom-category-columns.injectable";
import { defaultCategoryColumns, browseAllColumns, nameCategoryColumn } from "./internal-category-columns";
import { defaultCategoryColumns, browseAllColumns } from "./internal-category-columns";
import nameCategoryColumnInjectable from "./name-category-column.injectable";
interface Dependencies {
extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>;
nameCategoryColumn: RegisteredAdditionalCategoryColumn;
}
export interface GetCategoryColumnsParams {
@ -21,7 +23,7 @@ export interface GetCategoryColumnsParams {
export type CategoryColumns = Required<Pick<ItemListLayoutProps<CatalogEntity>, "sortingCallbacks" | "searchFilters" | "renderTableContents" | "renderTableHeader">>;
function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>): RegisteredAdditionalCategoryColumn[] {
function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionColumns: IComputedValue<Map<string, Map<string, RegisteredAdditionalCategoryColumn[]>>>, nameCategoryColumn: RegisteredAdditionalCategoryColumn): RegisteredAdditionalCategoryColumn[] {
const fromExtensions = (
extensionColumns
.get()
@ -41,7 +43,7 @@ function getSpecificCategoryColumns(activeCategory: CatalogCategory, extensionCo
];
}
function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] {
function getBrowseAllColumns(nameCategoryColumn: RegisteredAdditionalCategoryColumn): RegisteredAdditionalCategoryColumn[] {
return [
...browseAllColumns,
nameCategoryColumn,
@ -49,11 +51,11 @@ function getBrowseAllColumns(): RegisteredAdditionalCategoryColumn[] {
];
}
const getCategoryColumns = ({ extensionColumns }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => {
const getCategoryColumns = ({ extensionColumns, nameCategoryColumn }: Dependencies) => ({ activeCategory }: GetCategoryColumnsParams): CategoryColumns => {
const allRegistrations = orderBy(
activeCategory
? getSpecificCategoryColumns(activeCategory, extensionColumns)
: getBrowseAllColumns(),
? getSpecificCategoryColumns(activeCategory, extensionColumns, nameCategoryColumn)
: getBrowseAllColumns(nameCategoryColumn),
"priority",
"asc",
);
@ -89,6 +91,7 @@ const getCategoryColumnsInjectable = getInjectable({
instantiate: (di) => getCategoryColumns({
extensionColumns: di.inject(categoryColumnsInjectable),
nameCategoryColumn: di.inject(nameCategoryColumnInjectable),
}),
});

View File

@ -4,26 +4,54 @@
*/
import React, { ReactNode, useState } from "react";
import { HotbarStore } from "../../../common/hotbar-store";
import { MenuItem } from "../menu";
import type { CatalogEntity } from "../../api/catalog-entity";
import { withInjectables } from "@ogre-tools/injectable-react";
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
import type { HotbarStore } from "../../../common/hotbar-store";
export function HotbarToggleMenuItem(props: { entity: CatalogEntity; addContent: ReactNode; removeContent: ReactNode }) {
const store = HotbarStore.getInstance();
const [itemInHotbar, setItemInHotbar] = useState(store.isAddedToActive(props.entity));
interface Dependencies {
hotbarStore: HotbarStore;
}
interface HotbarToggleMenuItemProps {
entity: CatalogEntity;
addContent: ReactNode;
removeContent: ReactNode;
}
function NonInjectedHotbarToggleMenuItem({
addContent,
entity,
hotbarStore,
removeContent,
}: Dependencies & HotbarToggleMenuItemProps) {
const [itemInHotbar, setItemInHotbar] = useState(hotbarStore.isAddedToActive(entity));
return (
<MenuItem onClick={() => {
if (itemInHotbar) {
store.removeFromHotbar(props.entity.getId());
hotbarStore.removeFromHotbar(entity.getId());
setItemInHotbar(false);
} else {
store.addToHotbar(props.entity);
hotbarStore.addToHotbar(entity);
setItemInHotbar(true);
}
}}>
{itemInHotbar ? props.removeContent : props.addContent }
{itemInHotbar ? removeContent : addContent }
</MenuItem>
);
}
export const HotbarToggleMenuItem = withInjectables<Dependencies, HotbarToggleMenuItemProps>(
NonInjectedHotbarToggleMenuItem,
{
getProps: (di, props) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
...props,
}),
},
);

View File

@ -6,49 +6,10 @@
import styles from "./catalog.module.scss";
import React from "react";
import { HotbarStore } from "../../../common/hotbar-store";
import type { CatalogEntity } from "../../api/catalog-entity";
import { Avatar } from "../avatar";
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
import { Icon } from "../icon";
import { prevDefault } from "../../utils";
import { getLabelBadges } from "./helpers";
import { KubeObject } from "../../../common/k8s-api/kube-object";
function renderEntityName(entity: CatalogEntity) {
const hotbarStore = HotbarStore.getInstance();
const isItemInHotbar = hotbarStore.isAddedToActive(entity);
const onClick = prevDefault(
isItemInHotbar
? () => hotbarStore.removeFromHotbar(entity.getId())
: () => hotbarStore.addToHotbar(entity),
);
return (
<>
<Avatar
title={entity.getName()}
colorHash={`${entity.getName()}-${entity.getSource()}`}
src={entity.spec.icon?.src}
background={entity.spec.icon?.background}
className={styles.catalogAvatar}
size={24}
>
{entity.spec.icon?.material && <Icon material={entity.spec.icon?.material} small/>}
</Avatar>
<span>{entity.getName()}</span>
<Icon
small
className={styles.pinIcon}
material={!isItemInHotbar && "push_pin"}
svg={isItemInHotbar ? "push_off" : "push_pin"}
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
onClick={onClick}
/>
</>
);
}
export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [
{
id: "kind",
@ -63,20 +24,6 @@ export const browseAllColumns: RegisteredAdditionalCategoryColumn[] = [
},
];
export const nameCategoryColumn: RegisteredAdditionalCategoryColumn = {
id: "name",
priority: 0,
renderCell: renderEntityName,
titleProps: {
title: "Name",
className: styles.entityName,
id: "name",
sortBy: "name",
},
searchFilter: entity => entity.getName(),
sortCallback: entity => `name=${entity.getName()}`,
};
export const defaultCategoryColumns: RegisteredAdditionalCategoryColumn[] = [
{
id: "source",

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import styles from "./catalog.module.scss";
import type { CatalogEntity } from "../../../common/catalog";
import { prevDefault } from "../../utils";
import { Avatar } from "../avatar";
import { Icon } from "../icon";
import React from "react";
import type { RegisteredAdditionalCategoryColumn } from "./custom-category-columns";
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
import type { HotbarStore } from "../../../common/hotbar-store";
const renderEntityName = (hotbarStore: HotbarStore) => (entity: CatalogEntity) => {
const isItemInHotbar = hotbarStore.isAddedToActive(entity);
const onClick = prevDefault(
isItemInHotbar
? () => hotbarStore.removeFromHotbar(entity.getId())
: () => hotbarStore.addToHotbar(entity),
);
return (
<>
<Avatar
title={entity.getName()}
colorHash={`${entity.getName()}-${entity.getSource()}`}
src={entity.spec.icon?.src}
background={entity.spec.icon?.background}
className={styles.catalogAvatar}
size={24}
>
{entity.spec.icon?.material && <Icon material={entity.spec.icon?.material} small/>}
</Avatar>
<span>{entity.getName()}</span>
<Icon
small
className={styles.pinIcon}
material={!isItemInHotbar && "push_pin"}
svg={isItemInHotbar ? "push_off" : "push_pin"}
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
onClick={onClick}
/>
</>
);
};
const nameCategoryColumnInjectable = getInjectable({
id: "name-category-column",
instantiate: (di): RegisteredAdditionalCategoryColumn => ({
id: "name",
priority: 0,
renderCell: renderEntityName(di.inject(hotbarStoreInjectable)),
titleProps: {
title: "Name",
className: styles.entityName,
id: "name",
sortBy: "name",
},
searchFilter: (entity) => entity.getName(),
sortCallback: (entity) => `name=${entity.getName()}`,
}),
});
export default nameCategoryColumnInjectable;

View File

@ -4,7 +4,7 @@
*/
import "@testing-library/jest-dom/extend-expect";
import { KubeConfig } from "@kubernetes/client-node";
import { fireEvent, render } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import mockFs from "mock-fs";
import React from "react";
import * as selectEvent from "react-select-event";
@ -16,6 +16,11 @@ import type { ClusterModel } from "../../../../common/cluster-types";
import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing";
import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token";
import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable";
import deleteClusterDialogModelInjectable from "../delete-cluster-dialog-model/delete-cluster-dialog-model.injectable";
import type { DeleteClusterDialogModel } from "../delete-cluster-dialog-model/delete-cluster-dialog-model";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
jest.mock("electron", () => ({
app: {
@ -88,16 +93,23 @@ let config: KubeConfig;
describe("<DeleteClusterDialog />", () => {
let createCluster: (model: ClusterModel) => Cluster;
let deleteClusterDialogModel: DeleteClusterDialogModel;
let render: DiRender;
beforeEach(async () => {
const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true });
const { mainDi, rendererDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true });
render = renderFor(rendererDi);
mainDi.override(createContextHandlerInjectable, () => () => undefined);
mockFs();
rendererDi.override(hotbarStoreInjectable, () => ({}));
await runSetups();
deleteClusterDialogModel = rendererDi.inject(deleteClusterDialogModelInjectable);
createCluster = mainDi.inject(createClusterInjectionToken);
});
@ -137,7 +149,7 @@ describe("<DeleteClusterDialog />", () => {
kubeConfigPath: "./temp-kube-config",
});
DeleteClusterDialog.open({ cluster, config });
deleteClusterDialogModel.open({ cluster, config });
const { getByText } = render(<DeleteClusterDialog />);
const message = "The contents of kubeconfig file will be changed!";
@ -155,7 +167,7 @@ describe("<DeleteClusterDialog />", () => {
kubeConfigPath: "./temp-kube-config",
});
DeleteClusterDialog.open({ cluster, config });
deleteClusterDialogModel.open({ cluster, config });
const { getByTestId } = render(<DeleteClusterDialog />);
@ -172,7 +184,7 @@ describe("<DeleteClusterDialog />", () => {
kubeConfigPath: "./temp-kube-config",
});
DeleteClusterDialog.open({ cluster, config });
deleteClusterDialogModel.open({ cluster, config });
const { getByText } = render(<DeleteClusterDialog />);
@ -193,7 +205,7 @@ describe("<DeleteClusterDialog />", () => {
kubeConfigPath: "./temp-kube-config",
});
DeleteClusterDialog.open({ cluster, config });
deleteClusterDialogModel.open({ cluster, config });
const { getByText, getByTestId } = render(<DeleteClusterDialog />);
const link = getByTestId("context-switch");
@ -220,7 +232,7 @@ describe("<DeleteClusterDialog />", () => {
const spy = jest.spyOn(cluster, "isInLocalKubeconfig").mockImplementation(() => true);
DeleteClusterDialog.open({ cluster, config });
deleteClusterDialogModel.open({ cluster, config });
const { getByTestId } = render(<DeleteClusterDialog />);
@ -256,7 +268,7 @@ describe("<DeleteClusterDialog />", () => {
kubeConfigPath: "./temp-kube-config",
});
DeleteClusterDialog.open({ cluster, config });
deleteClusterDialogModel.open({ cluster, config });
const { getByTestId } = render(<DeleteClusterDialog />);

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 { DeleteClusterDialogModel } from "./delete-cluster-dialog-model";
const deleteClusterDialogModelInjectable = getInjectable({
id: "delete-cluster-dialog-model",
instantiate: () => new DeleteClusterDialogModel(),
});
export default deleteClusterDialogModelInjectable;

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 type { KubeConfig } from "@kubernetes/client-node";
import { observable, makeObservable, action } from "mobx";
import type { Cluster } from "../../../../common/cluster/cluster";
export class DeleteClusterDialogModel {
isOpen = false;
constructor() {
makeObservable(this, {
isOpen: observable,
open: action,
close: action,
});
}
cluster: Cluster;
config: KubeConfig;
open = ({ cluster, config }: { cluster: Cluster; config: KubeConfig }) => {
this.isOpen = true;
this.cluster = cluster;
this.config = config;
};
close = () => {
this.isOpen = false;
};
}

View File

@ -9,52 +9,35 @@ import { observer } from "mobx-react";
import React from "react";
import { Button } from "../button";
import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../../common/cluster/cluster";
import { saveKubeconfig } from "./save-config";
import { Notifications } from "../notifications";
import { HotbarStore } from "../../../common/hotbar-store";
import { boundMethod } from "autobind-decorator";
import { Dialog } from "../dialog";
import { Icon } from "../icon";
import { Select } from "../select";
import { Checkbox } from "../checkbox";
import { requestClearClusterAsDeleting, requestDeleteCluster, requestSetClusterAsDeleting } from "../../ipc";
import { withInjectables } from "@ogre-tools/injectable-react";
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
import type { DeleteClusterDialogModel } from "./delete-cluster-dialog-model/delete-cluster-dialog-model";
import deleteClusterDialogModelInjectable from "./delete-cluster-dialog-model/delete-cluster-dialog-model.injectable";
import type { HotbarStore } from "../../../common/hotbar-store";
interface DialogState {
isOpen: boolean;
config?: KubeConfig;
cluster?: Cluster;
interface Dependencies {
hotbarStore: HotbarStore;
model: DeleteClusterDialogModel;
}
const dialogState: DialogState = observable({
isOpen: false,
});
export interface DeleteClusterDialogProps {}
@observer
export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProps> {
class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
@observable showContextSwitch = false;
@observable newCurrentContext = "";
constructor(props: DeleteClusterDialogProps) {
constructor(props: Dependencies) {
super(props);
makeObservable(this);
}
static open({ config, cluster }: Partial<DialogState>) {
dialogState.isOpen = true;
dialogState.config = config;
dialogState.cluster = cluster;
}
static close() {
dialogState.isOpen = false;
dialogState.cluster = null;
dialogState.config = null;
}
@boundMethod
onOpen() {
this.newCurrentContext = "";
@ -66,25 +49,25 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
@boundMethod
onClose() {
DeleteClusterDialog.close();
this.props.model.close();
this.showContextSwitch = false;
}
removeContext() {
dialogState.config.contexts = dialogState.config.contexts.filter(item =>
item.name !== dialogState.cluster.contextName,
this.props.model.config.contexts = this.props.model.config.contexts.filter(item =>
item.name !== this.props.model.cluster.contextName,
);
}
changeCurrentContext() {
if (this.newCurrentContext && this.showContextSwitch) {
dialogState.config.currentContext = this.newCurrentContext;
this.props.model.config.currentContext = this.newCurrentContext;
}
}
@boundMethod
async onDelete() {
const { cluster, config } = dialogState;
const { cluster, config } = this.props.model;
await requestSetClusterAsDeleting(cluster.id);
this.removeContext();
@ -92,7 +75,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
try {
await saveKubeconfig(config, cluster.kubeConfigPath);
HotbarStore.getInstance().removeAllHotbarItems(cluster.id);
this.props.hotbarStore.removeAllHotbarItems(cluster.id);
await requestDeleteCluster(cluster.id);
} catch(error) {
Notifications.error(`Cannot remove cluster, failed to process config file. ${error}`);
@ -104,7 +87,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
}
@computed get disableDelete() {
const { cluster, config } = dialogState;
const { cluster, config } = this.props.model;
const noContextsAvailable = config.contexts.filter(context => context.name !== cluster.contextName).length == 0;
const newContextNotSelected = this.newCurrentContext === "";
@ -116,12 +99,12 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
}
isCurrentContext() {
return dialogState.config.currentContext == dialogState.cluster.contextName;
return this.props.model.config.currentContext == this.props.model.cluster.contextName;
}
renderCurrentContextSwitch() {
if (!this.showContextSwitch) return null;
const { cluster, config } = dialogState;
const { cluster, config } = this.props.model;
const contexts = config.contexts.filter(context => context.name !== cluster.contextName);
const options = [
@ -146,7 +129,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
}
renderDeleteMessage() {
const { cluster } = dialogState;
const { cluster } = this.props.model;
if (cluster.isInLocalKubeconfig()) {
return (
@ -164,7 +147,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
}
getWarningMessage() {
const { cluster, config } = dialogState;
const { cluster, config } = this.props.model;
const contexts = config.contexts.filter(context => context.name !== cluster.contextName);
if (!contexts.length) {
@ -206,7 +189,7 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
}
render() {
const { cluster, config, isOpen } = dialogState;
const { cluster, config, isOpen } = this.props.model;
if (!cluster || !config) return null;
@ -258,3 +241,14 @@ export class DeleteClusterDialog extends React.Component<DeleteClusterDialogProp
);
}
}
export const DeleteClusterDialog = withInjectables<Dependencies>(
NonInjectedDeleteClusterDialog,
{
getProps: (di) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
model: di.inject(deleteClusterDialogModelInjectable),
}),
},
);

View File

@ -10,7 +10,7 @@ import React from "react";
import type { DiContainer } from "@ogre-tools/injectable";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { DiRender, renderFor } from "../../test-utils/renderFor";
import hotbarStoreInjectable, { HotbarStore } from "../../../../common/hotbar-store.injectable";
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
import { ThemeStore } from "../../../theme.store";
import { ConfirmDialog } from "../../confirm-dialog";
import { UserStore } from "../../../../common/user-store";
@ -18,6 +18,7 @@ import mockFs from "mock-fs";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../../../common/get-configuration-file-model/app-version/app-version.injectable";
import type { HotbarStore } from "../../../../common/hotbar-store";
const mockHotbars: { [id: string]: any } = {
"1": {

View File

@ -10,7 +10,6 @@ import { observer } from "mobx-react";
import { HotbarEntityIcon } from "./hotbar-entity-icon";
import { cssNames, IClassName } from "../../utils";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { HotbarStore } from "../../../common/hotbar-store";
import type { CatalogEntity } from "../../api/catalog-entity";
import { DragDropContext, Draggable, Droppable, type DropResult } from "react-beautiful-dnd";
import { HotbarSelector } from "./hotbar-selector";
@ -18,26 +17,33 @@ import { HotbarCell } from "./hotbar-cell";
import { HotbarIcon } from "./hotbar-icon";
import { defaultHotbarCells, HotbarItem } from "../../../common/hotbar-types";
import { action, makeObservable, observable } from "mobx";
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { HotbarStore } from "../../../common/hotbar-store";
export interface HotbarMenuProps {
className?: IClassName;
}
interface Dependencies {
hotbarStore: HotbarStore;
}
@observer
export class HotbarMenu extends React.Component<HotbarMenuProps> {
class NonInjectedHotbarMenu extends React.Component<Dependencies & HotbarMenuProps> {
@observable draggingOver = false;
constructor(props: HotbarMenuProps) {
constructor(props: Dependencies & HotbarMenuProps) {
super(props);
makeObservable(this);
}
get hotbar() {
return HotbarStore.getInstance().getActive();
return this.props.hotbarStore.getActive();
}
getEntity(item: HotbarItem) {
const hotbar = HotbarStore.getInstance().getActive();
const hotbar = this.props.hotbarStore.getActive();
if (!hotbar) {
return null;
@ -64,17 +70,17 @@ export class HotbarMenu extends React.Component<HotbarMenuProps> {
const from = parseInt(source.droppableId);
const to = parseInt(destination.droppableId);
HotbarStore.getInstance().restackItems(from, to);
this.props.hotbarStore.restackItems(from, to);
}
removeItem(uid: string) {
const hotbar = HotbarStore.getInstance();
const hotbar = this.props.hotbarStore;
hotbar.removeFromHotbar(uid);
}
addItem(entity: CatalogEntity, index = -1) {
const hotbar = HotbarStore.getInstance();
const hotbar = this.props.hotbarStore;
hotbar.addToHotbar(entity, index);
}
@ -160,8 +166,7 @@ export class HotbarMenu extends React.Component<HotbarMenuProps> {
}
render() {
const { className } = this.props;
const hotbarStore = HotbarStore.getInstance();
const { className, hotbarStore } = this.props;
const hotbar = hotbarStore.getActive();
return (
@ -176,3 +181,14 @@ export class HotbarMenu extends React.Component<HotbarMenuProps> {
);
}
}
export const HotbarMenu = withInjectables<Dependencies, HotbarMenuProps>(
NonInjectedHotbarMenu,
{
getProps: (di, props) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
...props,
}),
},
);

View File

@ -6,13 +6,14 @@
import React from "react";
import { observer } from "mobx-react";
import { Select } from "../select";
import hotbarStoreInjectable, { HotbarStore } from "../../../common/hotbar-store.injectable";
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
import type { CommandOverlay } from "../command-palette";
import { HotbarAddCommand } from "./hotbar-add-command";
import { HotbarRemoveCommand } from "./hotbar-remove-command";
import { HotbarRenameCommand } from "./hotbar-rename-command";
import { withInjectables } from "@ogre-tools/injectable-react";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import type { HotbarStore } from "../../../common/hotbar-store";
const addActionId = "__add__";
const removeActionId = "__remove__";

View File

@ -5,17 +5,14 @@
import React from "react";
import "@testing-library/jest-dom/extend-expect";
import { render, fireEvent } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import { SidebarCluster } from "../sidebar-cluster";
import { KubernetesCluster } from "../../../../common/catalog-entities";
jest.mock("../../../../common/hotbar-store", () => ({
HotbarStore: {
getInstance: () => ({
isAddedToActive: jest.fn(),
}),
},
}));
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import hotbarStoreInjectable from "../../../../common/hotbar-store.injectable";
import type { HotbarStore } from "../../../../common/hotbar-store";
const clusterEntity = new KubernetesCluster({
metadata: {
@ -34,6 +31,18 @@ const clusterEntity = new KubernetesCluster({
});
describe("<SidebarCluster/>", () => {
let render: DiRender;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(hotbarStoreInjectable, () => ({
isAddedToActive: () => {},
}) as unknown as HotbarStore);
render = renderFor(di);
});
it("renders w/o errors", () => {
const { container } = render(<SidebarCluster clusterEntity={clusterEntity}/>);

View File

@ -6,7 +6,6 @@
import styles from "./sidebar-cluster.module.scss";
import { observable } from "mobx";
import React, { useState } from "react";
import { HotbarStore } from "../../../common/hotbar-store";
import { broadcastMessage } from "../../../common/ipc";
import type { CatalogEntity, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
import { IpcRendererNavigationEvents } from "../../navigation/events";
@ -16,6 +15,9 @@ import { navigate } from "../../navigation";
import { Menu, MenuItem } from "../menu";
import { ConfirmDialog } from "../confirm-dialog";
import { Tooltip } from "../tooltip";
import { withInjectables } from "@ogre-tools/injectable-react";
import hotbarStoreInjectable from "../../../common/hotbar-store.injectable";
import type { HotbarStore } from "../../../common/hotbar-store";
import { observer } from "mobx-react";
const contextMenu: CatalogEntityContextMenuContext = observable({
@ -60,7 +62,15 @@ function renderLoadingSidebarCluster() {
);
}
export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: CatalogEntity }) => {
interface Dependencies {
hotbarStore: HotbarStore;
}
interface SidebarClusterProps {
clusterEntity: CatalogEntity;
}
const NonInjectedSidebarCluster = observer(({ clusterEntity, hotbarStore }: Dependencies & SidebarClusterProps) => {
const [opened, setOpened] = useState(false);
if (!clusterEntity) {
@ -68,8 +78,7 @@ export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: Cata
}
const onMenuOpen = () => {
const hotbarStore = HotbarStore.getInstance();
const isAddedToActive = HotbarStore.getInstance().isAddedToActive(clusterEntity);
const isAddedToActive = hotbarStore.isAddedToActive(clusterEntity);
const title = isAddedToActive
? "Remove from Hotbar"
: "Add to Hotbar";
@ -140,3 +149,14 @@ export const SidebarCluster = observer(({ clusterEntity }: { clusterEntity: Cata
</div>
);
});
export const SidebarCluster = withInjectables<Dependencies, SidebarClusterProps>(
NonInjectedSidebarCluster,
{
getProps: (di, props) => ({
hotbarStore: di.inject(hotbarStoreInjectable),
...props,
}),
},
);

View File

@ -32,6 +32,7 @@ import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectab
import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake";
import joinPathsInjectable from "../common/path/join-paths.injectable";
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false },
@ -62,6 +63,8 @@ export const getDiForUnitTesting = (
// eslint-disable-next-line unused-imports/no-unused-vars-ts
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
di.override(hotbarStoreInjectable, () => ({}));
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
// eslint-disable-next-line unused-imports/no-unused-vars-ts

View File

@ -10,9 +10,9 @@ import { ClusterStore } from "../../common/cluster-store/cluster-store";
import { catalogCategoryRegistry } from "../api/catalog-category-registry";
import { WeblinkAddCommand } from "../components/catalog-entities/weblink-add-command";
import { loadConfigFromString } from "../../common/kube-helpers";
import { DeleteClusterDialog } from "../components/delete-cluster-dialog";
import type { DeleteClusterDialogModel } from "../components/delete-cluster-dialog/delete-cluster-dialog-model/delete-cluster-dialog-model";
async function onClusterDelete(clusterId: string) {
async function onClusterDelete(clusterId: string, deleteClusterDialogModel: DeleteClusterDialogModel) {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (!cluster) {
@ -25,14 +25,15 @@ async function onClusterDelete(clusterId: string) {
throw error;
}
DeleteClusterDialog.open({ cluster, config });
deleteClusterDialogModel.open({ cluster, config });
}
interface Dependencies {
openCommandDialog: (component: React.ReactElement) => void;
deleteClusterDialogModel: DeleteClusterDialogModel;
}
export function initCatalog({ openCommandDialog }: Dependencies) {
export function initCatalog({ openCommandDialog, deleteClusterDialogModel }: Dependencies) {
catalogCategoryRegistry
.getForGroupKind("entity.k8slens.dev", "WebLink")
.on("catalogAddMenu", ctx => {
@ -50,7 +51,7 @@ export function initCatalog({ openCommandDialog }: Dependencies) {
context.menuItems.push({
title: "Remove",
icon: "delete",
onClick: () => onClusterDelete(entity.getId()),
onClick: () => onClusterDelete(entity.getId(), deleteClusterDialogModel),
});
}
});