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

Merge remote-tracking branch 'origin/master' into mobx6-migration

# Conflicts:
#	src/renderer/kube-object.store.ts
This commit is contained in:
Roman 2021-05-07 12:29:55 +03:00
commit 81a6b3ce17
11 changed files with 151 additions and 44 deletions

View File

@ -2,7 +2,7 @@
"name": "open-lens", "name": "open-lens",
"productName": "OpenLens", "productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes", "description": "OpenLens - Open Source IDE for Kubernetes",
"version": "5.0.0-beta.2", "version": "5.0.0-beta.4",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors", "copyright": "© 2021 OpenLens Authors",
"license": "MIT", "license": "MIT",

View File

@ -207,6 +207,21 @@ describe("HotbarStore", () => {
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test"); expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test");
}); });
it("new items takes first empty cell", () => {
const hotbarStore = HotbarStore.createInstance();
const test = new CatalogEntityItem(testCluster);
const minikube = new CatalogEntityItem(minikubeCluster);
const aws = new CatalogEntityItem(awsCluster);
hotbarStore.load();
hotbarStore.addToHotbar(test);
hotbarStore.addToHotbar(aws);
hotbarStore.restackItems(0, 3);
hotbarStore.addToHotbar(minikube);
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube");
});
it("throws if invalid arguments provided", () => { it("throws if invalid arguments provided", () => {
// Prevent writing to stderr during this render. // Prevent writing to stderr during this render.
const err = console.error; const err = console.error;

View File

@ -1,31 +1,61 @@
import { splitArray } from "../splitArray"; import { bifurcateArray, splitArray } from "../splitArray";
describe("split array on element tests", () => { describe("split array on element tests", () => {
test("empty array", () => { it("empty array", () => {
expect(splitArray([], 10)).toStrictEqual([[], [], false]); expect(splitArray([], 10)).toStrictEqual([[], [], false]);
}); });
test("one element, not in array", () => { it("one element, not in array", () => {
expect(splitArray([1], 10)).toStrictEqual([[1], [], false]); expect(splitArray([1], 10)).toStrictEqual([[1], [], false]);
}); });
test("ten elements, not in array", () => { it("ten elements, not in array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]); expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
}); });
test("one elements, in array", () => { it("one elements, in array", () => {
expect(splitArray([1], 1)).toStrictEqual([[], [], true]); expect(splitArray([1], 1)).toStrictEqual([[], [], true]);
}); });
test("ten elements, in front array", () => { it("ten elements, in front array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]); expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
}); });
test("ten elements, in middle array", () => { it("ten elements, in middle array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]); expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
}); });
test("ten elements, in end array", () => { it("ten elements, in end array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]); expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
}); });
}); });
describe("bifurcateArray", () => {
it("should return tuple of empty arrays from empty array", () => {
const [left, right] = bifurcateArray([], () => true);
expect(left).toStrictEqual([]);
expect(right).toStrictEqual([]);
});
it("should return all true condition returning items in the right array", () => {
const [left, right] = bifurcateArray([1, 2, 3], () => true);
expect(left).toStrictEqual([]);
expect(right).toStrictEqual([1, 2, 3]);
});
it("should return all false condition returning items in the right array", () => {
const [left, right] = bifurcateArray([1, 2, 3], () => false);
expect(left).toStrictEqual([1, 2, 3]);
expect(right).toStrictEqual([]);
});
it("should split array as specified", () => {
const [left, right] = bifurcateArray([1, 2, 3], (i) => Boolean(i % 2));
expect(left).toStrictEqual([2]);
expect(right).toStrictEqual([1, 3]);
});
});

View File

@ -1,4 +1,3 @@
// Moved from dashboard/client/utils/arrays.ts
/** /**
* This function splits an array into two sub arrays on the first instance of * This function splits an array into two sub arrays on the first instance of
* element (from the left). If the array does not contain the element. The * element (from the left). If the array does not contain the element. The
@ -19,3 +18,20 @@ export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
return [array.slice(0, index), array.slice(index + 1, array.length), true]; return [array.slice(0, index), array.slice(index + 1, array.length), true];
} }
/**
* Splits an array into two parts based on the outcome of `condition`. If `true`
* the value will be returned as part of the right array. If `false` then part of
* the left array.
* @param src the full array to bifurcate
* @param condition the function to determine which set each is in
*/
export function bifurcateArray<T>(src: T[], condition: (item: T) => boolean): [falses: T[], trues: T[]] {
const res: [T[], T[]] = [[], []];
for (const item of src) {
res[+condition(item)].push(item);
}
return res;
}

