Merge remote-tracking branch 'origin/master' into eliminate-gst-from-app-paths

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen committed 2022-01-05 15:04:43 +02:00
commit 4224937260
52 files changed
+1168 -409

No files matched your search

-3
View File
@@ -107,9 +107,6 @@ export async function bootstrap(di: DependencyInjectionContainer) {
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
initializers.initKubeObjectDetailRegistry();
logger.info(`${logPrefix} initializing WelcomeMenuRegistry`);
initializers.initWelcomeMenuRegistry();
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
initializers.initWorkloadsOverviewDetailRegistry();
@@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store";
import { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { isWindows } from "../../../common/vars";
import { FormSwitch, Switcher } from "../switch";
import { Switch } from "../switch";
import moment from "moment-timezone";
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
import { action } from "mobx";
@@ -86,16 +86,12 @@ export const Application = observer(() => {
<section id="terminalSelection">
<SubTitle title="Terminal copy & paste" />
<FormSwitch
label="Copy on select and paste on right-click"
control={
<Switcher
checked={userStore.terminalCopyOnSelect}
onChange={v => userStore.terminalCopyOnSelect = v.target.checked}
name="terminalCopyOnSelect"
/>
}
/>
<Switch
checked={userStore.terminalCopyOnSelect}
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
>
Copy on select and paste on right-click
</Switch>
</section>
<hr/>
@@ -135,16 +131,9 @@ export const Application = observer(() => {
<section id="other">
<SubTitle title="Start-up"/>
<FormSwitch
control={
<Switcher
checked={userStore.openAtLogin}
onChange={v => userStore.openAtLogin = v.target.checked}
name="startup"
/>
}
label="Automatically start Lens on login"
/>
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
Automatically start Lens on login
</Switch>
</section>
<hr />
@@ -21,7 +21,7 @@
import { observer } from "mobx-react";
import React from "react";
import { UserStore } from "../../../common/user-store";
import { FormSwitch, Switcher } from "../switch";
import { Switch } from "../switch";
import { Select } from "../select";
import { SubTitle } from "../layout/sub-title";
import { SubHeader } from "../layout/sub-header";
@@ -45,15 +45,12 @@ export const Editor = observer(() => {
<section>
<div className="flex gaps justify-space-between">
<div className="flex gaps align-center">
<FormSwitch
label={<SubHeader compact>Show minimap</SubHeader>}
control={
<Switcher
checked={editorConfiguration.minimap.enabled}
onChange={(evt, checked) => editorConfiguration.minimap.enabled = checked}
/>
}
/>
<Switch
checked={editorConfiguration.minimap.enabled}
onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
>
Show minimap
</Switch>
</div>
<div className="flex gaps align-center">
<SubHeader compact>Position</SubHeader>
@@ -25,7 +25,7 @@ import { SubTitle } from "../layout/sub-title";
import { UserStore } from "../../../common/user-store";
import { bundledKubectlPath } from "../../../main/kubectl/kubectl";
import { SelectOption, Select } from "../select";
import { FormSwitch, Switcher } from "../switch";
import { Switch } from "../switch";
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
import directoryForBinariesInjectable
from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
@@ -54,16 +54,12 @@ const NonInjectedKubectlBinaries: React.FC<Dependencies> = (({ defaultPathForKub
<>
<section>
<SubTitle title="Kubectl binary download"/>
<FormSwitch
control={
<Switcher
checked={userStore.downloadKubectlBinaries}
onChange={v => userStore.downloadKubectlBinaries = v.target.checked}
name="kubectl-download"
/>
}
label="Download kubectl binaries matching the Kubernetes cluster version"
/>
<Switch
checked={userStore.downloadKubectlBinaries}
onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
>
Download kubectl binaries matching the Kubernetes cluster version
</Switch>
</section>
<section>
+5 -11
View File
@@ -24,10 +24,11 @@ import React from "react";
import { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { SubTitle } from "../layout/sub-title";
import { FormSwitch, Switcher } from "../switch";
import { Switch } from "../switch";
export const LensProxy = observer(() => {
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
const store = UserStore.getInstance();
return (
<section id="proxy">
@@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
<section className="small">
<SubTitle title="Certificate Trust"/>
<FormSwitch
control={
<Switcher
checked={UserStore.getInstance().allowUntrustedCAs}
onChange={v => UserStore.getInstance().allowUntrustedCAs = v.target.checked}
name="startup"
/>
}
label="Allow untrusted Certificate Authorities"
/>
<Switch checked={store.allowUntrustedCAs} onChange={() => store.allowUntrustedCAs = !store.allowUntrustedCAs}>
Allow untrusted Certificate Authorities
</Switch>
<small className="hint">
This will make Lens to trust ANY certificate authority without any validations.{" "}
Needed with some corporate proxies that do certificate re-writing.{" "}
@@ -20,45 +20,55 @@
*/
import React from "react";
import { render, screen } from "@testing-library/react";
import { screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { Welcome } from "../welcome";
import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries";
import { defaultWidth } from "../welcome";
import { defaultWidth, Welcome } from "../welcome";
import { computed } from "mobx";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import type { WelcomeBannerRegistration } from "../welcome-banner-items/welcome-banner-registration";
jest.mock(
"electron",
() => ({
ipcRenderer: {
on: jest.fn(),
},
app: {
getPath: () => "tmp",
},
}),
);
jest.mock("electron", () => ({
ipcRenderer: {
on: jest.fn(),
},
app: {
getPath: () => "tmp",
},
}));
describe("<Welcome/>", () => {
beforeEach(() => {
TopBarRegistry.createInstance();
WelcomeMenuRegistry.createInstance();
WelcomeBannerRegistry.createInstance();
});
let render: DiRender;
let di: ConfigurableDependencyInjectionContainer;
let welcomeBannersStub: WelcomeBannerRegistration[];
afterEach(() => {
TopBarRegistry.resetInstance();
WelcomeMenuRegistry.resetInstance();
WelcomeBannerRegistry.resetInstance();
beforeEach(() => {
di = getDiForUnitTesting();
render = renderFor(di);
welcomeBannersStub = [];
di.override(rendererExtensionsInjectable, () =>
computed(() => [
new TestExtension({
id: "some-id",
welcomeBanners: welcomeBannersStub,
}),
]),
);
});
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
const testId = "testId";
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{
Banner: () => <div data-testid={testId} />,
},
]);
welcomeBannersStub.push({
Banner: () => <div data-testid={testId} />,
});
const { container } = render(<Welcome />);
@@ -67,16 +77,15 @@ describe("<Welcome/>", () => {
});
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{
width: 100,
Banner: () => <div />,
},
{
width: 800,
Banner: () => <div />,
},
]);
welcomeBannersStub.push({
width: 100,
Banner: () => <div />,
});
welcomeBannersStub.push({
width: 800,
Banner: () => <div />,
});
render(<Welcome />);
@@ -92,3 +101,25 @@ describe("<Welcome/>", () => {
});
});
});
class TestExtension extends LensRendererExtension {
constructor({
id,
welcomeBanners,
}: {
id: string;
welcomeBanners: WelcomeBannerRegistration[];
}) {
super({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: { name: id, version: "some-version" },
manifestPath: "irrelevant",
});
this.welcomeBanners = welcomeBanners;
}
}
@@ -0,0 +1,37 @@
/**
* 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.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import { computed } from "mobx";
const welcomeBannerItemsInjectable = getInjectable({
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
return computed(() => [
...extensions.get().flatMap((extension) => extension.welcomeBanners),
]);
},
lifecycle: lifecycleEnum.singleton,
});
export default welcomeBannerItemsInjectable;
@@ -19,17 +19,17 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { catalogURL } from "../../common/routes";
import { WelcomeMenuRegistry } from "../../extensions/registries";
import { navigate } from "../navigation";
export function initWelcomeMenuRegistry() {
WelcomeMenuRegistry.getInstance()
.add([
{
title: "Browse Clusters in Catalog",
icon: "view_list",
click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )),
},
]);
/**
* WelcomeBannerRegistration is for an extension to register
* Provide a Banner component to be renderered in the welcome screen.
*/
export interface WelcomeBannerRegistration {
/**
* The banner component to be shown on the welcome screen.
*/
Banner?: React.ComponentType
/**
* The banner width in px.
*/
width?: number
}
@@ -0,0 +1,46 @@
/**
* 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.
*/
import { computed, IComputedValue } from "mobx";
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import { navigate } from "../../../navigation";
import { catalogURL } from "../../../../common/routes";
interface Dependencies {
extensions: IComputedValue<LensRendererExtension[]>;
}
export const getWelcomeMenuItems = ({ extensions }: Dependencies) => {
const browseClusters = {
title: "Browse Clusters in Catalog",
icon: "view_list",
click: () =>
navigate(
catalogURL({
params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" },
}),
),
};
return computed(() => [
browseClusters,
...extensions.get().flatMap((extension) => extension.welcomeMenus),
]);
};
@@ -0,0 +1,34 @@
/**
* 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.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import { getWelcomeMenuItems } from "./get-welcome-menu-items";
const welcomeMenuItemsInjectable = getInjectable({
instantiate: (di) =>
getWelcomeMenuItems({
extensions: di.inject(rendererExtensionsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default welcomeMenuItemsInjectable;
@@ -0,0 +1,26 @@
/**
* 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 interface WelcomeMenuRegistration {
title: string | (() => string);
icon: string;
click: () => void | Promise<void>;
}
+112 -61
View File
@@ -22,78 +22,129 @@
import "./welcome.scss";
import React from "react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import Carousel from "react-material-ui-carousel";
import { Icon } from "../icon";
import { productName, slackUrl } from "../../../common/vars";
import { WelcomeMenuRegistry } from "../../../extensions/registries";
import { WelcomeBannerRegistry } from "../../../extensions/registries";
import { withInjectables } from "@ogre-tools/injectable-react";
import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable";
import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration";
import welcomeBannerItemsInjectable from "./welcome-banner-items/welcome-banner-items.injectable";
import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-banner-registration";
export const defaultWidth = 320;
@observer
export class Welcome extends React.Component {
render() {
const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems();
interface Dependencies {
welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>
welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>
}
// if there is banner with specified width, use it to calculate the width of the container
const maxWidth = welcomeBanner.reduce((acc, curr) => {
const currWidth = curr.width ?? 0;
const NonInjectedWelcome: React.FC<Dependencies> = ({ welcomeMenuItems, welcomeBannerItems }) => {
const welcomeBanners = welcomeBannerItems.get();
if (acc > currWidth) {
return acc;
}
// if there is banner with specified width, use it to calculate the width of the container
const maxWidth = welcomeBanners.reduce((acc, curr) => {
const currWidth = curr.width ?? 0;
return currWidth;
}, defaultWidth);
if (acc > currWidth) {
return acc;
}
return (
<div className="flex justify-center Welcome align-center">
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
{welcomeBanner.length > 0 ? (
<Carousel
stopAutoPlayOnHover={true}
indicators={welcomeBanner.length > 1}
autoPlay={true}
navButtonsAlwaysInvisible={true}
indicatorIconButtonProps={{
style: {
color: "var(--iconActiveBackground)",
},
}}
activeIndicatorIconButtonProps={{
style: {
color: "var(--iconActiveColor)",
},
}}
interval={8000}
return currWidth;
}, defaultWidth);
return (
<div className="flex justify-center Welcome align-center">
<div
style={{ width: `${maxWidth}px` }}
data-testid="welcome-banner-container"
>
{welcomeBanners.length > 0 ? (
<Carousel
stopAutoPlayOnHover={true}
indicators={welcomeBanners.length > 1}
autoPlay={true}
navButtonsAlwaysInvisible={true}
indicatorIconButtonProps={{
style: {
color: "var(--iconActiveBackground)",
},
}}
activeIndicatorIconButtonProps={{
style: {
color: "var(--iconActiveColor)",
},
}}
interval={8000}
>
{welcomeBanners.map((item, index) => (
<item.Banner key={index} />
))}
</Carousel>
) : (
<Icon svg="logo-lens" className="logo" />
)}
<div className="flex justify-center">
<div
style={{ width: `${defaultWidth}px` }}
data-testid="welcome-text-container"
>
<h2>Welcome to {productName} 5!</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our{" "}
<a
href={slackUrl}
target="_blank"
rel="noreferrer"
className="link"
>
Lens Community slack channel
</a>
.
</p>
<ul
className="block"
style={{ width: `${defaultWidth}px` }}
data-testid="welcome-menu-container"
>
{welcomeBanner.map((item, index) =>
<item.Banner key={index} />,
)}
</Carousel>
) : <Icon svg="logo-lens" className="logo" />}
<div className="flex justify-center">
<div style={{ width: `${defaultWidth}px` }} data-testid="welcome-text-container">
<h2>Welcome to {productName} 5!</h2>
<p>
To get you started we have auto-detected your clusters in your kubeconfig file and added them to the catalog, your centralized view for managing all your cloud-native resources.
<br /><br />
If you have any questions or feedback, please join our <a href={slackUrl} target="_blank" rel="noreferrer" className="link">Lens Community slack channel</a>.
</p>
<ul className="block" style={{ width: `${defaultWidth}px` }} data-testid="welcome-menu-container">
{WelcomeMenuRegistry.getInstance().getItems().map((item, index) => (
<li key={index} className="flex grid-12" onClick={() => item.click()}>
<Icon material={item.icon} className="box col-1" /> <a className="box col-10">{typeof item.title === "string" ? item.title : item.title()}</a> <Icon material="navigate_next" className="box col-1" />
</li>
))}
</ul>
</div>
{welcomeMenuItems.get().map((item, index) => (
<li
key={index}
className="flex grid-12"
onClick={() => item.click()}
>
<Icon material={item.icon} className="box col-1" />
<a className="box col-10">
{typeof item.title === "string"
? item.title
: item.title()}
</a>
<Icon material="navigate_next" className="box col-1" />
</li>
))}
</ul>
</div>
</div>
</div>
);
}
}
</div>
);
};
export const Welcome = withInjectables<Dependencies>(
observer(NonInjectedWelcome),
{
getProps: (di) => ({
welcomeMenuItems: di.inject(welcomeMenuItemsInjectable),
welcomeBannerItems: di.inject(welcomeBannerItemsInjectable),
}),
},
);
+9
View File
@@ -91,6 +91,15 @@ html, body {
overflow: hidden;
}
#terminal-init {
position: absolute;
top: 0;
left: 0;
height: 0;
visibility: hidden;
overflow: hidden;
}
#app {
height: 100%;
min-height: 100%;
@@ -39,11 +39,10 @@ import { DeleteClusterDialog } from "../delete-cluster-dialog";
import { reaction } from "mobx";
import { navigation } from "../../navigation";
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
import { TopBar } from "../layout/topbar";
import { catalogURL, getPreviousTabUrl } from "../../../common/routes";
import { withInjectables } from "@ogre-tools/injectable-react";
import catalogPreviousActiveTabStorageInjectable
from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
import { TopBar } from "../layout/top-bar/top-bar";
import catalogPreviousActiveTabStorageInjectable from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable";
interface Dependencies {
catalogPreviousActiveTabStorage: { get: () => string }
@@ -38,17 +38,9 @@ interface Dependencies {
}
export class Terminal {
public static readonly spawningPool = (() => {
// terminal element must be in DOM before attaching via xterm.open(elem)
// https://xtermjs.org/docs/api/terminal/classes/terminal/#open
const pool = document.createElement("div");
pool.className = "terminal-init";
pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden";
document.body.appendChild(pool);
return pool;
})();
public static get spawningPool() {
return document.getElementById("terminal-init");
}
static async preloadFonts() {
const fontPath = require("../../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires
@@ -2,10 +2,6 @@
exports[`kube-object-menu given kube object renders 1`] = `
<body>
<div
class="terminal-init"
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
/>
<div>
<div>
<ul
@@ -22,8 +18,8 @@ exports[`kube-object-menu given kube object renders 1`] = `
>
<i
class="Icon material interactive focusable"
id="tooltip_target_2"
tabindex="0"
tooltip="Delete"
>
<span
class="icon"
@@ -31,7 +27,6 @@ exports[`kube-object-menu given kube object renders 1`] = `
>
delete
</span>
<div />
</i>
<span
class="title"
@@ -46,20 +41,11 @@ exports[`kube-object-menu given kube object renders 1`] = `
class="Animate opacity-scale Dialog flex center ConfirmDialog modal"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
/>
<div
class="Tooltip narrow invisible formatter"
>
Delete
</div>
</body>
`;
exports[`kube-object-menu given kube object when removing kube object renders 1`] = `
<body>
<div
class="terminal-init"
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
/>
<div>
<div>
<ul
@@ -76,8 +62,8 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
>
<i
class="Icon material interactive focusable"
id="tooltip_target_4"
tabindex="0"
tooltip="Delete"
>
<span
class="icon"
@@ -85,7 +71,6 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
>
delete
</span>
<div />
</i>
<span
class="title"
@@ -153,20 +138,11 @@ exports[`kube-object-menu given kube object when removing kube object renders 1`
</div>
</div>
</div>
<div
class="Tooltip narrow invisible formatter"
>
Delete
</div>
</body>
`;
exports[`kube-object-menu given kube object with namespace when removing kube object, renders confirmation dialog with namespace 1`] = `
<body>
<div
class="terminal-init"
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
/>
<div>
<div>
<ul
@@ -183,8 +159,8 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
>
<i
class="Icon material interactive focusable"
id="tooltip_target_12"
tabindex="0"
tooltip="Delete"
>
<span
class="icon"
@@ -192,7 +168,6 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
>
delete
</span>
<div />
</i>
<span
class="title"
@@ -260,20 +235,11 @@ exports[`kube-object-menu given kube object with namespace when removing kube ob
</div>
</div>
</div>
<div
class="Tooltip narrow invisible formatter"
>
Delete
</div>
</body>
`;
exports[`kube-object-menu given kube object without namespace when removing kube object, renders confirmation dialog without namespace 1`] = `
<body>
<div
class="terminal-init"
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
/>
<div>
<div>
<ul
@@ -290,8 +256,8 @@ exports[`kube-object-menu given kube object without namespace when removing kube
>
<i
class="Icon material interactive focusable"
id="tooltip_target_17"
tabindex="0"
tooltip="Delete"
>
<span
class="icon"
@@ -299,7 +265,6 @@ exports[`kube-object-menu given kube object without namespace when removing kube
>
delete
</span>
<div />
</i>
<span
class="title"
@@ -367,20 +332,11 @@ exports[`kube-object-menu given kube object without namespace when removing kube
</div>
</div>
</div>
<div
class="Tooltip narrow invisible formatter"
>
Delete
</div>
</body>
`;
exports[`kube-object-menu given no kube object, renders 1`] = `
<body>
<div
class="terminal-init"
style="position: absolute; top: 0px; left: 0px; height: 0px; visibility: hidden; overflow: hidden;"
/>
<div>
<ul
class="Animate opacity Menu MenuActions flex KubeObjectMenu toolbar gaps right bottom"
@@ -42,6 +42,9 @@ import type { ApiManager } from "../../../common/k8s-api/api-manager";
import apiManagerInjectable from "./dependencies/api-manager.injectable";
import { KubeObjectMenu } from "./index";
// TODO: Make tooltips free of side effects by making it deterministic
jest.mock("../tooltip");
describe("kube-object-menu", () => {
let di: ConfigurableDependencyInjectionContainer;
let render: DiRender;
@@ -66,8 +69,7 @@ describe("kube-object-menu", () => {
getStore: api => undefined,
}) as ApiManager);
di.override(hideDetailsInjectable, () => () => {
});
di.override(hideDetailsInjectable, () => () => {});
di.override(editResourceTabInjectable, () => () => ({
id: "irrelevant",
@@ -0,0 +1,52 @@
/**
* 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.
*/
.closeButton {
width: 35px;
height: 35px;
display: grid;
place-items: center;
cursor: pointer;
border: 2px solid var(--textColorDimmed);
border-radius: 50%;
&:hover {
background-color: #72767d25;
}
&:active {
transform: translateY(1px);
}
}
.icon {
color: var(--textColorAccent);
opacity: 0.6;
}
.esc {
text-align: center;
margin-top: var(--margin);
font-weight: bold;
user-select: none;
color: var(--textColorDimmed);
pointer-events: none;
}
@@ -0,0 +1,41 @@
/**
* 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.
*/
import styles from "./close-button.module.scss";
import React, { HTMLAttributes } from "react";
import { Icon } from "../icon";
interface Props extends HTMLAttributes<HTMLDivElement> {
}
export function CloseButton(props: Props) {
return (
<div {...props}>
<div className={styles.closeButton} role="button" aria-label="Close">
<Icon material="close" className={styles.icon}/>
</div>
<div className={styles.esc} aria-hidden="true">
ESC
</div>
</div>
);
}
@@ -138,41 +138,7 @@
}
> .toolsRegion {
.fixedTools {
position: fixed;
top: 60px;
.closeBtn {
width: 35px;
height: 35px;
display: grid;
place-items: center;
border: 2px solid var(--textColorDimmed);
border-radius: 50%;
cursor: pointer;
&:hover {
background-color: #72767d4d;
}
&:active {
transform: translateY(1px);
}
.Icon {
color: var(--textColorTertiary);
}
}
.esc {
text-align: center;
margin-top: 4px;
font-weight: 600;
font-size: 14px;
color: var(--textColorDimmed);
pointer-events: none;
}
}
width: 45px;
}
}
@@ -25,8 +25,8 @@ import React from "react";
import { observer } from "mobx-react";
import { cssNames, IClassName } from "../../utils";
import { navigation } from "../../navigation";
import { Icon } from "../icon";
import { catalogURL } from "../../../common/routes";
import { CloseButton } from "./close-button";
export interface SettingLayoutProps extends React.DOMAttributes<any> {
className?: IClassName;
@@ -104,13 +104,8 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
<div className="toolsRegion">
{
this.props.provideBackButtonNavigation && (
<div className="fixedTools">
<div className="closeBtn" role="button" aria-label="Close" onClick={back}>
<Icon material="close" />
</div>
<div className="esc" aria-hidden="true">
ESC
</div>
<div className="fixed top-[60px]">
<CloseButton onClick={back}/>
</div>
)
}
@@ -0,0 +1,37 @@
/**
* 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.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import rendererExtensionsInjectable from "../../../../../extensions/renderer-extensions.injectable";
const topBarItemsInjectable = getInjectable({
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
return computed(() =>
extensions.get().flatMap((extension) => extension.topBarItems),
);
},
lifecycle: lifecycleEnum.singleton,
});
export default topBarItemsInjectable;
@@ -0,0 +1,27 @@
/**
* 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.
*/
interface TopBarComponents {
Item: React.ComponentType;
}
export interface TopBarRegistration {
components: TopBarComponents;
}
@@ -20,23 +20,32 @@
*/
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { TopBar } from "../topbar";
import { TopBarRegistry } from "../../../../extensions/registries";
import { TopBar } from "./top-bar";
import { IpcMainWindowEvents } from "../../../../main/window-manager";
import { broadcastMessage } from "../../../../common/ipc";
import * as vars from "../../../../common/vars";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { DiRender, renderFor } from "../../test-utils/renderFor";
import directoryForUserDataInjectable
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs";
const mockConfig = vars as { isWindows: boolean, isLinux: boolean };
const mockConfig = vars as { isWindows: boolean; isLinux: boolean };
jest.mock("../../../../common/ipc");
jest.mock("../../../../common/vars", () => {
const SemVer = require("semver").SemVer;
const versionStub = new SemVer("1.0.0");
return {
__esModule: true,
isWindows: null,
isLinux: null,
appSemVer: versionStub,
};
});
@@ -57,20 +66,30 @@ jest.mock("@electron/remote", () => {
};
});
describe("<Tobar/> in Windows and Linux", () => {
beforeEach(() => {
TopBarRegistry.createInstance();
describe("<TopBar/> in Windows and Linux", () => {
let render: DiRender;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs();
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await di.runSetups();
render = renderFor(di);
});
afterEach(() => {
TopBarRegistry.resetInstance();
mockFs.restore();
});
it("shows window controls on Windows", () => {
mockConfig.isWindows = true;
mockConfig.isLinux = false;
const { getByTestId } = render(<TopBar/>);
const { getByTestId } = render(<TopBar />);
expect(getByTestId("window-menu")).toBeInTheDocument();
expect(getByTestId("window-minimize")).toBeInTheDocument();
@@ -82,7 +101,7 @@ describe("<Tobar/> in Windows and Linux", () => {
mockConfig.isWindows = false;
mockConfig.isLinux = true;
const { getByTestId } = render(<TopBar/>);
const { getByTestId } = render(<TopBar />);
expect(getByTestId("window-menu")).toBeInTheDocument();
expect(getByTestId("window-minimize")).toBeInTheDocument();
@@ -93,7 +112,7 @@ describe("<Tobar/> in Windows and Linux", () => {
it("triggers ipc events on click", () => {
mockConfig.isWindows = true;
const { getByTestId } = render(<TopBar/>);
const { getByTestId } = render(<TopBar />);
const menu = getByTestId("window-menu");
const minimize = getByTestId("window-minimize");
@@ -20,14 +20,26 @@
*/
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { TopBar } from "../topbar";
import { TopBarRegistry } from "../../../../extensions/registries";
import { TopBar } from "./top-bar";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import { DiRender, renderFor } from "../../test-utils/renderFor";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { computed } from "mobx";
import directoryForUserDataInjectable
from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import mockFs from "mock-fs";
jest.mock("../../../../common/vars", () => {
const SemVer = require("semver").SemVer;
const versionStub = new SemVer("1.0.0");
return {
isMac: true,
appSemVer: versionStub,
};
});
@@ -47,9 +59,6 @@ jest.mock(
},
),
},
app: {
getPath: () => "tmp",
},
}),
);
@@ -76,12 +85,23 @@ jest.mock("@electron/remote", () => {
});
describe("<TopBar/>", () => {
beforeEach(() => {
TopBarRegistry.createInstance();
let di: ConfigurableDependencyInjectionContainer;
let render: DiRender;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs();
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await di.runSetups();
render = renderFor(di);
});
afterEach(() => {
TopBarRegistry.resetInstance();
mockFs.restore();
});
it("renders w/o errors", () => {
@@ -129,13 +149,13 @@ describe("<TopBar/>", () => {
const testId = "testId";
const text = "an item";
TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
di.override(topBarItemsInjectable, () => computed(() => [
{
components: {
Item: () => <span data-testid={testId}>{text}</span>,
},
},
]);
]));
const { getByTestId } = render(<TopBar/>);
@@ -19,22 +19,28 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import styles from "./topbar.module.scss";
import styles from "./top-bar.module.scss";
import React, { useEffect, useMemo, useRef } from "react";
import { observer } from "mobx-react";
import { TopBarRegistry } from "../../../extensions/registries";
import { Icon } from "../icon";
import type { IComputedValue } from "mobx";
import { Icon } from "../../icon";
import { webContents, getCurrentWindow } from "@electron/remote";
import { observable } from "mobx";
import { broadcastMessage, ipcRendererOn } from "../../../common/ipc";
import { watchHistoryState } from "../../remote-helpers/history-updater";
import { isActiveRoute, navigate } from "../../navigation";
import { catalogRoute, catalogURL } from "../../../common/routes";
import { IpcMainWindowEvents } from "../../../main/window-manager";
import { isLinux, isWindows } from "../../../common/vars";
import { cssNames } from "../../utils";
import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc";
import { watchHistoryState } from "../../../remote-helpers/history-updater";
import { isActiveRoute, navigate } from "../../../navigation";
import { catalogRoute, catalogURL } from "../../../../common/routes";
import { IpcMainWindowEvents } from "../../../../main/window-manager";
import { isLinux, isWindows } from "../../../../common/vars";
import { cssNames } from "../../../utils";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { TopBarRegistration } from "./top-bar-registration";
interface Props extends React.HTMLAttributes<any> {
interface Props extends React.HTMLAttributes<any> {}
interface Dependencies {
items: IComputedValue<TopBarRegistration[]>;
}
const prevEnabled = observable.box(false);
@@ -48,34 +54,10 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
nextEnabled.set(state);
});
export const TopBar = observer(({ children, ...rest }: Props) => {
const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => {
const elem = useRef<HTMLDivElement>();
const window = useMemo(() => getCurrentWindow(), []);
const renderRegisteredItems = () => {
const items = TopBarRegistry.getInstance().getItems();
if (!Array.isArray(items)) {
return null;
}
return (
<div>
{items.map((registration, index) => {
if (!registration?.components?.Item) {
return null;
}
return (
<div key={index}>
<registration.components.Item />
</div>
);
})}
</div>
);
};
const openContextMenu = () => {
broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU);
};
@@ -156,7 +138,7 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
/>
</div>
<div className={styles.controls}>
{renderRegisteredItems()}
{renderRegisteredItems(items.get())}
{children}
{(isWindows || isLinux) && (
<div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}>
@@ -174,3 +156,29 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
</div>
);
});
const renderRegisteredItems = (items: TopBarRegistration[]) => (
<div>
{items.map((registration, index) => {
if (!registration?.components?.Item) {
return null;
}
return (
<div key={index}>
<registration.components.Item />
</div>
);
})}
</div>
);
export const TopBar = withInjectables(observer(NonInjectedTopBar), {
getProps: (di, props) => ({
items: di.inject(topBarItemsInjectable),
...props,
}),
});
+6 -1
View File
@@ -228,10 +228,15 @@ html {
}
.Select {
&__value-container {
margin-top: 2px;
margin-bottom: 2px;
}
&__control {
box-shadow: 0 0 0 1px var(--inputControlBorder);
background: var(--inputControlBackground);
border-radius: 5px;
border-radius: var(--border-radius);
}
&__single-value {
@@ -0,0 +1,67 @@
/**
* 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.
*/
import React from "react";
import { fireEvent, render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { Switch } from "..";
describe("<Switch/>", () => {
it("renders w/o errors", () => {
const { container } = render(<Switch />);
expect(container).toBeInstanceOf(HTMLElement);
});
it("render label text", () => {
const { getByLabelText } = render(<Switch>Test label</Switch>);
expect(getByLabelText("Test label")).toBeTruthy();
});
it("passes disabled and checked attributes to input", () => {
const { container } = render(<Switch checked disabled/>);
const checkbox = container.querySelector("input[type=checkbox]");
expect(checkbox).toHaveAttribute("disabled");
expect(checkbox).toHaveAttribute("checked");
});
it("onClick event fired", () => {
const onClick = jest.fn();
const { getByTestId } = render(<Switch onClick={onClick}/>);
const switcher = getByTestId("switch");
fireEvent.click(switcher);
expect(onClick).toHaveBeenCalled();
});
it("onClick event not fired for disabled item", () => {
const onClick = jest.fn();
const { getByTestId } = render(<Switch onClick={onClick} disabled/>);
const switcher = getByTestId("switch");
fireEvent.click(switcher);
expect(onClick).not.toHaveBeenCalled();
});
});
@@ -35,6 +35,9 @@ const useStyles = makeStyles({
},
});
/**
* @deprecated Use <Switch/> instead from "../switch.tsx".
*/
export function FormSwitch(props: FormControlLabelProps) {
const classes = useStyles();
+1
View File
@@ -21,3 +21,4 @@
export * from "./switcher";
export * from "./form-switcher";
export * from "./switch";
@@ -0,0 +1,121 @@
/**
* 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.
*/
.Switch {
--thumb-size: 2rem;
--thumb-color: hsl(0 0% 100%);
--thumb-color-highlight: hsl(0 0% 100% / 25%);
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-color-inactive: hsl(80 0% 35%);
--track-color-active: hsl(110, 60%, 60%);
--thumb-position: 0%;
--thumb-transition-duration: .25s;
--hover-highlight-size: 0;
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
cursor: pointer;
user-select: none;
color: var(--textColorAccent);
font-weight: 500;
& > input {
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
appearance: none;
pointer-events: none;
border: none;
outline-offset: 5px;
box-sizing: content-box;
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
transition: background-color .25s ease;
&::before {
content: "";
cursor: pointer;
pointer-events: auto;
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
background: var(--thumb-color);
box-shadow: 0 0 0 var(--hover-highlight-size) var(--thumb-color-highlight);
border-radius: 50%;
transform: translateX(var(--thumb-position));
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}
&:not(:disabled):hover::before {
--hover-highlight-size: .5rem;
}
&:checked {
background: var(--track-color-active);
--thumb-position: 100%;
}
&:disabled {
--track-color-inactive: hsl(80 0% 30%);
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 40%);
}
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--blue);
}
}
&.disabled {
cursor: not-allowed;
}
}
@include theme-light {
.Switch {
--thumb-color-highlight: hsl(0 0% 0% / 25%);
& > input {
&:disabled {
--track-color-inactive: hsl(80 0% 80%);
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
/**
* 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.
*/
import styles from "./switch.module.scss";
import React, { ChangeEvent, HTMLProps } from "react";
import { cssNames } from "../../utils";
interface Props extends Omit<HTMLProps<HTMLInputElement>, "onChange"> {
onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
}
export function Switch({ children, disabled, onChange, ...props }: Props) {
return (
<label className={cssNames(styles.Switch, { [styles.disabled]: disabled })} data-testid="switch">
{children}
<input type="checkbox" role="switch" disabled={disabled} onChange={(event) => onChange?.(props.checked, event)} {...props}/>
</label>
);
}
@@ -31,6 +31,9 @@ interface Props extends SwitchProps {
classes: Styles;
}
/**
* @deprecated Use <Switch/> instead from "../switch.tsx".
*/
export const Switcher = withStyles((theme: Theme) =>
createStyles({
root: {
-1
View File
@@ -27,7 +27,6 @@ export * from "./ipc";
export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry";
export * from "./registries";
export * from "./welcome-menu-registry";
export * from "./workloads-overview-detail-registry";
export * from "./catalog-category-registry";
export * from "./status-bar-registry";
-3
View File
@@ -33,8 +33,5 @@ export function initRegistries() {
registries.KubeObjectMenuRegistry.createInstance();
registries.KubeObjectStatusRegistry.createInstance();
registries.StatusBarRegistry.createInstance();
registries.WelcomeMenuRegistry.createInstance();
registries.WelcomeBannerRegistry.createInstance();
registries.WorkloadsOverviewDetailRegistry.createInstance();
registries.TopBarRegistry.createInstance();
}
+1
View File
@@ -6,6 +6,7 @@
<body>
<div id="app"></div>
<div id="terminal-init"></div>
</body>
</html>