mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into run-tests-on-win
This commit is contained in:
commit
352dfd9b25
@ -4,6 +4,7 @@ module.exports = {
|
||||
ignorePatterns: [
|
||||
"**/node_modules/**/*",
|
||||
"**/dist/**/*",
|
||||
"**/static/**/*",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
2
extensions/example-extension/package-lock.json
generated
2
extensions/example-extension/package-lock.json
generated
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "extension-example",
|
||||
"name": "example-extension",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
|
||||
@ -1594,6 +1594,7 @@ msgstr "Names"
|
||||
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
||||
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
||||
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
||||
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:144
|
||||
msgid "Namespace"
|
||||
msgstr "Namespace"
|
||||
|
||||
@ -2003,6 +2004,10 @@ msgstr "Read-only Root Filesystem"
|
||||
msgid "Readiness"
|
||||
msgstr "Readiness"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:145
|
||||
msgid "Ready"
|
||||
msgstr "Ready"
|
||||
|
||||
#: src/renderer/components/+events/event-details.tsx:32
|
||||
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
||||
msgid "Reason"
|
||||
|
||||
@ -1585,6 +1585,7 @@ msgstr ""
|
||||
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
||||
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
||||
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
||||
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:144
|
||||
msgid "Namespace"
|
||||
msgstr ""
|
||||
|
||||
@ -1986,6 +1987,10 @@ msgstr ""
|
||||
msgid "Readiness"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:145
|
||||
msgid "Ready"
|
||||
msgstr ""
|
||||
|
||||
#: src/renderer/components/+events/event-details.tsx:32
|
||||
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
||||
msgid "Reason"
|
||||
|
||||
@ -1595,6 +1595,7 @@ msgstr ""
|
||||
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
||||
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
||||
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
||||
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:144
|
||||
msgid "Namespace"
|
||||
msgstr "Namespace"
|
||||
|
||||
@ -2004,6 +2005,10 @@ msgstr ""
|
||||
msgid "Readiness"
|
||||
msgstr "Готовность"
|
||||
|
||||
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:145
|
||||
msgid "Ready"
|
||||
msgstr "Готовы"
|
||||
|
||||
#: src/renderer/components/+events/event-details.tsx:32
|
||||
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
||||
msgid "Reason"
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
|
||||
"lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 src/ integration/ __mocks__/ build/ extensions/",
|
||||
"lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 .",
|
||||
"lint:fix": "yarn run lint --fix",
|
||||
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
||||
"verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
||||
|
||||
@ -9,7 +9,7 @@ jest.mock(
|
||||
() => ({
|
||||
ipcRenderer: {
|
||||
invoke: jest.fn(async (channel: string) => {
|
||||
if (channel === "extensions:loaded") {
|
||||
if (channel === "extensions:main") {
|
||||
return [
|
||||
[
|
||||
manifestPath,
|
||||
@ -44,7 +44,7 @@ jest.mock(
|
||||
}),
|
||||
on: jest.fn(
|
||||
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||
if (channel === "extensions:loaded") {
|
||||
if (channel === "extensions:main") {
|
||||
// First initialize with extensions 1 and 2
|
||||
// and then broadcast event to remove extensioin 2 and add extension number 3
|
||||
setTimeout(() => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { app, ipcRenderer, remote } from "electron";
|
||||
import { EventEmitter } from "events";
|
||||
import { isEqual } from "lodash";
|
||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import path from "path";
|
||||
import { getHostedCluster } from "../common/cluster-store";
|
||||
@ -25,7 +26,12 @@ const logModule = "[EXTENSIONS-LOADER]";
|
||||
export class ExtensionLoader {
|
||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||
protected readonly requestExtensionsChannel = "extensions:loaded";
|
||||
|
||||
// IPC channel to broadcast changes to extensions from main
|
||||
protected static readonly extensionsMainChannel = "extensions:main";
|
||||
|
||||
// IPC channel to broadcast changes to extensions from renderer
|
||||
protected static readonly extensionsRendererChannel = "extensions:renderer";
|
||||
|
||||
// emits event "remove" of type LensExtension when the extension is removed
|
||||
private events = new EventEmitter();
|
||||
@ -95,28 +101,27 @@ export class ExtensionLoader {
|
||||
this.loadOnMain();
|
||||
this.broadcastExtensions();
|
||||
|
||||
reaction(() => this.extensions.toJS(), () => {
|
||||
reaction(() => this.toJSON(), () => {
|
||||
this.broadcastExtensions();
|
||||
});
|
||||
|
||||
handleRequest(this.requestExtensionsChannel, () => {
|
||||
handleRequest(ExtensionLoader.extensionsMainChannel, () => {
|
||||
return Array.from(this.toJSON());
|
||||
});
|
||||
|
||||
subscribeToBroadcast(ExtensionLoader.extensionsRendererChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
this.syncExtensions(extensions);
|
||||
});
|
||||
}
|
||||
|
||||
protected async initRenderer() {
|
||||
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
this.isLoaded = true;
|
||||
this.syncExtensions(extensions);
|
||||
|
||||
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
||||
|
||||
// Add new extensions
|
||||
extensions.forEach(([extId, ext]) => {
|
||||
if (!this.extensions.has(extId)) {
|
||||
this.extensions.set(extId, ext);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove deleted extensions
|
||||
|
||||
// Remove deleted extensions in renderer side only
|
||||
this.extensions.forEach((_, lensExtensionId) => {
|
||||
if (!receivedExtensionIds.includes(lensExtensionId)) {
|
||||
this.removeExtension(lensExtensionId);
|
||||
@ -124,14 +129,26 @@ export class ExtensionLoader {
|
||||
});
|
||||
};
|
||||
|
||||
requestMain(this.requestExtensionsChannel).then(extensionListHandler);
|
||||
subscribeToBroadcast(this.requestExtensionsChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
reaction(() => this.toJSON(), () => {
|
||||
this.broadcastExtensions(false);
|
||||
});
|
||||
|
||||
requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler);
|
||||
subscribeToBroadcast(ExtensionLoader.extensionsMainChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||
extensionListHandler(extensions);
|
||||
});
|
||||
}
|
||||
|
||||
syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) {
|
||||
extensions.forEach(([lensExtensionId, extension]) => {
|
||||
if (!isEqual(this.extensions.get(lensExtensionId), extension)) {
|
||||
this.extensions.set(lensExtensionId, extension);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadOnMain() {
|
||||
logger.info(`${logModule}: load on main`);
|
||||
logger.debug(`${logModule}: load on main`);
|
||||
this.autoInitExtensions(async (extension: LensMainExtension) => {
|
||||
// Each .add returns a function to remove the item
|
||||
const removeItems = [
|
||||
@ -151,7 +168,7 @@ export class ExtensionLoader {
|
||||
}
|
||||
|
||||
loadOnClusterManagerRenderer() {
|
||||
logger.info(`${logModule}: load on main renderer (cluster manager)`);
|
||||
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
const removeItems = [
|
||||
registries.globalPageRegistry.add(extension.globalPages, extension),
|
||||
@ -174,7 +191,7 @@ export class ExtensionLoader {
|
||||
}
|
||||
|
||||
loadOnClusterRenderer() {
|
||||
logger.info(`${logModule}: load on cluster renderer (dashboard)`);
|
||||
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
|
||||
const cluster = getHostedCluster();
|
||||
|
||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||
@ -204,26 +221,26 @@ export class ExtensionLoader {
|
||||
|
||||
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Function[]>) {
|
||||
return reaction(() => this.toJSON(), installedExtensions => {
|
||||
for (const [extId, ext] of installedExtensions) {
|
||||
for (const [extId, extension] of installedExtensions) {
|
||||
const alreadyInit = this.instances.has(extId);
|
||||
|
||||
if (ext.isEnabled && !alreadyInit) {
|
||||
if (extension.isEnabled && !alreadyInit) {
|
||||
try {
|
||||
const LensExtensionClass = this.requireExtension(ext);
|
||||
const LensExtensionClass = this.requireExtension(extension);
|
||||
|
||||
if (!LensExtensionClass) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const instance = new LensExtensionClass(ext);
|
||||
const instance = new LensExtensionClass(extension);
|
||||
|
||||
instance.whenEnabled(() => register(instance));
|
||||
instance.enable();
|
||||
this.instances.set(extId, instance);
|
||||
} catch (err) {
|
||||
logger.error(`${logModule}: activation extension error`, { ext, err });
|
||||
logger.error(`${logModule}: activation extension error`, { ext: extension, err });
|
||||
}
|
||||
} else if (!ext.isEnabled && alreadyInit) {
|
||||
} else if (!extension.isEnabled && alreadyInit) {
|
||||
this.removeInstance(extId);
|
||||
}
|
||||
}
|
||||
@ -262,8 +279,8 @@ export class ExtensionLoader {
|
||||
});
|
||||
}
|
||||
|
||||
broadcastExtensions() {
|
||||
broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON()));
|
||||
broadcastExtensions(main = true) {
|
||||
broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -45,17 +45,6 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
await extensionLoader.whenLoaded;
|
||||
await this.whenLoaded;
|
||||
|
||||
// apply state on changes from store
|
||||
reaction(() => this.state.toJS(), extensionsState => {
|
||||
extensionsState.forEach((state, extId) => {
|
||||
const ext = extensionLoader.getExtension(extId);
|
||||
|
||||
if (ext && !ext.isBundled) {
|
||||
ext.isEnabled = state.enabled;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// save state on change `extension.isEnabled`
|
||||
reaction(() => this.getState(extensionLoader), extensionsState => {
|
||||
this.state.merge(extensionsState);
|
||||
@ -65,7 +54,9 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
isEnabled(extId: LensExtensionId) {
|
||||
const state = this.state.get(extId);
|
||||
|
||||
return state && state.enabled; // by default false
|
||||
// By default false, so that copied extensions are disabled by default.
|
||||
// If user installs the extension from the UI, the Extensions component will specifically enable it.
|
||||
return Boolean(state?.enabled);
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@ -44,7 +44,7 @@ export function getExtensionPageUrl<P extends object>({ extensionId, pageId = ""
|
||||
const extensionBaseUrl = compile(`/extension/:name`)({
|
||||
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
|
||||
});
|
||||
const extPageRoutePath = path.join(extensionBaseUrl, pageId); // page-id might contain route :param-s, so don't compile yet
|
||||
const extPageRoutePath = path.posix.join(extensionBaseUrl, pageId);
|
||||
|
||||
if (params) {
|
||||
return compile(extPageRoutePath)(params); // might throw error when required params not passed
|
||||
|
||||
@ -62,6 +62,9 @@ export class Extensions extends React.Component {
|
||||
@observable search = "";
|
||||
@observable installPath = "";
|
||||
|
||||
// True if the preliminary install steps have started, but unpackExtension has not started yet
|
||||
@observable startingInstall = false;
|
||||
|
||||
/**
|
||||
* Extensions that were removed from extensions but are still in "uninstalling" state
|
||||
*/
|
||||
@ -91,11 +94,20 @@ export class Extensions extends React.Component {
|
||||
});
|
||||
|
||||
this.addedInstalling.forEach(({ id, displayName }) => {
|
||||
const extension = this.extensions.find(extension => extension.id === id);
|
||||
|
||||
if (!extension) {
|
||||
throw new Error("Extension not found");
|
||||
}
|
||||
|
||||
Notifications.ok(
|
||||
<p>Extension <b>{displayName}</b> successfully installed!</p>
|
||||
);
|
||||
this.extensionState.delete(id);
|
||||
this.installPath = "";
|
||||
|
||||
// Enable installed extensions by default.
|
||||
extension.isEnabled = true;
|
||||
});
|
||||
})
|
||||
);
|
||||
@ -152,6 +164,8 @@ export class Extensions extends React.Component {
|
||||
const { installPath } = this;
|
||||
|
||||
if (!installPath) return;
|
||||
|
||||
this.startingInstall = true;
|
||||
const fileName = path.basename(installPath);
|
||||
|
||||
try {
|
||||
@ -161,13 +175,14 @@ export class Extensions extends React.Component {
|
||||
const { promise: filePromise } = downloadFile({ url: installPath, timeout: 60000 /*1m*/ });
|
||||
const data = await filePromise;
|
||||
|
||||
this.requestInstall({ fileName, data });
|
||||
await this.requestInstall({ fileName, data });
|
||||
}
|
||||
// otherwise installing from system path
|
||||
else if (InputValidators.isPath.validate(installPath)) {
|
||||
this.requestInstall({ fileName, filePath: installPath });
|
||||
await this.requestInstall({ fileName, filePath: installPath });
|
||||
}
|
||||
} catch (error) {
|
||||
this.startingInstall = false;
|
||||
Notifications.error(
|
||||
<p>Installation has failed: <b>{String(error)}</b></p>
|
||||
);
|
||||
@ -186,11 +201,11 @@ export class Extensions extends React.Component {
|
||||
};
|
||||
|
||||
async preloadExtensions(requests: InstallRequest[], { showError = true } = {}) {
|
||||
const preloadedRequests = requests.filter(req => req.data);
|
||||
const preloadedRequests = requests.filter(request => request.data);
|
||||
|
||||
await Promise.all(
|
||||
requests
|
||||
.filter(req => !req.data && req.filePath)
|
||||
.filter(request => !request.data && request.filePath)
|
||||
.map(async request => {
|
||||
try {
|
||||
const data = await fse.readFile(request.filePath);
|
||||
@ -247,11 +262,11 @@ export class Extensions extends React.Component {
|
||||
// copy files to temp
|
||||
await fse.ensureDir(this.getExtensionPackageTemp());
|
||||
|
||||
requests.forEach(req => {
|
||||
const tempFile = this.getExtensionPackageTemp(req.fileName);
|
||||
for (const request of requests) {
|
||||
const tempFile = this.getExtensionPackageTemp(request.fileName);
|
||||
|
||||
fse.writeFileSync(tempFile, req.data);
|
||||
});
|
||||
await fse.writeFile(tempFile, request.data);
|
||||
}
|
||||
|
||||
// validate packages
|
||||
await Promise.all(
|
||||
@ -289,15 +304,24 @@ export class Extensions extends React.Component {
|
||||
const preloadedRequests = await this.preloadExtensions(requests);
|
||||
const validatedRequests = await this.createTempFilesAndValidate(preloadedRequests);
|
||||
|
||||
validatedRequests.forEach(install => {
|
||||
// If there are no requests for installing, reset startingInstall state
|
||||
if (validatedRequests.length === 0) {
|
||||
this.startingInstall = false;
|
||||
}
|
||||
|
||||
for (const install of validatedRequests) {
|
||||
const { name, version, description } = install.manifest;
|
||||
const extensionFolder = this.getExtensionDestFolder(name);
|
||||
const folderExists = fse.existsSync(extensionFolder);
|
||||
|
||||
const folderExists = await fse.pathExists(extensionFolder);
|
||||
|
||||
if (!folderExists) {
|
||||
// auto-install extension if not yet exists
|
||||
this.unpackExtension(install);
|
||||
} else {
|
||||
// If we show the confirmation dialog, we stop the install spinner until user clicks ok
|
||||
// and the install continues
|
||||
this.startingInstall = false;
|
||||
|
||||
// otherwise confirmation required (re-install / update)
|
||||
const removeNotification = Notifications.info(
|
||||
<div className="InstallingExtensionNotification flex gaps align-center">
|
||||
@ -315,21 +339,23 @@ export class Extensions extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) {
|
||||
const displayName = extensionDisplayName(name, version);
|
||||
const extensionFolder = this.getExtensionDestFolder(name);
|
||||
const unpackingTempFolder = path.join(path.dirname(tempFile), `${path.basename(tempFile)}-unpacked`);
|
||||
const extensionId = path.join(extensionDiscovery.nodeModulesPath, name, "package.json");
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
this.extensionState.set(extensionId, {
|
||||
state: "installing",
|
||||
displayName
|
||||
});
|
||||
this.startingInstall = false;
|
||||
|
||||
const extensionFolder = this.getExtensionDestFolder(name);
|
||||
const unpackingTempFolder = path.join(path.dirname(tempFile), `${path.basename(tempFile)}-unpacked`);
|
||||
|
||||
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
|
||||
|
||||
try {
|
||||
// extract to temp folder first
|
||||
@ -455,7 +481,7 @@ export class Extensions extends React.Component {
|
||||
* True if at least one extension is in installing state
|
||||
*/
|
||||
@computed get isInstalling() {
|
||||
return [...this.extensionState.values()].some(extension => extension.state === "installing");
|
||||
return this.startingInstall || [...this.extensionState.values()].some(extension => extension.state === "installing");
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@ -115,6 +115,7 @@ export class PodDetailsList extends React.Component<Props> {
|
||||
<TableCell className="name">{pod.getName()}</TableCell>
|
||||
<TableCell className="warning"><KubeObjectStatusIcon key="icon" object={pod}/></TableCell>
|
||||
<TableCell className="namespace">{pod.getNs()}</TableCell>
|
||||
<TableCell className="ready">{pod.getRunningContainers().length}/{pod.getContainers().length}</TableCell>
|
||||
<TableCell className="cpu">{this.renderCpuUsage(`cpu-${pod.getId()}`, metrics.cpu)}</TableCell>
|
||||
<TableCell className="memory">{this.renderMemoryUsage(`memory-${pod.getId()}`, metrics.memory)}</TableCell>
|
||||
<TableCell className={cssNames("status", kebabCase(pod.getStatusMessage()))}>{pod.getStatusMessage()}</TableCell>
|
||||
@ -148,7 +149,8 @@ export class PodDetailsList extends React.Component<Props> {
|
||||
<TableHead>
|
||||
<TableCell className="name" sortBy={sortBy.name}><Trans>Name</Trans></TableCell>
|
||||
<TableCell className="warning"/>
|
||||
<TableCell className="namespace" sortBy={sortBy.namespace}>Namespace</TableCell>
|
||||
<TableCell className="namespace" sortBy={sortBy.namespace}><Trans>Namespace</Trans></TableCell>
|
||||
<TableCell className="ready"><Trans>Ready</Trans></TableCell>
|
||||
<TableCell className="cpu" sortBy={sortBy.cpu}><Trans>CPU</Trans></TableCell>
|
||||
<TableCell className="memory" sortBy={sortBy.memory}><Trans>Memory</Trans></TableCell>
|
||||
<TableCell className="status"><Trans>Status</Trans></TableCell>
|
||||
|
||||
52
src/renderer/components/cluster-manager/bottom-bar.test.tsx
Normal file
52
src/renderer/components/cluster-manager/bottom-bar.test.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
|
||||
import { BottomBar } from "./bottom-bar";
|
||||
jest.mock("../../../extensions/registries");
|
||||
import { statusBarRegistry } from "../../../extensions/registries";
|
||||
|
||||
describe("<BottomBar />", () => {
|
||||
|
||||
it("renders w/o errors", () => {
|
||||
const { container } = render(<BottomBar />);
|
||||
|
||||
expect(container).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
// some defensive testing
|
||||
it("renders w/o errors when .getItems() returns edge cases", async () => {
|
||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => undefined);
|
||||
expect(() => render(<BottomBar />)).not.toThrow();
|
||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => null);
|
||||
expect(() => render(<BottomBar />)).not.toThrow();
|
||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => []);
|
||||
expect(() => render(<BottomBar />)).not.toThrow();
|
||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => { return {};});
|
||||
expect(() => render(<BottomBar />)).not.toThrow();
|
||||
});
|
||||
|
||||
it("renders items [{item: React.ReactNode}] (4.0.0-rc.1)", async () => {
|
||||
const testId = "testId";
|
||||
const text = "heee";
|
||||
|
||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => [
|
||||
{ item: <span data-testid={testId} >{text}</span> }
|
||||
]);
|
||||
const { getByTestId } = render(<BottomBar />);
|
||||
|
||||
expect(await getByTestId(testId)).toHaveTextContent(text);
|
||||
});
|
||||
|
||||
it("renders items [{item: () => React.ReactNode}] (4.0.0-rc.1+)", async () => {
|
||||
const testId = "testId";
|
||||
const text = "heee";
|
||||
|
||||
statusBarRegistry.getItems = jest.fn().mockImplementationOnce(() => [
|
||||
{ item: () => <span data-testid={testId} >{text}</span> }
|
||||
]);
|
||||
const { getByTestId } = render(<BottomBar />);
|
||||
|
||||
expect(await getByTestId(testId)).toHaveTextContent(text);
|
||||
});
|
||||
});
|
||||
@ -11,6 +11,8 @@ import { statusBarRegistry } from "../../../extensions/registries";
|
||||
export class BottomBar extends React.Component {
|
||||
render() {
|
||||
const { currentWorkspace } = workspaceStore;
|
||||
// in case .getItems() returns undefined
|
||||
const items = statusBarRegistry.getItems() ?? [];
|
||||
|
||||
return (
|
||||
<div className="BottomBar flex gaps">
|
||||
@ -22,10 +24,17 @@ export class BottomBar extends React.Component {
|
||||
htmlFor="current-workspace"
|
||||
/>
|
||||
<div className="extensions box grow flex gaps justify-flex-end">
|
||||
{statusBarRegistry.getItems().map(({ item }, index) => {
|
||||
{Array.isArray(items) && items.map(({ item }, index) => {
|
||||
if (!item) return;
|
||||
|
||||
return <div className="flex align-center gaps item" key={index}>{item}</div>;
|
||||
return (
|
||||
<div
|
||||
className="flex align-center gaps item"
|
||||
key={index}
|
||||
>
|
||||
{typeof item === "function" ? item() : item}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
4
types/dom.d.ts
vendored
4
types/dom.d.ts
vendored
@ -1,7 +1,7 @@
|
||||
export {}
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Element {
|
||||
scrollIntoViewIfNeeded(opt_center?: boolean): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
types/font-face.d.ts
vendored
12
types/font-face.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
// https://www.w3.org/TR/css-font-loading/
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/FontFace
|
||||
export {}
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
const FontFace: FontFace;
|
||||
@ -10,11 +10,11 @@ declare global {
|
||||
}
|
||||
|
||||
type CSSOMString = string;
|
||||
type FontFaceLoadStatus = 'unloaded' | 'loading' | 'loaded' | 'error';
|
||||
type FontFaceSetStatus = 'loading' | 'loaded';
|
||||
type FontFaceLoadStatus = "unloaded" | "loading" | "loaded" | "error";
|
||||
type FontFaceSetStatus = "loading" | "loaded";
|
||||
|
||||
interface FontFace extends FontFaceDescriptors {
|
||||
new(family: string, source: string | ArrayBuffer, descriptors?: FontFaceDescriptors): FontFace;
|
||||
class FontFace implements FontFaceDescriptors {
|
||||
constructor(family: string, source: string | ArrayBuffer, descriptors?: FontFaceDescriptors);
|
||||
readonly status: FontFaceLoadStatus;
|
||||
readonly loaded: Promise<FontFace>;
|
||||
variationSettings: CSSOMString;
|
||||
@ -41,4 +41,4 @@ declare global {
|
||||
delete(font: FontFace): void;
|
||||
clear(): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,87 +1,90 @@
|
||||
|
||||
import path from 'path';
|
||||
import path from "path";
|
||||
import webpack from "webpack";
|
||||
import { sassCommonVars } from "./src/common/vars";
|
||||
|
||||
export default function (): webpack.Configuration {
|
||||
const entry = "./src/extensions/extension-api.ts"
|
||||
const outDir = "./src/extensions/npm/extensions/dist";
|
||||
return {
|
||||
// Compile for Electron for renderer process
|
||||
// see <https://webpack.js.org/configuration/target/>
|
||||
target: "electron-renderer",
|
||||
entry,
|
||||
output: {
|
||||
filename: 'extension-api.js',
|
||||
// need to be an absolute path
|
||||
path: path.resolve(__dirname, `${outDir}/src/extensions`),
|
||||
// can be use in commonjs environments
|
||||
// e.g. require('@k8slens/extensions')
|
||||
libraryTarget: "commonjs"
|
||||
const entry = "./src/extensions/extension-api.ts";
|
||||
const outDir = "./src/extensions/npm/extensions/dist";
|
||||
|
||||
return {
|
||||
// Compile for Electron for renderer process
|
||||
// see <https://webpack.js.org/configuration/target/>
|
||||
target: "electron-renderer",
|
||||
entry,
|
||||
// this is the default mode, so we should make it explicit to silence the warning
|
||||
mode: "production",
|
||||
output: {
|
||||
filename: "extension-api.js",
|
||||
// need to be an absolute path
|
||||
path: path.resolve(__dirname, `${outDir}/src/extensions`),
|
||||
// can be use in commonjs environments
|
||||
// e.g. require('@k8slens/extensions')
|
||||
libraryTarget: "commonjs"
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
// !! ts-loader will use tsconfig.json at folder root
|
||||
// !! changes in tsconfig.json may have side effects
|
||||
// !! on '@k8slens/extensions' module
|
||||
compilerOptions: {
|
||||
declaration: true, // output .d.ts
|
||||
sourceMap: false, // to override sourceMap: true in tsconfig.json
|
||||
outDir // where the .d.ts should be located
|
||||
}
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
// !! ts-loader will use tsconfig.json at folder root
|
||||
// !! changes in tsconfig.json may have side effects
|
||||
// !! on '@k8slens/extensions' module
|
||||
compilerOptions: {
|
||||
declaration: true, // output .d.ts
|
||||
sourceMap: false, // to override sourceMap: true in tsconfig.json
|
||||
outDir // where the .d.ts should be located
|
||||
}
|
||||
}
|
||||
// for src/renderer/components/fonts/roboto-mono-nerd.ttf
|
||||
// in src/renderer/components/dock/terminal.ts 95:25-65
|
||||
{
|
||||
test: /\.(ttf|eot|woff2?)$/,
|
||||
use: {
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
name: "fonts/[name].[ext]"
|
||||
}
|
||||
}
|
||||
},
|
||||
// for import scss files
|
||||
{
|
||||
test: /\.s?css$/,
|
||||
use: [
|
||||
// creates `style` nodes from JS strings
|
||||
"style-loader",
|
||||
// translates CSS into CommonJS
|
||||
"css-loader",
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
prependData: `@import "${path.basename(sassCommonVars)}";`,
|
||||
sassOptions: {
|
||||
includePaths: [
|
||||
path.dirname(sassCommonVars)
|
||||
]
|
||||
},
|
||||
// for src/renderer/components/fonts/roboto-mono-nerd.ttf
|
||||
// in src/renderer/components/dock/terminal.ts 95:25-65
|
||||
{
|
||||
test: /\.(ttf|eot|woff2?)$/,
|
||||
use: {
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
name: "fonts/[name].[ext]"
|
||||
}
|
||||
}
|
||||
},
|
||||
// for import scss files
|
||||
{
|
||||
test: /\.s?css$/,
|
||||
use: [
|
||||
// creates `style` nodes from JS strings
|
||||
"style-loader",
|
||||
// translates CSS into CommonJS
|
||||
"css-loader",
|
||||
{
|
||||
loader: "sass-loader",
|
||||
options: {
|
||||
prependData: `@import "${path.basename(sassCommonVars)}";`,
|
||||
sassOptions: {
|
||||
includePaths: [
|
||||
path.dirname(sassCommonVars)
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js']
|
||||
},
|
||||
plugins: [
|
||||
// In ts-loader's README they said to output a built .d.ts file,
|
||||
// you can set "declaration": true in tsconfig.extensions.json,
|
||||
// and use the DeclarationBundlerPlugin in your webpack config... but
|
||||
// !! the DeclarationBundlerPlugin doesn't work anymore, author archived it.
|
||||
// https://www.npmjs.com/package/declaration-bundler-webpack-plugin
|
||||
// new DeclarationBundlerPlugin({
|
||||
// moduleName: '@k8slens/extensions',
|
||||
// out: 'extension-api.d.ts',
|
||||
// })
|
||||
]
|
||||
};
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js"]
|
||||
},
|
||||
plugins: [
|
||||
// In ts-loader's README they said to output a built .d.ts file,
|
||||
// you can set "declaration": true in tsconfig.extensions.json,
|
||||
// and use the DeclarationBundlerPlugin in your webpack config... but
|
||||
// !! the DeclarationBundlerPlugin doesn't work anymore, author archived it.
|
||||
// https://www.npmjs.com/package/declaration-bundler-webpack-plugin
|
||||
// new DeclarationBundlerPlugin({
|
||||
// moduleName: '@k8slens/extensions',
|
||||
// out: 'extension-api.d.ts',
|
||||
// })
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import path from "path";
|
||||
import webpack from "webpack";
|
||||
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"
|
||||
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
|
||||
import { isDevelopment, isProduction, mainDir, buildDir } from "./src/common/vars";
|
||||
import nodeExternals from "webpack-node-externals";
|
||||
import ProgressBarPlugin from "progress-bar-webpack-plugin";
|
||||
import * as vars from "./src/common/vars";
|
||||
|
||||
export default function (): webpack.Configuration {
|
||||
console.info('WEBPACK:main', require("./src/common/vars"))
|
||||
console.info("WEBPACK:main", vars);
|
||||
|
||||
return {
|
||||
context: __dirname,
|
||||
target: "electron-main",
|
||||
@ -21,7 +23,7 @@ export default function (): webpack.Configuration {
|
||||
path: buildDir,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.json', '.js', '.ts']
|
||||
extensions: [".json", ".js", ".ts"]
|
||||
},
|
||||
externals: [
|
||||
nodeExternals()
|
||||
@ -48,5 +50,5 @@ export default function (): webpack.Configuration {
|
||||
new ProgressBarPlugin(),
|
||||
new ForkTsCheckerPlugin(),
|
||||
].filter(Boolean)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,18 +4,20 @@ import webpack from "webpack";
|
||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
||||
import TerserPlugin from "terser-webpack-plugin";
|
||||
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"
|
||||
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
|
||||
import ProgressBarPlugin from "progress-bar-webpack-plugin";
|
||||
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'
|
||||
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
|
||||
import * as vars from "./src/common/vars";
|
||||
|
||||
export default [
|
||||
webpackLensRenderer
|
||||
]
|
||||
];
|
||||
|
||||
export function webpackLensRenderer({ showVars = true } = {}): webpack.Configuration {
|
||||
if (showVars) {
|
||||
console.info('WEBPACK:renderer', require("./src/common/vars"));
|
||||
console.info("WEBPACK:renderer", vars);
|
||||
}
|
||||
|
||||
return {
|
||||
context: __dirname,
|
||||
target: "electron-renderer",
|
||||
@ -27,7 +29,7 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
|
||||
hot: true,
|
||||
// to avoid cors errors when requests is from iframes
|
||||
disableHostCheck: true,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
},
|
||||
name: "lens-app",
|
||||
mode: isProduction ? "production" : "development",
|
||||
@ -39,10 +41,10 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
|
||||
libraryTarget: "global",
|
||||
library: "",
|
||||
globalObject: "this",
|
||||
publicPath: publicPath,
|
||||
publicPath,
|
||||
path: buildDir,
|
||||
filename: '[name].js',
|
||||
chunkFilename: 'chunks/[name].js',
|
||||
filename: "[name].js",
|
||||
chunkFilename: "chunks/[name].js",
|
||||
},
|
||||
stats: {
|
||||
warningsFilter: [
|
||||
@ -51,8 +53,8 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
|
||||
},
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.js', '.jsx', '.json',
|
||||
'.ts', '.tsx',
|
||||
".js", ".jsx", ".json",
|
||||
".ts", ".tsx",
|
||||
]
|
||||
},
|
||||
optimization: {
|
||||
@ -91,7 +93,7 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
|
||||
}],
|
||||
],
|
||||
plugins: [
|
||||
isDevelopment && require.resolve('react-refresh/babel'),
|
||||
isDevelopment && require.resolve("react-refresh/babel"),
|
||||
].filter(Boolean),
|
||||
}
|
||||
},
|
||||
@ -190,5 +192,5 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
|
||||
isDevelopment && new ReactRefreshWebpackPlugin(),
|
||||
|
||||
].filter(Boolean),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user