mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Copy old Dock
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
parent
4d2ee16436
commit
3d8d95ada3
@ -0,0 +1,17 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { TabId } from "./store";
|
||||||
|
import dockStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
const closeDockTabInjectable = getInjectable({
|
||||||
|
id: "close-dock-tab",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const dockStore = di.inject(dockStoreInjectable);
|
||||||
|
|
||||||
|
return (tabId: TabId): void => {
|
||||||
|
dockStore.closeTab(tabId);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default closeDockTabInjectable;
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { keyboardShortcutInjectionToken } from "@k8slens/keyboard-shortcuts";
|
||||||
|
|
||||||
|
const closeDockShortcutInjectable = getInjectable({
|
||||||
|
id: "close-dock-shortcut",
|
||||||
|
|
||||||
|
instantiate: () => ({
|
||||||
|
scope: "dock",
|
||||||
|
|
||||||
|
binding: {
|
||||||
|
shift: true,
|
||||||
|
code: "Escape",
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke: () => {
|
||||||
|
// Close Dock
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: keyboardShortcutInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default closeDockShortcutInjectable;
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { keyboardShortcutInjectionToken } from "@k8slens/keyboard-shortcuts";
|
||||||
|
|
||||||
|
const closeTabShortcutInjectable = getInjectable({
|
||||||
|
id: "close-tab-shortcut",
|
||||||
|
|
||||||
|
instantiate: () => ({
|
||||||
|
scope: "dock",
|
||||||
|
|
||||||
|
binding: {
|
||||||
|
ctrlOrCommand: true,
|
||||||
|
code: "KeyW",
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke: () => {
|
||||||
|
// Close Tab
|
||||||
|
// Focus the Dock to avoid losing the focus
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: keyboardShortcutInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default closeTabShortcutInjectable;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import dockStoreInjectable from "./store.injectable";
|
||||||
|
import type { DockTab, DockTabCreate } from "./store";
|
||||||
|
|
||||||
|
const createDockTabInjectable = getInjectable({
|
||||||
|
id: "create-dock-tab",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const dockStore = di.inject(dockStoreInjectable);
|
||||||
|
|
||||||
|
return (rawTabDesc: DockTabCreate, addNumber?: boolean): DockTab =>
|
||||||
|
dockStore.createTab(rawTabDesc, addNumber);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createDockTabInjectable;
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||||
|
import type { DockStorageState } from "./store";
|
||||||
|
|
||||||
|
const dockStorageInjectable = getInjectable({
|
||||||
|
id: "dock-storage",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const createStorage = di.inject(createStorageInjectable);
|
||||||
|
|
||||||
|
return createStorage<DockStorageState>("dock", {
|
||||||
|
height: 300,
|
||||||
|
tabs: [
|
||||||
|
// {
|
||||||
|
// id: "terminal",
|
||||||
|
// kind: TabKind.TERMINAL,
|
||||||
|
// title: "Terminal",
|
||||||
|
// pinned: false,
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
isOpen: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default dockStorageInjectable;
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { DockTabStoreOptions } from "./dock-tab.store";
|
||||||
|
import { DockTabStore } from "./dock-tab.store";
|
||||||
|
import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable";
|
||||||
|
|
||||||
|
const createDockTabStoreInjectable = getInjectable({
|
||||||
|
id: "create-dock-tab-store",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const dependencies = {
|
||||||
|
createStorage: di.inject(createStorageInjectable),
|
||||||
|
};
|
||||||
|
|
||||||
|
return <T>(options: DockTabStoreOptions = {}) => new DockTabStore<T>(dependencies, options);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createDockTabStoreInjectable;
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import { action, observable, reaction } from "mobx";
|
||||||
|
import type { StorageLayer } from "../../../utils/storage-helper";
|
||||||
|
import type { CreateStorage } from "../../../utils/create-storage/create-storage.injectable";
|
||||||
|
import type { TabId } from "../store";
|
||||||
|
import autoBind from "auto-bind";
|
||||||
|
import { toJS } from "../../../../common/utils";
|
||||||
|
|
||||||
|
export interface DockTabStoreOptions {
|
||||||
|
autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true
|
||||||
|
storageKey?: string; // save data to persistent storage under the key
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DockTabStorageState<T> = Record<TabId, T>;
|
||||||
|
|
||||||
|
export interface DockTabStoreDependencies {
|
||||||
|
createStorage: CreateStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DockTabStore<T> {
|
||||||
|
protected readonly storage?: StorageLayer<DockTabStorageState<T>>;
|
||||||
|
|
||||||
|
private readonly data = observable.map<TabId, T>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly dependencies: DockTabStoreDependencies,
|
||||||
|
protected readonly options: DockTabStoreOptions,
|
||||||
|
) {
|
||||||
|
autoBind(this);
|
||||||
|
this.options.autoInit ??= true;
|
||||||
|
|
||||||
|
const { storageKey, autoInit } = this.options;
|
||||||
|
|
||||||
|
if (autoInit && storageKey) {
|
||||||
|
const storage = (this.storage = this.dependencies.createStorage(storageKey, {}));
|
||||||
|
|
||||||
|
this.data.replace(storage.get());
|
||||||
|
reaction(
|
||||||
|
() => this.toJSON(),
|
||||||
|
(data) => storage.set(data),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected finalizeDataForSave(data: T): T {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected toJSON(): DockTabStorageState<T> {
|
||||||
|
const deepCopy = toJS(this.data);
|
||||||
|
|
||||||
|
deepCopy.forEach((tabData, key) => {
|
||||||
|
deepCopy.set(key, this.finalizeDataForSave(tabData));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries<T>(deepCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getAllData() {
|
||||||
|
return this.data.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
findTabIdFromData(inspecter: (val: T) => any): TabId | undefined {
|
||||||
|
for (const [tabId, data] of this.data) {
|
||||||
|
if (inspecter(data)) {
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
isReady(tabId: TabId): boolean {
|
||||||
|
return this.getData(tabId) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
getData(tabId: TabId) {
|
||||||
|
return this.data.get(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(tabId: TabId, data: T) {
|
||||||
|
this.data.set(tabId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearData(tabId: TabId) {
|
||||||
|
this.data.delete(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
reset() {
|
||||||
|
for (const tabId of this.data.keys()) {
|
||||||
|
this.clearData(tabId);
|
||||||
|
}
|
||||||
|
this.storage?.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
93
packages/business-features/dock-old/dock-tab.module.scss
Normal file
93
packages/business-features/dock-old/dock-tab.module.scss
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
.DockTab {
|
||||||
|
--color-active: var(--dockTabActiveBackground);
|
||||||
|
--color-text-active: var(--textColorAccent);
|
||||||
|
--color-border-active: var(--primary);
|
||||||
|
|
||||||
|
padding: var(--padding);
|
||||||
|
height: 32px;
|
||||||
|
position: relative;
|
||||||
|
border-right: 1px solid var(--dockTabBorderColor);
|
||||||
|
background-size: 1px 3ch;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
/* Allow tabs to shrink and take all parent space */
|
||||||
|
min-width: var(--min-tab-width);
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
max-width: fit-content;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pinned {
|
||||||
|
padding-right: var(--padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-right: var(--padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.active) {
|
||||||
|
background-color: var(--color-active);
|
||||||
|
background-image: none;
|
||||||
|
border-bottom: 1px solid var(--color-border-active);
|
||||||
|
color: var(--color-text-active)!important;
|
||||||
|
|
||||||
|
.close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(90deg, transparent 0%, var(--dockHeadBackground) 65%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:global(.active)):hover {
|
||||||
|
background-color: var(--dockTabActiveBackground);
|
||||||
|
background-image: none;
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
|
||||||
|
.close {
|
||||||
|
opacity: 1;
|
||||||
|
background: linear-gradient(90deg, transparent 0%, var(--dockTabActiveBackground) 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
width: 4ch;
|
||||||
|
opacity: 0;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(90deg, transparent 0%, var(--color-active) 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabIcon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-right: calc(var(--margin) * 3);
|
||||||
|
}
|
||||||
128
packages/business-features/dock-old/dock-tab.tsx
Normal file
128
packages/business-features/dock-old/dock-tab.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import styles from "./dock-tab.module.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { cssNames, prevDefault, isMiddleClick } from "@k8slens/utilities";
|
||||||
|
import type { DockStore, DockTab as DockTabModel } from "./store";
|
||||||
|
import type { TabProps } from "../tabs";
|
||||||
|
import { Tab } from "../tabs";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { Menu, MenuItem } from "../menu";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import dockStoreInjectable from "./store.injectable";
|
||||||
|
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||||
|
import isMacInjectable from "../../../common/vars/is-mac.injectable";
|
||||||
|
import autoBindReact from "auto-bind/react";
|
||||||
|
|
||||||
|
export interface DockTabProps extends TabProps<DockTabModel> {
|
||||||
|
moreActions?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
dockStore: DockStore;
|
||||||
|
isMac: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class NonInjectedDockTab extends React.Component<DockTabProps & Dependencies> {
|
||||||
|
private readonly menuVisible = observable.box(false);
|
||||||
|
|
||||||
|
constructor(props: DockTabProps & Dependencies) {
|
||||||
|
super(props);
|
||||||
|
autoBindReact(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(id: string) {
|
||||||
|
this.props.dockStore.closeTab(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMenu(tabId: string) {
|
||||||
|
const { closeTab, closeAllTabs, closeOtherTabs, closeTabsToTheRight, tabs, getTabIndex } =
|
||||||
|
this.props.dockStore;
|
||||||
|
const closeAllDisabled = tabs.length === 1;
|
||||||
|
const closeOtherDisabled = tabs.length === 1;
|
||||||
|
const closeRightDisabled = getTabIndex(tabId) === tabs.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
usePortal
|
||||||
|
htmlFor={`tab-${tabId}`}
|
||||||
|
className="DockTabMenu"
|
||||||
|
isOpen={this.menuVisible.get()}
|
||||||
|
open={() => this.menuVisible.set(true)}
|
||||||
|
close={() => this.menuVisible.set(false)}
|
||||||
|
toggleEvent="contextmenu"
|
||||||
|
>
|
||||||
|
<MenuItem onClick={() => closeTab(tabId)}>Close</MenuItem>
|
||||||
|
<MenuItem onClick={() => closeAllTabs()} disabled={closeAllDisabled}>
|
||||||
|
Close all tabs
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => closeOtherTabs(tabId)} disabled={closeOtherDisabled}>
|
||||||
|
Close other tabs
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => closeTabsToTheRight(tabId)} disabled={closeRightDisabled}>
|
||||||
|
Close tabs to the right
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, moreActions, dockStore, active, isMac, ...tabProps } = this.props;
|
||||||
|
|
||||||
|
if (!tabProps.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, pinned, id } = tabProps.value;
|
||||||
|
const close = prevDefault(() => this.close(id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tab
|
||||||
|
{...tabProps}
|
||||||
|
id={`tab-${id}`}
|
||||||
|
className={cssNames(styles.DockTab, className, {
|
||||||
|
[styles.pinned]: pinned,
|
||||||
|
})}
|
||||||
|
onContextMenu={() => this.menuVisible.set(true)}
|
||||||
|
label={
|
||||||
|
<div className="flex align-center" onAuxClick={isMiddleClick(close)}>
|
||||||
|
<span className={styles.title}>{title}</span>
|
||||||
|
{moreActions}
|
||||||
|
{!pinned && (
|
||||||
|
<div className={styles.close}>
|
||||||
|
<Icon
|
||||||
|
small
|
||||||
|
material="close"
|
||||||
|
tooltip={`Close ${this.props.isMac ? "⌘+W" : "Ctrl+W"}`}
|
||||||
|
onClick={close}
|
||||||
|
data-testid={`dock-tab-close-for-${id}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Tooltip
|
||||||
|
targetId={`tab-${id}`}
|
||||||
|
preferredPositions={[TooltipPosition.TOP, TooltipPosition.TOP_LEFT]}
|
||||||
|
style={{ transitionDelay: "700ms" }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
data-testid={`dock-tab-for-${id}`}
|
||||||
|
/>
|
||||||
|
{this.renderMenu(id)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DockTab = withInjectables<Dependencies, DockTabProps>(NonInjectedDockTab, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
dockStore: di.inject(dockStoreInjectable),
|
||||||
|
isMac: di.inject(isMacInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
55
packages/business-features/dock-old/dock-tabs.module.scss
Normal file
55
packages/business-features/dock-old/dock-tabs.module.scss
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
.dockTabs {
|
||||||
|
--min-tab-width: 120px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.scrollable) {
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: overlay; /* Set scrollbar inside content area */
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 0;
|
||||||
|
height: 3px;
|
||||||
|
background-color: rgba(106, 115, 125, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
content: "\00A0";
|
||||||
|
position: sticky;
|
||||||
|
min-width: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(270deg, transparent 0%, var(--dockHeadBackground) 65%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(90deg, transparent 0%, var(--dockHeadBackground) 65%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
packages/business-features/dock-old/dock-tabs.tsx
Normal file
81
packages/business-features/dock-old/dock-tabs.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import React, { Fragment, useEffect, useRef, useState } from "react";
|
||||||
|
import { Tabs } from "../tabs/tabs";
|
||||||
|
import type { DockTab as DockTabModel } from "./store";
|
||||||
|
import { cssVar } from "@k8slens/utilities";
|
||||||
|
import { useResizeObserver } from "../../hooks";
|
||||||
|
|
||||||
|
export interface DockTabsProps {
|
||||||
|
tabs: DockTabModel[];
|
||||||
|
autoFocus: boolean;
|
||||||
|
selectedTab: DockTabModel | undefined;
|
||||||
|
onChangeTab: (tab: DockTabModel) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DockTabs = ({ tabs, autoFocus, selectedTab, onChangeTab }: DockTabsProps) => {
|
||||||
|
const elem = useRef<HTMLDivElement | null>(null);
|
||||||
|
const minTabSize = useRef<number>(0);
|
||||||
|
const [showScrollbar, setShowScrollbar] = useState(false);
|
||||||
|
|
||||||
|
const getTabElements = (): HTMLDivElement[] => {
|
||||||
|
return Array.from(elem.current?.querySelectorAll(".Tabs .Tab") ?? []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTab = (tab?: DockTabModel) => {
|
||||||
|
if (!tab) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (tab.kind) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollActiveTabIntoView = () => {
|
||||||
|
const tab = elem.current?.querySelector(".Tab.active");
|
||||||
|
|
||||||
|
tab?.scrollIntoView();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateScrollbarVisibility = () => {
|
||||||
|
const allTabsShrunk = getTabElements().every((tab) => tab.offsetWidth == minTabSize.current);
|
||||||
|
|
||||||
|
setShowScrollbar(allTabsShrunk);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollTabsWithMouseWheel = (left: number) => {
|
||||||
|
elem.current?.children[0]?.scrollBy({ left });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseWheel = (event: React.WheelEvent) => {
|
||||||
|
scrollTabsWithMouseWheel(event.deltaY);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (elem.current) {
|
||||||
|
const cssVars = cssVar(elem.current);
|
||||||
|
|
||||||
|
minTabSize.current = cssVars.get("--min-tab-width").valueOf();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useResizeObserver(elem.current, () => {
|
||||||
|
scrollActiveTabIntoView();
|
||||||
|
updateScrollbarVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.dockTabs} ref={elem} role="tablist">
|
||||||
|
<Tabs
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
value={selectedTab}
|
||||||
|
onChange={onChangeTab}
|
||||||
|
onWheel={onMouseWheel}
|
||||||
|
scrollable={showScrollbar}
|
||||||
|
className={styles.tabs}
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Fragment key={tab.id}>{renderTab(tab)}</Fragment>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
150
packages/business-features/dock-old/dock.tsx
Normal file
150
packages/business-features/dock-old/dock.tsx
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import "../dock/src/dock/dock.scss";
|
||||||
|
import React from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { cssNames } from "@k8slens/utilities";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { MenuActions } from "../menu/menu-actions";
|
||||||
|
import { ResizeDirection, ResizingAnchor } from "../resizing-anchor";
|
||||||
|
import { DockTabs } from "./dock-tabs";
|
||||||
|
import type { DockStore, DockTab } from "./store";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import dockStoreInjectable from "./store.injectable";
|
||||||
|
import { ErrorBoundary } from "../error-boundary";
|
||||||
|
|
||||||
|
export interface DockProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
dockStore: DockStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Direction {
|
||||||
|
NEXT = 1,
|
||||||
|
PREV = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class NonInjectedDock extends React.Component<DockProps & Dependencies> {
|
||||||
|
private readonly element = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
onChangeTab = (tab: DockTab) => {
|
||||||
|
const { open, selectTab } = this.props.dockStore;
|
||||||
|
|
||||||
|
open();
|
||||||
|
selectTab(tab.id);
|
||||||
|
this.element.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
switchToNextTab = (selectedTab: DockTab, direction: Direction) => {
|
||||||
|
const { tabs } = this.props.dockStore;
|
||||||
|
const currentIndex = tabs.indexOf(selectedTab);
|
||||||
|
const nextIndex = currentIndex + direction;
|
||||||
|
|
||||||
|
// check if moving to the next or previous tab is possible.
|
||||||
|
if (nextIndex >= tabs.length || nextIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextElement = tabs[nextIndex];
|
||||||
|
|
||||||
|
this.onChangeTab(nextElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTab(tab: DockTab) {
|
||||||
|
switch (tab.kind) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTabContent() {
|
||||||
|
const { isOpen, height, selectedTab } = this.props.dockStore;
|
||||||
|
|
||||||
|
if (!isOpen || !selectedTab) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`tab-content ${selectedTab.kind}`}
|
||||||
|
style={{ flexBasis: height }}
|
||||||
|
data-testid={`dock-tab-content-for-${selectedTab.id}`}
|
||||||
|
>
|
||||||
|
{this.renderTab(selectedTab)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, dockStore } = this.props;
|
||||||
|
const { isOpen, toggle, tabs, toggleFillSize, selectedTab, hasTabs, fullSize } =
|
||||||
|
this.props.dockStore;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cssNames("Dock", className, { isOpen, fullSize })}
|
||||||
|
ref={this.element}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<ResizingAnchor
|
||||||
|
disabled={!hasTabs()}
|
||||||
|
getCurrentExtent={() => dockStore.height}
|
||||||
|
minExtent={dockStore.minHeight}
|
||||||
|
maxExtent={dockStore.maxHeight}
|
||||||
|
direction={ResizeDirection.VERTICAL}
|
||||||
|
onStart={dockStore.open}
|
||||||
|
onMinExtentSubceed={dockStore.close}
|
||||||
|
onMinExtentExceed={dockStore.open}
|
||||||
|
onDrag={(extent) => (dockStore.height = extent)}
|
||||||
|
/>
|
||||||
|
<div className="tabs-container flex align-center">
|
||||||
|
<DockTabs
|
||||||
|
tabs={tabs}
|
||||||
|
selectedTab={selectedTab}
|
||||||
|
autoFocus={isOpen}
|
||||||
|
onChangeTab={this.onChangeTab}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cssNames("toolbar flex gaps align-center box grow", {
|
||||||
|
"pl-0": tabs.length == 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="dock-menu box grow">
|
||||||
|
<MenuActions
|
||||||
|
id="menu-actions-for-dock"
|
||||||
|
usePortal
|
||||||
|
triggerIcon={{ material: "add", className: "new-dock-tab", tooltip: "New tab" }}
|
||||||
|
closeOnScroll={false}
|
||||||
|
></MenuActions>
|
||||||
|
</div>
|
||||||
|
{hasTabs() && (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
material={fullSize ? "fullscreen_exit" : "fullscreen"}
|
||||||
|
tooltip={fullSize ? "Exit full size mode" : "Fit to window"}
|
||||||
|
onClick={toggleFillSize}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
material={`keyboard_arrow_${isOpen ? "down" : "up"}`}
|
||||||
|
tooltip={isOpen ? "Minimize" : "Open"}
|
||||||
|
onClick={toggle}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ErrorBoundary>{this.renderTabContent()}</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dock = withInjectables<Dependencies, DockProps>(
|
||||||
|
NonInjectedDock,
|
||||||
|
|
||||||
|
{
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
dockStore: di.inject(dockStoreInjectable),
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { keyboardShortcutInjectionToken } from "@k8slens/keyboard-shortcuts";
|
||||||
|
|
||||||
|
const switchToNextTabShortcutInjectable = getInjectable({
|
||||||
|
id: "switch-to-next-tab-shortcut",
|
||||||
|
|
||||||
|
instantiate: () => ({
|
||||||
|
scope: "dock",
|
||||||
|
|
||||||
|
binding: {
|
||||||
|
ctrl: true,
|
||||||
|
code: "Period",
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke: () => {
|
||||||
|
// Next tab
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: keyboardShortcutInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default switchToNextTabShortcutInjectable;
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { keyboardShortcutInjectionToken } from "@k8slens/keyboard-shortcuts";
|
||||||
|
|
||||||
|
const switchToPreviousTabShortcutInjectable = getInjectable({
|
||||||
|
id: "switch-to-previous-tab-shortcut",
|
||||||
|
|
||||||
|
instantiate: () => ({
|
||||||
|
scope: "dock",
|
||||||
|
|
||||||
|
binding: {
|
||||||
|
ctrl: true,
|
||||||
|
code: "Comma",
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke: () => {
|
||||||
|
// Previous tab
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
injectionToken: keyboardShortcutInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default switchToPreviousTabShortcutInjectable;
|
||||||
17
packages/business-features/dock-old/rename-tab.injectable.ts
Normal file
17
packages/business-features/dock-old/rename-tab.injectable.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import dockStoreInjectable from "./store.injectable";
|
||||||
|
import type { TabId } from "./store";
|
||||||
|
|
||||||
|
const renameTabInjectable = getInjectable({
|
||||||
|
id: "rename-tab",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const dockStore = di.inject(dockStoreInjectable);
|
||||||
|
|
||||||
|
return (tabId: TabId, title: string): void => {
|
||||||
|
dockStore.renameTab(tabId, title);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default renameTabInjectable;
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import type { TabId } from "./store";
|
||||||
|
import dockStoreInjectable from "./store.injectable";
|
||||||
|
|
||||||
|
const selectDockTabInjectable = getInjectable({
|
||||||
|
id: "select-dock-tab",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const dockStore = di.inject(dockStoreInjectable);
|
||||||
|
|
||||||
|
return (tabId: TabId): void => {
|
||||||
|
dockStore.selectTab(tabId);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default selectDockTabInjectable;
|
||||||
25
packages/business-features/dock-old/store.injectable.ts
Normal file
25
packages/business-features/dock-old/store.injectable.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { DockStore } from "./store";
|
||||||
|
import dockStorageInjectable from "./dock-storage.injectable";
|
||||||
|
|
||||||
|
const dockStoreInjectable = getInjectable({
|
||||||
|
id: "dock-store",
|
||||||
|
|
||||||
|
instantiate: (di) =>
|
||||||
|
new DockStore({
|
||||||
|
storage: di.inject(dockStorageInjectable),
|
||||||
|
tabDataClearers: {
|
||||||
|
// [TabKind.POD_LOGS]: di.inject(clearLogTabDataInjectable),
|
||||||
|
// [TabKind.UPGRADE_CHART]: di.inject(clearUpgradeChartTabDataInjectable),
|
||||||
|
// [TabKind.CREATE_RESOURCE]: di.inject(clearCreateResourceTabDataInjectable),
|
||||||
|
// [TabKind.EDIT_RESOURCE]: di.inject(clearEditResourceTabDataInjectable),
|
||||||
|
// [TabKind.INSTALL_CHART]: di.inject(clearInstallChartTabDataInjectable),
|
||||||
|
// [TabKind.TERMINAL]: di.inject(clearTerminalTabDataInjectable),
|
||||||
|
},
|
||||||
|
tabDataValidator: {
|
||||||
|
// [TabKind.POD_LOGS]: di.inject(isLogsTabDataValidInjectable),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default dockStoreInjectable;
|
||||||
406
packages/business-features/dock-old/store.ts
Normal file
406
packages/business-features/dock-old/store.ts
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
import * as uuid from "uuid";
|
||||||
|
import {
|
||||||
|
action,
|
||||||
|
comparer,
|
||||||
|
computed,
|
||||||
|
makeObservable,
|
||||||
|
observable,
|
||||||
|
reaction,
|
||||||
|
runInAction,
|
||||||
|
} from "mobx";
|
||||||
|
import throttle from "lodash/throttle";
|
||||||
|
import type { StorageLayer } from "../../../utils/storage-helper";
|
||||||
|
import autoBind from "auto-bind";
|
||||||
|
|
||||||
|
export type TabId = string;
|
||||||
|
export type TabKind = string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the storage model for dock tabs.
|
||||||
|
*
|
||||||
|
* All fields are required.
|
||||||
|
*/
|
||||||
|
export type DockTab = Required<DockTabCreate>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the arguments for creating a new Tab on the dock
|
||||||
|
*/
|
||||||
|
export interface DockTabCreate {
|
||||||
|
/**
|
||||||
|
* The ID of the tab for reference purposes.
|
||||||
|
*/
|
||||||
|
id?: TabId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What kind of dock tab it is
|
||||||
|
*/
|
||||||
|
kind: TabKind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tab's title, defaults to `kind`
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true then the dock entry will take up the whole view and will not be
|
||||||
|
* closable.
|
||||||
|
*/
|
||||||
|
pinned?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra fields are supported.
|
||||||
|
*/
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This type is for function which specifically create a single type of dock tab.
|
||||||
|
*
|
||||||
|
* That way users should get a type error if they try and specify a `kind`
|
||||||
|
* themselves.
|
||||||
|
*/
|
||||||
|
export type DockTabCreateSpecific = Omit<DockTabCreate, "kind">;
|
||||||
|
|
||||||
|
export interface DockStorageState {
|
||||||
|
height: number;
|
||||||
|
tabs: DockTab[];
|
||||||
|
selectedTabId?: TabId;
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DockTabChangeEvent {
|
||||||
|
tab: DockTab;
|
||||||
|
tabId: TabId;
|
||||||
|
prevTab?: DockTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DockTabChangeEventOptions {
|
||||||
|
/**
|
||||||
|
* apply a callback right after initialization
|
||||||
|
*/
|
||||||
|
fireImmediately?: boolean;
|
||||||
|
/**
|
||||||
|
* filter: by dockStore.selectedTab.kind == tabKind
|
||||||
|
*/
|
||||||
|
tabKind?: TabKind;
|
||||||
|
/**
|
||||||
|
* filter: dock and selected tab should be visible (default: true)
|
||||||
|
*/
|
||||||
|
dockIsVisible?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DockTabCloseEvent {
|
||||||
|
tabId: TabId; // closed tab id
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
readonly storage: StorageLayer<DockStorageState>;
|
||||||
|
readonly tabDataClearers: Record<TabKind, (tabId: TabId) => void>;
|
||||||
|
readonly tabDataValidator: Partial<Record<TabKind, (tabId: TabId) => boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DockStore implements DockStorageState {
|
||||||
|
constructor(private readonly dependencies: Dependencies) {
|
||||||
|
makeObservable(this);
|
||||||
|
autoBind(this);
|
||||||
|
|
||||||
|
// adjust terminal height if window size changes
|
||||||
|
window.addEventListener("resize", throttle(this.adjustHeight, 250));
|
||||||
|
|
||||||
|
for (const tab of this.tabs) {
|
||||||
|
const tabDataIsValid = this.dependencies.tabDataValidator[tab.kind] ?? (() => true);
|
||||||
|
|
||||||
|
if (!tabDataIsValid(tab.id)) {
|
||||||
|
this.closeTab(tab.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly minHeight = 100;
|
||||||
|
|
||||||
|
@observable fullSize = false;
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get isOpen(): boolean {
|
||||||
|
return this.dependencies.storage.get().isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
set isOpen(isOpen: boolean) {
|
||||||
|
this.dependencies.storage.merge({ isOpen });
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get height(): number {
|
||||||
|
return this.dependencies.storage.get().height;
|
||||||
|
}
|
||||||
|
|
||||||
|
set height(height: number) {
|
||||||
|
this.dependencies.storage.merge({
|
||||||
|
height: Math.max(this.minHeight, Math.min(height || this.minHeight, this.maxHeight)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get tabs(): DockTab[] {
|
||||||
|
return this.dependencies.storage.get().tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
set tabs(tabs: DockTab[]) {
|
||||||
|
this.dependencies.storage.merge({ tabs });
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get selectedTabId(): TabId | undefined {
|
||||||
|
const storageData = this.dependencies.storage.get();
|
||||||
|
|
||||||
|
return storageData.selectedTabId || (this.tabs.length > 0 ? this.tabs[0]?.id : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectedTabId(tabId: TabId | undefined) {
|
||||||
|
if (tabId && !this.getTabById(tabId)) {
|
||||||
|
return;
|
||||||
|
} // skip invalid ids
|
||||||
|
|
||||||
|
this.dependencies.storage.merge({ selectedTabId: tabId });
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get tabsNumber(): number {
|
||||||
|
return this.tabs.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get selectedTab() {
|
||||||
|
return this.tabs.find((tab) => tab.id === this.selectedTabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxHeight() {
|
||||||
|
const mainLayoutHeader = 40;
|
||||||
|
const mainLayoutTabs = 33;
|
||||||
|
const mainLayoutMargin = 16;
|
||||||
|
const dockTabs = 33;
|
||||||
|
const preferredMax =
|
||||||
|
window.innerHeight - mainLayoutHeader - mainLayoutTabs - mainLayoutMargin - dockTabs;
|
||||||
|
|
||||||
|
return Math.max(preferredMax, this.minHeight); // don't let max < min
|
||||||
|
}
|
||||||
|
|
||||||
|
protected adjustHeight() {
|
||||||
|
if (this.height < this.minHeight) {
|
||||||
|
this.height = this.minHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.height > this.maxHeight) {
|
||||||
|
this.height = this.maxHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(callback: () => void, opts: { fireImmediately?: boolean } = {}) {
|
||||||
|
return reaction(() => [this.height, this.fullSize], callback, {
|
||||||
|
fireImmediately: opts.fireImmediately,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabClose(callback: (evt: DockTabCloseEvent) => void, opts: { fireImmediately?: boolean } = {}) {
|
||||||
|
return reaction(
|
||||||
|
() => this.tabs.map((tab) => tab.id),
|
||||||
|
(tabs: TabId[], prevTabs?: TabId[]) => {
|
||||||
|
if (!Array.isArray(prevTabs)) {
|
||||||
|
return; // tabs not yet modified
|
||||||
|
}
|
||||||
|
|
||||||
|
const closedTabs: TabId[] = prevTabs.filter((id) => !tabs.includes(id));
|
||||||
|
|
||||||
|
if (closedTabs.length > 0) {
|
||||||
|
runInAction(() => {
|
||||||
|
closedTabs.forEach((tabId) => callback({ tabId }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
equals: comparer.structural,
|
||||||
|
fireImmediately: opts.fireImmediately,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabChange(
|
||||||
|
callback: (evt: DockTabChangeEvent) => void,
|
||||||
|
options: DockTabChangeEventOptions = {},
|
||||||
|
) {
|
||||||
|
const { tabKind, dockIsVisible = true, ...reactionOpts } = options;
|
||||||
|
|
||||||
|
return reaction(
|
||||||
|
() => this.selectedTab,
|
||||||
|
(tab, prevTab) => {
|
||||||
|
if (!tab) {
|
||||||
|
return;
|
||||||
|
} // skip when dock is empty
|
||||||
|
|
||||||
|
if (tabKind && tabKind !== tab.kind) {
|
||||||
|
return;
|
||||||
|
} // handle specific tab.kind only
|
||||||
|
|
||||||
|
if (dockIsVisible && !this.isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
tab,
|
||||||
|
prevTab,
|
||||||
|
tabId: tab.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reactionOpts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTabs() {
|
||||||
|
return this.tabs.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
open(fullSize?: boolean) {
|
||||||
|
this.isOpen = true;
|
||||||
|
|
||||||
|
if (typeof fullSize === "boolean") {
|
||||||
|
this.fullSize = fullSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
close() {
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggle() {
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.close();
|
||||||
|
} else {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleFillSize() {
|
||||||
|
if (!this.isOpen) {
|
||||||
|
this.open();
|
||||||
|
}
|
||||||
|
this.fullSize = !this.fullSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTabById(tabId: TabId) {
|
||||||
|
return this.tabs.find((tab) => tab.id === tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTabIndex(tabId: TabId) {
|
||||||
|
return this.tabs.findIndex((tab) => tab.id === tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getNewTabNumber(kind: TabKind) {
|
||||||
|
const tabNumbers = this.tabs
|
||||||
|
.filter((tab) => tab.kind === kind)
|
||||||
|
.map((tab) => {
|
||||||
|
const tabNumber = Number(tab.title.match(/\d+/));
|
||||||
|
|
||||||
|
return tabNumber === 0 ? 1 : tabNumber; // tab without a number is first
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 1; ; i++) {
|
||||||
|
if (!tabNumbers.includes(i)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTab = action((rawTabDesc: DockTabCreate, addNumber = true): DockTab => {
|
||||||
|
const { id = uuid.v4(), kind, pinned = false, ...restOfTabFields } = rawTabDesc;
|
||||||
|
let { title = kind } = rawTabDesc;
|
||||||
|
|
||||||
|
if (addNumber) {
|
||||||
|
const tabNumber = this.getNewTabNumber(kind);
|
||||||
|
|
||||||
|
if (tabNumber > 1) {
|
||||||
|
title += ` (${tabNumber})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab: DockTab = {
|
||||||
|
...restOfTabFields,
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
pinned,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tabs.push(tab);
|
||||||
|
this.selectTab(tab.id);
|
||||||
|
this.open();
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
|
||||||
|
@action
|
||||||
|
closeTab(tabId: TabId) {
|
||||||
|
const tab = this.getTabById(tabId);
|
||||||
|
const tabIndex = this.getTabIndex(tabId);
|
||||||
|
|
||||||
|
if (!tab || tab.pinned) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tabs = this.tabs.filter((tab) => tab.id !== tabId);
|
||||||
|
this.dependencies.tabDataClearers[tab.kind](tab.id);
|
||||||
|
|
||||||
|
if (this.selectedTabId === tab.id) {
|
||||||
|
if (this.tabs.length) {
|
||||||
|
const newTab = tabIndex < this.tabsNumber ? this.tabs[tabIndex] : this.tabs[tabIndex - 1];
|
||||||
|
|
||||||
|
this.selectTab(newTab.id);
|
||||||
|
} else {
|
||||||
|
this.selectedTabId = undefined;
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
closeTabs(tabs: DockTab[]) {
|
||||||
|
tabs.forEach((tab) => this.closeTab(tab.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllTabs() {
|
||||||
|
this.closeTabs([...this.tabs]);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOtherTabs(tabId: TabId) {
|
||||||
|
const index = this.getTabIndex(tabId);
|
||||||
|
const tabs = [...this.tabs.slice(0, index), ...this.tabs.slice(index + 1)];
|
||||||
|
|
||||||
|
this.closeTabs(tabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTabsToTheRight(tabId: TabId) {
|
||||||
|
const index = this.getTabIndex(tabId);
|
||||||
|
const tabs = this.tabs.slice(index + 1);
|
||||||
|
|
||||||
|
this.closeTabs(tabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
renameTab(tabId: TabId, title: string) {
|
||||||
|
const tab = this.getTabById(tabId);
|
||||||
|
|
||||||
|
if (tab) {
|
||||||
|
tab.title = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
selectTab(tabId: TabId) {
|
||||||
|
this.selectedTabId = this.getTabById(tabId)?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
reset() {
|
||||||
|
this.dependencies.storage?.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForNode;
|
module.exports = require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
"description": "Highly extendable dock in the Lens.",
|
"description": "Highly extendable dock in the Lens.",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"files": [
|
"files": [
|
||||||
"agnostic/dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
@ -15,7 +15,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/lensapp/lens.git"
|
"url": "git+https://github.com/lensapp/lens.git"
|
||||||
},
|
},
|
||||||
"main": "agnostic/dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "OpenLens Authors",
|
"name": "OpenLens Authors",
|
||||||
@ -24,8 +24,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"buildasd": "webpack",
|
||||||
"dev": "webpack --mode=development --watch",
|
"devasdasd": "webpack --mode=development --watch",
|
||||||
"test:unit": "jest --coverage --runInBand",
|
"test:unit": "jest --coverage --runInBand",
|
||||||
"lint": "lens-lint",
|
"lint": "lens-lint",
|
||||||
"lint:fix": "lens-lint --fix"
|
"lint:fix": "lens-lint --fix"
|
||||||
@ -33,10 +33,13 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@k8slens/feature-core": "^6.5.0-alpha.0",
|
"@k8slens/feature-core": "^6.5.0-alpha.0",
|
||||||
"@ogre-tools/injectable": "^15.1.2",
|
"@ogre-tools/injectable": "^15.1.2",
|
||||||
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2"
|
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
|
||||||
|
"@ogre-tools/fp": "^15.1.2",
|
||||||
|
"lodash": "^4.17.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@async-fn/jest": "^1.6.4",
|
"@async-fn/jest": "^1.6.4",
|
||||||
"@k8slens/eslint-config": "6.5.0-alpha.1"
|
"@k8slens/eslint-config": "6.5.0-alpha.1",
|
||||||
|
"@k8slens/react-testing-library-discovery": "^1.0.0-alpha.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
77
packages/business-features/dock/src/dock/dock.scss
Normal file
77
packages/business-features/dock/src/dock/dock.scss
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
.Dock {
|
||||||
|
position: relative;
|
||||||
|
background: var(--dockHeadBackground);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:not(:focus-within) .DockTab.active {
|
||||||
|
&::after {
|
||||||
|
color: var(--halfGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
color: var(--line-color-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isOpen {
|
||||||
|
&.fullSize {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.isOpen) {
|
||||||
|
height: auto !important;
|
||||||
|
|
||||||
|
.Tab {
|
||||||
|
--color-active: var(--colorVague);
|
||||||
|
--color-text-active: inherit;
|
||||||
|
--color-border-active: transparent;
|
||||||
|
|
||||||
|
&:not(:focus):after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-container {
|
||||||
|
padding: 0 $padding * 2;
|
||||||
|
border-top: 1px solid var(--borderColor);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.Tabs:empty + .toolbar {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
min-height: $unit * 4;
|
||||||
|
padding-left: $padding;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.pl-0 {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: flex-basis 25ms ease-in;
|
||||||
|
background: var(--dockInfoBackground);
|
||||||
|
|
||||||
|
> *:not(.Spinner) {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "@k8slens/typescript/config/base.json",
|
"extends": "@k8slens/typescript/config/base.json",
|
||||||
"include": ["**/*.ts", "**/*.tsx"]
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
module.exports = require("@k8slens/webpack").configForNode;
|
module.exports = require("@k8slens/webpack").configForReact;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user