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

Make sure that there are only ever 12 slots in a hotbar (#3379)

* Make sure that there are only ever 12 slots in a hotbar

- Send notification if hotbar is full

- Filter out on every fromStore call

- Switch to broadcast on error

- Fix addToHotbar index check

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* change when to/fromStore are located

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-08-04 03:37:08 -04:00 committed by GitHub
parent fb74859f66
commit 17b14f9871
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 27 deletions

View File

@ -19,7 +19,9 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { anyObject } from "jest-mock-extended";
import mockFs from "mock-fs";
import logger from "../../main/logger";
import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store";
@ -187,7 +189,7 @@ describe("HotbarStore", () => {
hotbarStore.removeFromHotbar("catalog-entity");
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items.length).toEqual(0);
expect(items).toStrictEqual([]);
});
it("does nothing if removing with invalid uid", () => {
@ -245,6 +247,43 @@ describe("HotbarStore", () => {
expect(items.slice(0, 4)).toEqual(["catalog-entity", "minikube", "aws", "test"]);
});
it("logs an error if cellIndex is out of bounds", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.add({ name: "hottest", id: "hottest" });
hotbarStore.activeHotbarId = "hottest";
const { error } = logger;
const mocked = jest.fn();
logger.error = mocked;
hotbarStore.addToHotbar(testCluster, -1);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 12);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
hotbarStore.addToHotbar(testCluster, 13);
expect(mocked).toBeCalledWith("[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range", anyObject());
logger.error = error;
});
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();

View File

@ -28,6 +28,8 @@ import { getAppVersion, Singleton, toJS, Disposer } from "./utils";
import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual";
import { isTestEnv } from "./vars";
import { kebabCase } from "lodash";
export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: IReactionOptions;
@ -59,12 +61,14 @@ export abstract class BaseStore<T> extends Singleton {
const res: any = this.fromStore(this.storeConfig.store);
if (res instanceof Promise || (typeof res === "object" && res && typeof res.then === "function")) {
console.error(`${this.name} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`);
console.error(`${this.constructor.name} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`);
}
this.enableSync();
logger.info(`[STORE]: LOADED from ${this.path}`);
if (!isTestEnv) {
logger.info(`[${kebabCase(this.constructor.name).toUpperCase()}]: LOADED from ${this.path}`);
}
}
get name() {

View File

@ -22,11 +22,12 @@
import { action, comparer, observable, makeObservable } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store";
import isNull from "lodash/isNull";
import { toJS } from "./utils";
import { CatalogEntity } from "./catalog";
import { catalogEntity } from "../main/catalog-sources/general";
import { Hotbar, HotbarCreateOptions, HotbarItem, getEmptyHotbar } from "./hotbar-types";
import logger from "../main/logger";
import { broadcastMessage, HotbarTooManyItems } from "./ipc";
import { defaultHotbarCells, getEmptyHotbar, Hotbar, HotbarCreateOptions } from "./hotbar-types";
export interface HotbarStoreModel {
hotbars: Hotbar[];
@ -82,6 +83,8 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
this.hotbars = data.hotbars;
}
this.hotbars.forEach(ensureExactHotbarItemLength);
if (data.activeHotbarId) {
if (this.getById(data.activeHotbarId)) {
this.activeHotbarId = data.activeHotbarId;
@ -93,6 +96,15 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
}
toJSON(): HotbarStoreModel {
const model: HotbarStoreModel = {
hotbars: this.hotbars,
activeHotbarId: this.activeHotbarId
};
return toJS(model);
}
getActive() {
return this.getById(this.activeHotbarId);
}
@ -139,39 +151,52 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
@action
addToHotbar(item: CatalogEntity, cellIndex = -1) {
addToHotbar(item: CatalogEntity, cellIndex?: number) {
const hotbar = this.getActive();
const uid = item.metadata?.uid;
const name = item.metadata?.name;
if (typeof uid !== "string") {
throw new TypeError("CatalogEntity.metadata.uid must be a string");
}
if (typeof name !== "string") {
throw new TypeError("CatalogEntity.metadata.name must be a string");
}
const newItem = { entity: {
uid: item.metadata.uid,
name: item.metadata.name,
source: item.metadata.source
uid,
name,
source: item.metadata.source,
}};
if (hotbar.items.find(i => i?.entity.uid === item.metadata.uid)) {
if (hotbar.items.find(i => i?.entity.uid === uid)) {
return;
}
if (cellIndex == -1) {
if (cellIndex === undefined) {
// Add item to empty cell
const emptyCellIndex = hotbar.items.findIndex(isNull);
const emptyCellIndex = hotbar.items.indexOf(null);
if (emptyCellIndex != -1) {
hotbar.items[emptyCellIndex] = newItem;
} else {
// Add new item to the end of list
hotbar.items.push(newItem);
broadcastMessage(HotbarTooManyItems);
}
} else {
} 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((i) => i?.entity.uid === uid);
const index = hotbar.items.findIndex(item => item?.entity.uid === uid);
if (index == -1) {
if (index < 0) {
return;
}
@ -185,13 +210,10 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
*/
@action
removeAllHotbarItems(uid: string) {
const undoItems: [Hotbar, number, HotbarItem][] = [];
for (const hotbar of this.hotbars) {
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
if (index >= 0) {
undoItems.push([hotbar, index, hotbar.items[index]]);
hotbar.items[index] = null;
}
}
@ -252,13 +274,33 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
}
}
toJSON(): HotbarStoreModel {
const model: HotbarStoreModel = {
hotbars: this.hotbars,
activeHotbarId: this.activeHotbarId
};
/**
* 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 (hotbar.items.length === defaultHotbarCells) {
// if we already have `defaultHotbarCells` then we are good to stop
return;
}
return toJS(model);
// otherwise, keep adding empty entries until full
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;
}
}
}

22
src/common/ipc/hotbar.ts Normal file
View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export const HotbarTooManyItems = "hotbar:too-many-items";

View File

@ -24,3 +24,4 @@ export * from "./invalid-kubeconfig";
export * from "./update-available.ipc";
export * from "./cluster.ipc";
export * from "./type-enforced-ipc";
export * from "./hotbar";

View File

@ -21,13 +21,14 @@
import React from "react";
import { ipcRenderer, IpcRendererEvent } from "electron";
import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ClusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs, ListNamespaceForbiddenArgs } from "../../common/ipc";
import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg, ClusterListNamespaceForbiddenChannel, isListNamespaceForbiddenArgs, ListNamespaceForbiddenArgs, HotbarTooManyItems } from "../../common/ipc";
import { Notifications, notificationsStore } from "../components/notifications";
import { Button } from "../components/button";
import { isMac } from "../../common/vars";
import { ClusterStore } from "../../common/cluster-store";
import { navigate } from "../navigation";
import { entitySettingsURL } from "../../common/routes";
import { defaultHotbarCells } from "../../common/hotbar-types";
function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void {
notificationsStore.remove(notificationId);
@ -112,6 +113,10 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]:
);
}
function HotbarTooManyItemsHandler(): void {
Notifications.error(`Cannot have more than ${defaultHotbarCells} items pinned to a hotbar`);
}
export function registerIpcHandlers() {
onCorrect({
source: ipcRenderer,
@ -125,4 +130,10 @@ export function registerIpcHandlers() {
listener: ListNamespacesForbiddenHandler,
verifier: isListNamespaceForbiddenArgs,
});
onCorrect({
source: ipcRenderer,
channel: HotbarTooManyItems,
listener: HotbarTooManyItemsHandler,
verifier: (args: unknown[]): args is [] => args.length === 0,
});
}