View File

@ -4,7 +4,7 @@ import { isDevelopment, isPublishConfigured, isTestEnv } from "../common/vars";
import { delay } from "../common/utils"; import { delay } from "../common/utils";
import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc"; import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
import { once } from "lodash"; import { once } from "lodash";
import { app, ipcMain } from "electron"; import { ipcMain } from "electron";
let installVersion: null | string = null; let installVersion: null | string = null;
@ -17,7 +17,6 @@ function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: Upd
if (arg.now) { if (arg.now) {
logger.info(`${AutoUpdateLogPrefix}: User chose to update now`); logger.info(`${AutoUpdateLogPrefix}: User chose to update now`);
autoUpdater.quitAndInstall(true, true); autoUpdater.quitAndInstall(true, true);
app.exit(); // this is needed for the installer not to fail on windows.
} else { } else {
logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`); logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`);
autoUpdater.autoInstallOnAppQuit = true; autoUpdater.autoInstallOnAppQuit = true;

View File

@ -63,10 +63,6 @@ export class ClusterManager extends Singleton {
@action syncClustersFromCatalog(entities: KubernetesCluster[]) { @action syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) { for (const entity of entities) {
if (entity.metadata.source !== "local") {
continue;
}
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid); const cluster = ClusterStore.getInstance().getById(entity.metadata.uid);
if (!cluster) { if (!cluster) {

View File

@ -50,6 +50,7 @@ import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale
import { CommandContainer } from "./command-palette/command-container"; import { CommandContainer } from "./command-palette/command-container";
import { KubeObjectStore } from "../kube-object.store"; import { KubeObjectStore } from "../kube-object.store";
import { clusterContext } from "./context"; import { clusterContext } from "./context";
import { namespaceStore } from "./+namespaces/namespace.store";
@observer @observer
export class App extends React.Component { export class App extends React.Component {
@ -89,7 +90,7 @@ export class App extends React.Component {
componentDidMount() { componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], { kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore, namespaceStore], {
preload: true, preload: true,
}) })
]); ]);

View File

@ -6,6 +6,8 @@
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
transition: none; transition: none;
text-shadow: 0 0 4px #0000008f;
position: relative;
div.MuiAvatar-colorDefault { div.MuiAvatar-colorDefault {
font-weight:500; font-weight:500;
@ -25,7 +27,9 @@
} }
&:hover { &:hover {
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30; &:not(.active) {
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30;
}
} }
&.isDragging { &.isDragging {

View File

@ -46,12 +46,32 @@
position: relative; position: relative;
&.isDraggingOver { &.isDraggingOver {
box-shadow: 0 0 0px 3px $clusterMenuBackground, 0 0 0px 6px #fff; background-color: #3e4148;
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff30;
&:not(.isDraggingOwner) {
z-index: 50;
> div:not(:empty) {
border-radius: 6px;
box-shadow: 0 0 9px #00000042;
}
&.animateUp {
> div {
transform: translate(0px, -40px)!important;
}
}
&.animateDown {
> div {
transform: translate(0px, 40px);
}
}
}
} }
&.animating { &.animating {
transform: translateZ(0); // Remove flickering artifacts
&:empty { &:empty {
animation: shake .6s cubic-bezier(.36,.07,.19,.97) both; animation: shake .6s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
@ -60,7 +80,9 @@
} }
&:not(:empty) { &:not(:empty) {
animation: outline 1s cubic-bezier(0.19, 1, 0.22, 1); .HotbarIcon {
animation: click .1s;
}
} }
} }
} }
@ -85,13 +107,12 @@
} }
} }
// TODO: Use theme-aware colors @keyframes click {
@keyframes outline {
0% { 0% {
box-shadow: 0 0 0px 11px $clusterMenuBackground, 0 0 0px 15px #ffffff00; margin-top: 0;
} }
100% { 100% {
box-shadow: 0 0 0px 3px $clusterMenuBackground, 0 0 0px 6px #ffffff; margin-top: 2px;
} }
} }

View File

@ -48,9 +48,16 @@ export class HotbarMenu extends React.Component<Props> {
HotbarStore.getInstance().restackItems(from, to); HotbarStore.getInstance().restackItems(from, to);
} }
getMoveAwayDirection(entityId: string, cellIndex: number) {
const draggableItemIndex = this.hotbar.items.findIndex(item => item?.entity.uid == entityId);
return draggableItemIndex > cellIndex ? "animateDown" : "animateUp";
}
renderGrid() { renderGrid() {
return this.hotbar.items.map((item, index) => { return this.hotbar.items.map((item, index) => {
const entity = this.getEntity(item); const entity = this.getEntity(item);
const isActive = !entity ? false : this.isActive(entity);
return ( return (
<Droppable droppableId={`${index}`} key={index}> <Droppable droppableId={`${index}`} key={index}>
@ -59,11 +66,14 @@ export class HotbarMenu extends React.Component<Props> {
index={index} index={index}
key={entity ? entity.getId() : `cell${index}`} key={entity ? entity.getId() : `cell${index}`}
innerRef={provided.innerRef} innerRef={provided.innerRef}
className={cssNames({ isDraggingOver: snapshot.isDraggingOver })} className={cssNames({
isDraggingOver: snapshot.isDraggingOver,
isDraggingOwner: snapshot.draggingOverWith == entity?.getId(),
}, this.getMoveAwayDirection(snapshot.draggingOverWith, index))}
{...provided.droppableProps} {...provided.droppableProps}
> >
{entity && ( {entity && (
<Draggable draggableId={item.entity.uid} key={item.entity.uid} index={0}> <Draggable draggableId={item.entity.uid} key={item.entity.uid} index={0} >
{(provided, snapshot) => { {(provided, snapshot) => {
const style = { const style = {
zIndex: defaultHotbarCells - index, zIndex: defaultHotbarCells - index,
@ -83,7 +93,7 @@ export class HotbarMenu extends React.Component<Props> {
key={index} key={index}
index={index} index={index}
entity={entity} entity={entity}
isActive={this.isActive(entity)} isActive={isActive}
onClick={() => entity.onRun(catalogEntityRunContext)} onClick={() => entity.onRun(catalogEntityRunContext)}
className={cssNames({ isDragging: snapshot.isDragging })} className={cssNames({ isDragging: snapshot.isDragging })}
/> />

View File

@ -1,7 +1,7 @@
import type { ClusterContext } from "./components/context"; import type { ClusterContext } from "./components/context";
import { action, computed, observable, reaction, when, makeObservable } from "mobx"; import { action, computed, observable, reaction, when, makeObservable } from "mobx";
import { autoBind, noop, rejectPromiseBy } from "./utils"; import { autoBind, bifurcateArray, noop, rejectPromiseBy } from "./utils";
import { KubeObject, KubeStatus } from "./api/kube-object"; import { KubeObject, KubeStatus } from "./api/kube-object";
import { IKubeWatchEvent } from "./api/kube-watch-api"; import { IKubeWatchEvent } from "./api/kube-watch-api";
import { ItemStore } from "./item.store"; import { ItemStore } from "./item.store";
@ -282,21 +282,36 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
subscribe(apis = this.getSubscribeApis()) { subscribe(apis = this.getSubscribeApis()) {
const abortController = new AbortController(); const abortController = new AbortController();
const [clusterScopedApis, namespaceScopedApis] = bifurcateArray(apis, api => api.isNamespaced);
// This waits for the context and namespaces to be ready or fails fast if the disposer is called for (const api of namespaceScopedApis) {
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])]) const store = apiManager.getStore(api);
.then(() => {
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) { // This waits for the context and namespaces to be ready or fails fast if the disposer is called
apis.forEach(api => this.watchNamespace(api, "", abortController)); Promise.race([rejectPromiseBy(abortController.signal), Promise.all([store.contextReady, store.namespacesReady])])
} else { .then(() => {
apis.forEach(api => { if (
this.loadedNamespaces.forEach((namespace) => { store.context.cluster.isGlobalWatchEnabled
this.watchNamespace(api, namespace, abortController); && store.loadedNamespaces.length === 0
}); ) {
}); return store.watchNamespace(api, "", abortController);
} }
})
.catch(noop); // ignore DOMExceptions for (const namespace of this.loadedNamespaces) {
store.watchNamespace(api, namespace, abortController);
}
})
.catch(noop); // ignore DOMExceptions
}
for (const api of clusterScopedApis) {
/**
* if the api is cluster scoped then we will never assign to `loadedNamespaces`
* and thus `store.namespacesReady` will never resolve. Futhermore, we don't care
* about watching namespaces.
*/
apiManager.getStore(api).watchNamespace(api, "", abortController);
}
return () => { return () => {
abortController.abort(); abortController.abort();