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

Merge branch 'master' into fix-misleading-no-metrics-message

This commit is contained in:
Alex Andreev 2022-04-27 10:26:06 +03:00
commit 995d4e8418
32 changed files with 905 additions and 661 deletions

View File

@ -67,6 +67,10 @@ jobs:
- run: make test-extensions - run: make test-extensions
name: Run In-tree Extension tests name: Run In-tree Extension tests
- run: make ci-validate-dev
if: ${{ contains(github.event.pull_request.labels.*.name, 'dependencies') }}
name: Validate dev mode will work
- name: Install integration test dependencies - name: Install integration test dependencies
id: minikube id: minikube
uses: medyagh/setup-minikube@5a9a7104d7322fa40424de8855c84685e89cefd7 uses: medyagh/setup-minikube@5a9a7104d7322fa40424de8855c84685e89cefd7

View File

@ -28,6 +28,9 @@ compile-dev: node_modules
yarn compile:main --cache yarn compile:main --cache
yarn compile:renderer --cache yarn compile:renderer --cache
.PHONY: validate-dev
ci-validate-dev: binaries/client build-extensions compile-dev
.PHONY: dev .PHONY: dev
dev: binaries/client build-extensions dev: binaries/client build-extensions
rm -rf static/build/ rm -rf static/build/

View File

@ -183,9 +183,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
} }
const testPodName = "nginx-create-pod-test"; const testPodName = "nginx-create-pod-test";
const monacoEditor = await frame.waitForSelector( const monacoEditor = await frame.waitForSelector(`.Dock.isOpen [data-test-id="monaco-editor"]`);
`.Dock.isOpen [data-test-component="monaco-editor"]`,
);
await monacoEditor.click(); await monacoEditor.click();
await monacoEditor.type("apiVersion: v1", { delay: 10 }); await monacoEditor.type("apiVersion: v1", { delay: 10 });

View File

@ -17,11 +17,11 @@
"debug-build": "concurrently yarn:compile:main yarn:compile:extension-types", "debug-build": "concurrently yarn:compile:main yarn:compile:extension-types",
"dev-run": "nodemon --watch ./static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"", "dev-run": "nodemon --watch ./static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
"dev:main": "yarn run compile:main --watch --progress", "dev:main": "yarn run compile:main --watch --progress",
"dev:renderer": "yarn run ts-node webpack.dev-server.ts", "dev:renderer": "yarn run ts-node webpack/dev-server.ts",
"compile": "env NODE_ENV=production concurrently yarn:compile:*", "compile": "env NODE_ENV=production concurrently yarn:compile:*",
"compile:main": "yarn run webpack --config webpack.main.ts", "compile:main": "yarn run webpack --config webpack/main.ts",
"compile:renderer": "yarn run webpack --config webpack.renderer.ts", "compile:renderer": "yarn run webpack --config webpack/renderer.ts",
"compile:extension-types": "yarn run webpack --config webpack.extensions.ts", "compile:extension-types": "yarn run webpack --config webpack/extensions.ts",
"npm:fix-build-version": "yarn run ts-node build/set_build_version.ts", "npm:fix-build-version": "yarn run ts-node build/set_build_version.ts",
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts", "npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
"build:linux": "yarn run compile && electron-builder --linux --dir", "build:linux": "yarn run compile && electron-builder --linux --dir",
@ -199,6 +199,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^8.0.1", "@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.3", "@kubernetes/client-node": "^0.16.3",
@ -229,7 +230,6 @@
"joi": "^17.6.0", "joi": "^17.6.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsdom": "^16.7.0", "jsdom": "^16.7.0",
"jsonpath": "^1.1.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mac-ca": "^1.0.6", "mac-ca": "^1.0.6",
"marked": "^4.0.12", "marked": "^4.0.12",
@ -239,7 +239,7 @@
"mobx-react": "^7.3.0", "mobx-react": "^7.3.0",
"mobx-utils": "^6.0.4", "mobx-utils": "^6.0.4",
"mock-fs": "^5.1.2", "mock-fs": "^5.1.2",
"moment": "^2.29.1", "moment": "^2.29.3",
"moment-timezone": "^0.5.34", "moment-timezone": "^0.5.34",
"monaco-editor": "^0.29.1", "monaco-editor": "^0.29.1",
"monaco-editor-webpack-plugin": "^5.0.0", "monaco-editor-webpack-plugin": "^5.0.0",
@ -259,7 +259,7 @@
"request-promise-native": "^1.0.9", "request-promise-native": "^1.0.9",
"rfc6902": "^4.0.2", "rfc6902": "^4.0.2",
"selfsigned": "^2.0.1", "selfsigned": "^2.0.1",
"semver": "^7.3.2", "semver": "^7.3.7",
"shell-env": "^3.0.1", "shell-env": "^3.0.1",
"spdy": "^4.0.2", "spdy": "^4.0.2",
"tar": "^6.1.11", "tar": "^6.1.11",
@ -278,7 +278,7 @@
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@sentry/types": "^6.19.3", "@sentry/types": "^6.19.3",
"@testing-library/jest-dom": "^5.16.3", "@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4", "@testing-library/react": "^12.1.4",
@ -299,7 +299,6 @@
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/jsdom": "^16.2.14", "@types/jsdom": "^16.2.14",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.181", "@types/lodash": "^4.14.181",
"@types/marked": "^4.0.3", "@types/marked": "^4.0.3",
"@types/md5-file": "^4.0.2", "@types/md5-file": "^4.0.2",
@ -313,7 +312,7 @@
"@types/react": "^17.0.43", "@types/react": "^17.0.43",
"@types/react-beautiful-dnd": "^13.1.2", "@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.14", "@types/react-dom": "^17.0.14",
"@types/react-router-dom": "^5.3.2", "@types/react-router-dom": "^5.3.3",
"@types/react-select": "3.1.2", "@types/react-select": "3.1.2",
"@types/react-table": "^7.7.9", "@types/react-table": "^7.7.9",
"@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-virtualized-auto-sizer": "^1.0.1",
@ -347,15 +346,15 @@
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dompurify": "^2.3.6", "dompurify": "^2.3.6",
"electron": "^14.2.9", "electron": "^14.2.9",
"electron-builder": "^22.14.13", "electron-builder": "^23.0.3",
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
"esbuild": "^0.14.34", "esbuild": "^0.14.38",
"esbuild-loader": "^2.18.0", "esbuild-loader": "^2.18.0",
"eslint": "^8.11.0", "eslint": "^8.11.0",
"eslint-plugin-header": "^3.1.1", "eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "^7.29.4", "eslint-plugin-react": "^7.29.4",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.4.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"flex.box": "^3.4.4", "flex.box": "^3.4.4",
"fork-ts-checker-webpack-plugin": "^6.5.0", "fork-ts-checker-webpack-plugin": "^6.5.0",
@ -380,9 +379,9 @@
"postcss-loader": "^6.2.1", "postcss-loader": "^6.2.1",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-refresh": "^0.11.0", "react-refresh": "^0.12.0",
"react-refresh-typescript": "^2.0.4", "react-refresh-typescript": "^2.0.4",
"react-router-dom": "^5.3.0", "react-router-dom": "^5.3.1",
"react-select": "3.2.0", "react-select": "3.2.0",
"react-select-event": "^5.3.0", "react-select-event": "^5.3.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",
@ -403,7 +402,7 @@
"typeface-roboto": "^1.1.13", "typeface-roboto": "^1.1.13",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"typescript-plugin-css-modules": "^3.4.0", "typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.69.0", "webpack": "^5.72.0",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1", "webpack-dev-server": "^4.8.1",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",

View File

@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Setup variable in global scope (top-level object)
// Global type definition must be added separately to `mocks.d.ts` in form:
// declare const __globalName: any;
export function defineGlobal(propName: string, descriptor: PropertyDescriptor) {
const scope = typeof global !== "undefined" ? global : window;
if (Object.prototype.hasOwnProperty.call(scope, propName)) {
return;
}
Object.defineProperty(scope, propName, descriptor);
}

View File

@ -20,7 +20,6 @@ export * from "./collection-functions";
export * from "./convertCpu"; export * from "./convertCpu";
export * from "./convertMemory"; export * from "./convertMemory";
export * from "./debouncePromise"; export * from "./debouncePromise";
export * from "./defineGlobal";
export * from "./delay"; export * from "./delay";
export * from "./disposer"; export * from "./disposer";
export * from "./downloadFile"; export * from "./downloadFile";

View File

@ -7,7 +7,6 @@
import path from "path"; import path from "path";
import { SemVer } from "semver"; import { SemVer } from "semver";
import packageInfo from "../../package.json"; import packageInfo from "../../package.json";
import { defineGlobal } from "./utils/defineGlobal";
import { lazyInitialized } from "./utils/lazy-initialized"; import { lazyInitialized } from "./utils/lazy-initialized";
export const isMac = process.platform === "darwin"; export const isMac = process.platform === "darwin";
@ -101,26 +100,12 @@ export const kubectlBinaryName = getBinaryName("kubectl");
* @deprecated for being explicit side effect. * @deprecated for being explicit side effect.
*/ */
export const kubectlBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), kubectlBinaryName)); export const kubectlBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), kubectlBinaryName));
export const staticFilesDirectory = path.resolve(
// Webpack build paths !isProduction
export const contextDir = process.cwd(); ? process.cwd()
export const buildDir = path.join(contextDir, "static", publicPath); : process.resourcesPath,
export const preloadEntrypoint = path.join(contextDir, "src/preload.ts"); "static",
export const mainDir = path.join(contextDir, "src/main"); );
export const rendererDir = path.join(contextDir, "src/renderer");
export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
// Special runtime paths
defineGlobal("__static", {
get() {
const root = isDevelopment
? contextDir
: (process.resourcesPath ?? contextDir);
return path.resolve(root, "static");
},
});
// Apis // Apis
export const apiPrefix = "/api" as string; // local router apis export const apiPrefix = "/api" as string; // local router apis
@ -142,5 +127,3 @@ export const appSemVer = new SemVer(packageInfo.version);
export const docsUrl = "https://docs.k8slens.dev/main/" as string; export const docsUrl = "https://docs.k8slens.dev/main/" as string;
export const sentryDsn = packageInfo.config?.sentryDsn ?? ""; export const sentryDsn = packageInfo.config?.sentryDsn ?? "";
export const webpackDevServerPort: number = Number(process.env.WEBPACK_DEV_SERVER_PORT) || 9191;

View File

@ -1,13 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { contextDir } from "../vars";
const contextDirInjectable = getInjectable({
id: "context-dir",
instantiate: () => contextDir,
});
export default contextDirInjectable;

View File

@ -16,7 +16,7 @@ import logger from "../../main/logger";
import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { ExtensionsStore } from "../extensions-store/extensions-store";
import type { ExtensionLoader } from "../extension-loader"; import type { ExtensionLoader } from "../extension-loader";
import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; import type { LensExtensionId, LensExtensionManifest } from "../lens-extension";
import { isProduction } from "../../common/vars"; import { isProduction, staticFilesDirectory } from "../../common/vars";
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
import type { PackageJson } from "type-fest"; import type { PackageJson } from "type-fest";
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
@ -112,7 +112,7 @@ export class ExtensionDiscovery {
} }
get inTreeFolderPath(): string { get inTreeFolderPath(): string {
return path.resolve(__static, "../extensions"); return path.resolve(staticFilesDirectory, "../extensions");
} }
get nodeModulesPath(): string { get nodeModulesPath(): string {

View File

@ -64,6 +64,11 @@ export * from "../../renderer/components/layout/sub-title";
export * from "../../renderer/components/input/search-input"; export * from "../../renderer/components/input/search-input";
export * from "../../renderer/components/chart/bar-chart"; export * from "../../renderer/components/chart/bar-chart";
export * from "../../renderer/components/chart/pie-chart"; export * from "../../renderer/components/chart/pie-chart";
export {
MonacoEditor,
type MonacoEditorProps, type MonacoEditorId,
type MonacoTheme, type MonacoCustomTheme,
} from "../../renderer/components/monaco-editor";
// kube helpers // kube helpers
export * from "../../renderer/components/kube-detail-params"; export * from "../../renderer/components/kube-detail-params";

View File

@ -11,7 +11,7 @@ import httpProxy from "http-proxy";
import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsMainApi from "../extensions/main-api"; import * as LensExtensionsMainApi from "../extensions/main-api";
import { app, autoUpdater, dialog, powerMonitor } from "electron"; import { app, autoUpdater, dialog, powerMonitor } from "electron";
import { appName, isIntegrationTesting, isMac, isWindows, productName } from "../common/vars"; import { appName, isIntegrationTesting, isMac, isWindows, productName, staticFilesDirectory } from "../common/vars";
import { LensProxy } from "./lens-proxy"; import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
@ -198,7 +198,7 @@ async function main(di: DiContainer) {
powerMonitor.on("shutdown", () => app.exit()); powerMonitor.on("shutdown", () => app.exit());
registerFileProtocol("static", __static); registerFileProtocol("static", staticFilesDirectory);
PrometheusProviderRegistry.createInstance(); PrometheusProviderRegistry.createInstance();
initializers.initPrometheusProviderRegistry(); initializers.initPrometheusProviderRegistry();

View File

@ -11,9 +11,6 @@ import type { Cluster } from "../../common/cluster/cluster";
import type { LensApiResultContentType } from "./router-content-types"; import type { LensApiResultContentType } from "./router-content-types";
import { contentTypes } from "./router-content-types"; import { contentTypes } from "./router-content-types";
// TODO: Import causes side effect, sets value for __static
import "../../common/vars";
export interface RouterRequestOpts { export interface RouterRequestOpts {
req: http.IncomingMessage; req: http.IncomingMessage;
res: http.ServerResponse; res: http.ServerResponse;

View File

@ -8,7 +8,7 @@ import type { SupportedFileExtension } from "../router/router-content-types";
import { contentTypes } from "../router/router-content-types"; import { contentTypes } from "../router/router-content-types";
import logger from "../logger"; import logger from "../logger";
import { routeInjectionToken } from "../router/router.injectable"; import { routeInjectionToken } from "../router/router.injectable";
import { appName, publicPath, webpackDevServerPort } from "../../common/vars"; import { appName, publicPath, staticFilesDirectory } from "../../common/vars";
import path from "path"; import path from "path";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import httpProxy from "http-proxy"; import httpProxy from "http-proxy";
@ -17,6 +17,7 @@ import type { GetAbsolutePath } from "../../common/path/get-absolute-path.inject
import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable"; import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable";
import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { JoinPaths } from "../../common/path/join-paths.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable";
import { webpackDevServerPort } from "../../../webpack/vars";
interface ProductionDependencies { interface ProductionDependencies {
readFileBuffer: (path: string) => Promise<Buffer>; readFileBuffer: (path: string) => Promise<Buffer>;
@ -27,7 +28,7 @@ interface ProductionDependencies {
const handleStaticFileInProduction = const handleStaticFileInProduction =
({ readFileBuffer, getAbsolutePath, joinPaths }: ProductionDependencies) => ({ readFileBuffer, getAbsolutePath, joinPaths }: ProductionDependencies) =>
async ({ params }: LensApiRequest) => { async ({ params }: LensApiRequest) => {
const staticPath = getAbsolutePath(__static); const staticPath = getAbsolutePath(staticFilesDirectory);
let filePath = params.path; let filePath = params.path;
for (let retryCount = 0; retryCount < 5; retryCount += 1) { for (let retryCount = 0; retryCount < 5; retryCount += 1) {

View File

@ -12,7 +12,7 @@ import { showAbout } from "../menu/menu";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater"; import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import type { WindowManager } from "../window-manager"; import type { WindowManager } from "../window-manager";
import logger from "../logger"; import logger from "../logger";
import { isDevelopment, isWindows, productName } from "../../common/vars"; import { isDevelopment, isWindows, productName, staticFilesDirectory } from "../../common/vars";
import { exitApp } from "../exit-app"; import { exitApp } from "../exit-app";
import { toJS } from "../../common/utils"; import { toJS } from "../../common/utils";
import type { TrayMenuRegistration } from "./tray-menu-registration"; import type { TrayMenuRegistration } from "./tray-menu-registration";
@ -25,7 +25,7 @@ export let tray: Tray;
export function getTrayIcon(): string { export function getTrayIcon(): string {
return path.resolve( return path.resolve(
__static, staticFilesDirectory,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
"trayIconTemplate.png", "trayIconTemplate.png",
); );

View File

@ -6,7 +6,6 @@
import "./crd-resource-details.scss"; import "./crd-resource-details.scss";
import React from "react"; import React from "react";
import jsonPath from "jsonpath";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Badge } from "../badge"; import { Badge } from "../badge";
@ -16,10 +15,11 @@ import { KubeObjectMeta } from "../kube-object-meta";
import { Input } from "../input"; import { Input } from "../input";
import type { AdditionalPrinterColumnsV1 } from "../../../common/k8s-api/endpoints/crd.api"; import type { AdditionalPrinterColumnsV1 } from "../../../common/k8s-api/endpoints/crd.api";
import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api"; import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/crd.api";
import { parseJsonPath } from "../../utils/jsonPath"; import { convertKubectlJsonPathToNodeJsonPath } from "../../utils/jsonPath";
import type { KubeObjectMetadata, KubeObjectStatus } from "../../../common/k8s-api/kube-object"; import type { KubeObjectMetadata, KubeObjectStatus } from "../../../common/k8s-api/kube-object";
import { KubeObject } from "../../../common/k8s-api/kube-object"; import { KubeObject } from "../../../common/k8s-api/kube-object";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
import { JSONPath } from "@astronautlabs/jsonpath";
export interface CustomResourceDetailsProps extends KubeObjectDetailsProps<KubeObject> { export interface CustomResourceDetailsProps extends KubeObjectDetailsProps<KubeObject> {
crd: CustomResourceDefinition; crd: CustomResourceDefinition;
@ -48,9 +48,9 @@ function convertSpecValue(value: any): any {
@observer @observer
export class CustomResourceDetails extends React.Component<CustomResourceDetailsProps> { export class CustomResourceDetails extends React.Component<CustomResourceDetailsProps> {
renderAdditionalColumns(resource: KubeObject, columns: AdditionalPrinterColumnsV1[]) { renderAdditionalColumns(resource: KubeObject, columns: AdditionalPrinterColumnsV1[]) {
return columns.map(({ name, jsonPath: jp }) => ( return columns.map(({ name, jsonPath }) => (
<DrawerItem key={name} name={name} renderBoolean> <DrawerItem key={name} name={name} renderBoolean>
{convertSpecValue(jsonPath.value(resource, parseJsonPath(jp.slice(1))))} {convertSpecValue(JSONPath.query(resource, convertKubectlJsonPathToNodeJsonPath(jsonPath)))}
</DrawerItem> </DrawerItem>
)); ));
} }

View File

@ -6,14 +6,13 @@
import "./crd-resources.scss"; import "./crd-resources.scss";
import React from "react"; import React from "react";
import { value } from "jsonpath";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { KubeObjectListLayout } from "../kube-object-list-layout"; import { KubeObjectListLayout } from "../kube-object-list-layout";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { computed, makeObservable } from "mobx"; import { computed, makeObservable } from "mobx";
import { crdStore } from "./crd.store"; import { crdStore } from "./crd.store";
import { apiManager } from "../../../common/k8s-api/api-manager"; import { apiManager } from "../../../common/k8s-api/api-manager";
import { parseJsonPath } from "../../utils/jsonPath"; import { safeJSONPathValue } from "../../utils/jsonPath";
import { TabLayout } from "../layout/tab-layout-2"; import { TabLayout } from "../layout/tab-layout-2";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import customResourcesRouteParametersInjectable from "./custom-resources-route-parameters.injectable"; import customResourcesRouteParametersInjectable from "./custom-resources-route-parameters.injectable";
@ -70,7 +69,7 @@ class NonInjectedCrdResources extends React.Component<Dependencies> {
[columnId.age]: customResource => -customResource.getCreationTimestamp(), [columnId.age]: customResource => -customResource.getCreationTimestamp(),
...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [ ...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [
name, name,
customResource => value(customResource, parseJsonPath(jsonPath.slice(1))), customResource => safeJSONPathValue(customResource, jsonPath),
])), ])),
}} }}
searchFilters={[ searchFilters={[
@ -95,22 +94,11 @@ class NonInjectedCrdResources extends React.Component<Dependencies> {
})), })),
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]} ]}
renderTableContents={crdInstance => [ renderTableContents={customResource => [
crdInstance.getName(), customResource.getName(),
isNamespaced && crdInstance.getNs(), isNamespaced && customResource.getNs(),
...extraColumns.map((column) => { ...extraColumns.map((column) => safeJSONPathValue(customResource, column.jsonPath)),
let rawValue = value(crdInstance, parseJsonPath(column.jsonPath.slice(1))); <KubeObjectAge key="age" object={customResource} />,
if (Array.isArray(rawValue) || typeof rawValue === "object") {
rawValue = JSON.stringify(rawValue);
}
return {
renderBoolean: true,
children: rawValue,
};
}),
<KubeObjectAge key="age" object={crdInstance} />,
]} ]}
failedToLoadMessage={( failedToLoadMessage={(
<> <>

View File

@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import path from "path"; import path from "path";
import { hasCorrectExtension } from "./has-correct-extension"; import { hasCorrectExtension } from "./has-correct-extension";
import "../../../../common/vars"; import { staticFilesDirectory } from "../../../../common/vars";
import readFileInjectable from "../../../../common/fs/read-file.injectable"; import readFileInjectable from "../../../../common/fs/read-file.injectable";
import readDirInjectable from "../../../../common/fs/read-dir.injectable"; import readDirInjectable from "../../../../common/fs/read-dir.injectable";
import type { RawTemplates } from "./create-resource-templates.injectable"; import type { RawTemplates } from "./create-resource-templates.injectable";
@ -19,7 +19,7 @@ interface Dependencies {
} }
async function getTemplates({ readDir, readFile, getAbsolutePath }: Dependencies) { async function getTemplates({ readDir, readFile, getAbsolutePath }: Dependencies) {
const templatesFolder = getAbsolutePath(__static, "../templates/create-resource"); const templatesFolder = getAbsolutePath(staticFilesDirectory, "../templates/create-resource");
/** /**
* Mapping between file names and their contents * Mapping between file names and their contents

View File

@ -26,7 +26,7 @@
&:first-of-type { &:first-of-type {
padding-left: 1.5rem; padding-left: 1.5rem;
& > * { & > *:not(:empty) {
margin-right: 1rem; margin-right: 1rem;
} }
} }
@ -34,7 +34,7 @@
&:last-of-type { &:last-of-type {
padding-right: 1.5rem; padding-right: 1.5rem;
& > * { & > *:not(:empty) {
margin-left: 1rem; margin-left: 1rem;
} }
} }

View File

@ -9,12 +9,12 @@ import { observer } from "mobx-react";
import { action, computed, makeObservable, observable, reaction } from "mobx"; import { action, computed, makeObservable, observable, reaction } from "mobx";
import { editor, Uri } from "monaco-editor"; import { editor, Uri } from "monaco-editor";
import type { MonacoTheme } from "./monaco-themes"; import type { MonacoTheme } from "./monaco-themes";
import type { MonacoValidator } from "./monaco-validators"; import { type MonacoValidator, monacoValidators } from "./monaco-validators";
import { monacoValidators } from "./monaco-validators";
import { debounce, merge } from "lodash"; import { debounce, merge } from "lodash";
import { cssNames, disposer } from "../../utils"; import { autoBind, cssNames, disposer } from "../../utils";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { ThemeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import logger from "../../../main/logger";
export type MonacoEditorId = string; export type MonacoEditorId = string;
@ -45,32 +45,28 @@ export const defaultEditorProps: Partial<MonacoEditorProps> = {
@observer @observer
export class MonacoEditor extends React.Component<MonacoEditorProps> { export class MonacoEditor extends React.Component<MonacoEditorProps> {
static defaultProps = defaultEditorProps as object; static readonly defaultProps = defaultEditorProps as object;
static viewStates = new WeakMap<Uri, editor.ICodeEditorViewState>(); static readonly viewStates = new WeakMap<Uri, editor.ICodeEditorViewState>();
static createUri(id: MonacoEditorId): Uri { static createUri(id: MonacoEditorId): Uri {
return Uri.file(`/monaco-editor/${id}`); return Uri.file(`/monaco-editor/${id}`);
} }
public staticId = `editor-id#${Math.round(1e7 * Math.random())}`; private staticId = `editor-id#${Math.round(1e7 * Math.random())}`;
public dispose = disposer(); private dispose = disposer();
// TODO: investigate how to replace with "common/logger"
// currently leads for stucking UI forever & infinite loop.
// e.g. happens on tab change/create, maybe some other cases too.
logger = console;
@observable.ref containerElem: HTMLElement; @observable.ref containerElem: HTMLElement;
@observable.ref editor: editor.IStandaloneCodeEditor; @observable.ref editor: editor.IStandaloneCodeEditor;
@observable dimensions: { width?: number; height?: number } = {}; @observable readonly dimensions: { width?: number; height?: number } = {};
@observable unmounting = false; @observable private unmounting = false;
constructor(props: MonacoEditorProps) { constructor(props: MonacoEditorProps) {
super(props); super(props);
makeObservable(this); makeObservable(this);
autoBind(this);
} }
@computed get id() { @computed get id(): MonacoEditorId {
return this.props.id ?? this.staticId; return this.props.id ?? this.staticId;
} }
@ -94,7 +90,8 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
); );
} }
@computed get logMetadata() { @computed
private get logMetadata() {
return { return {
editorId: this.id, editorId: this.id,
model: this.model, model: this.model,
@ -121,8 +118,8 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
return () => resizeObserver.unobserve(containerElem); return () => resizeObserver.unobserve(containerElem);
} }
onModelChange = (model: editor.ITextModel, oldModel?: editor.ITextModel) => { protected onModelChange(model: editor.ITextModel, oldModel?: editor.ITextModel) {
this.logger?.info("[MONACO]: model change", { model, oldModel }, this.logMetadata); logger.info("[MONACO]: model change", { model, oldModel }, this.logMetadata);
if (oldModel) { if (oldModel) {
this.saveViewState(oldModel); this.saveViewState(oldModel);
@ -134,18 +131,17 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
this.editor.focus(); // keep focus in editor, e.g. when clicking between dock-tabs this.editor.focus(); // keep focus in editor, e.g. when clicking between dock-tabs
this.props.onModelChange?.(model, oldModel); this.props.onModelChange?.(model, oldModel);
this.validateLazy(); this.validateLazy();
}; }
/** /**
* Save current view-model state in the editor. * Save current view-model state in the editor.
* This will allow restore cursor position, selected text, etc. * This will allow restore cursor position, selected text, etc.
* @param {editor.ITextModel} model
*/ */
private saveViewState(model: editor.ITextModel) { protected saveViewState(model: editor.ITextModel) {
MonacoEditor.viewStates.set(model.uri, this.editor.saveViewState()); MonacoEditor.viewStates.set(model.uri, this.editor.saveViewState());
} }
private restoreViewState(model: editor.ITextModel) { protected restoreViewState(model: editor.ITextModel) {
const viewState = MonacoEditor.viewStates.get(model.uri); const viewState = MonacoEditor.viewStates.get(model.uri);
if (viewState) { if (viewState) {
@ -156,9 +152,9 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
componentDidMount() { componentDidMount() {
try { try {
this.createEditor(); this.createEditor();
this.logger?.info(`[MONACO]: editor did mount`, this.logMetadata); logger.info(`[MONACO]: editor did mount`, this.logMetadata);
} catch (error) { } catch (error) {
this.logger?.error(`[MONACO]: mounting failed: ${error}`, this.logMetadata); logger.error(`[MONACO]: mounting failed: ${error}`, this.logMetadata);
} }
} }
@ -168,7 +164,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
this.destroy(); this.destroy();
} }
private createEditor() { protected createEditor() {
if (!this.containerElem || this.editor || this.unmounting) { if (!this.containerElem || this.editor || this.unmounting) {
return; return;
} }
@ -184,7 +180,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
...this.options, ...this.options,
}); });
this.logger?.info(`[MONACO]: editor created for language=${language}, theme=${theme}`, this.logMetadata); logger.info(`[MONACO]: editor created for language=${language}, theme=${theme}`, this.logMetadata);
this.validateLazy(); // validate initial value this.validateLazy(); // validate initial value
this.restoreViewState(this.model); // restore previous state if any this.restoreViewState(this.model); // restore previous state if any
@ -250,7 +246,8 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
this.editor?.focus(); this.editor?.focus();
} }
validate = action((value = this.getValue()) => { @action
validate(value = this.getValue()) {
const validators: MonacoValidator[] = [ const validators: MonacoValidator[] = [
monacoValidators[this.props.language], // parsing syntax check monacoValidators[this.props.language], // parsing syntax check
].filter(Boolean); ].filter(Boolean);
@ -262,19 +259,21 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
this.props.onError?.(error); // emit error outside this.props.onError?.(error); // emit error outside
} }
} }
}); }
// avoid excessive validations during typing // avoid excessive validations during typing
validateLazy = debounce(this.validate, 250); validateLazy = debounce(this.validate, 250);
bindRef = (elem: HTMLElement) => this.containerElem = elem; protected bindRef(elem: HTMLElement) {
this.containerElem = elem;
}
render() { render() {
const { className, style } = this.props; const { className, style } = this.props;
return ( return (
<div <div
data-test-component="monaco-editor" data-test-id="monaco-editor"
className={cssNames(styles.MonacoEditor, className)} className={cssNames(styles.MonacoEditor, className)}
style={style} style={style}
ref={this.bindRef} ref={this.bindRef}

View File

@ -3,44 +3,166 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { parseJsonPath } from "../jsonPath"; import { convertKubectlJsonPathToNodeJsonPath, safeJSONPathValue } from "../jsonPath";
describe("parseJsonPath", () => { describe("parseJsonPath", () => {
test("should convert \\. to use indexed notation", () => { it("should convert \\. to use indexed notation", () => {
const res = parseJsonPath(".metadata.labels.kubesphere\\.io/alias-name"); const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels.kubesphere\\.io/alias-name");
expect(res).toBe(".metadata.labels['kubesphere.io/alias-name']"); expect(res).toBe("$.metadata.labels['kubesphere.io/alias-name']");
}); });
test("should convert keys with escaped characters to use indexed notation", () => { it("should convert keys with escaped characters to use indexed notation", () => {
const res = parseJsonPath(".metadata.labels.kubesphere\\\"io/alias-name"); const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels.kubesphere\\\"io/alias-name");
expect(res).toBe(".metadata.labels['kubesphere\"io/alias-name']"); expect(res).toBe("$.metadata.labels['kubesphere\"io/alias-name']");
}); });
test("should convert '-' to use indexed notation", () => { it("should convert '-' to use indexed notation", () => {
const res = parseJsonPath(".metadata.labels.alias-name"); const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels.alias-name");
expect(res).toBe(".metadata.labels['alias-name']"); expect(res).toBe("$.metadata.labels['alias-name']");
}); });
test("should handle scenario when both \\. and indexed notation are present", () => { it("should drop leading dot if first group is converted to index notation", () => {
const rest = parseJsonPath(".metadata.labels\\.serving['some.other.item']"); const res = convertKubectlJsonPathToNodeJsonPath(".metadata\\.labels.alias-name");
expect(rest).toBe(".metadata['labels.serving']['some.other.item']"); expect(res).toBe("$['metadata.labels']['alias-name']");
}); });
it("should handle scenario when both \\. and indexed notation are present", () => {
const rest = convertKubectlJsonPathToNodeJsonPath(".metadata.labels\\.serving['some.other.item']");
test("should not touch given jsonPath if no invalid characters present", () => { expect(rest).toBe("$.metadata['labels.serving']['some.other.item']");
const res = parseJsonPath(".status.conditions[?(@.type=='Ready')].status");
expect(res).toBe(".status.conditions[?(@.type=='Ready')].status");
}); });
test("strips '\\' away from the result", () => { it("should not touch given jsonPath if no invalid characters present", () => {
const res = parseJsonPath(".metadata.labels['serving\\.knative\\.dev/configuration']"); const res = convertKubectlJsonPathToNodeJsonPath(".status.conditions[?(@.type=='Ready')].status");
expect(res).toBe(".metadata.labels['serving.knative.dev/configuration']"); expect(res).toBe("$.status.conditions[?(@.type=='Ready')].status");
}); });
it("strips '\\' away from the result", () => {
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels['serving\\.knative\\.dev/configuration']");
expect(res).toBe("$.metadata.labels['serving.knative.dev/configuration']");
});
it("converts all [] to [0]", () => {
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels[].foo[]");
expect(res).toBe("$.metadata.labels[0].foo[0]");
});
it("converts ending .. to ..*", () => {
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels[]..");
expect(res).toBe("$.metadata.labels[0]");
});
it("converts ending ...name to ..name", () => {
const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels[]...name");
expect(res).toBe("$.metadata.labels[0]..name");
});
});
describe("safeJSONPathValue", () => {
let oldWarn: typeof console["warn"];
beforeEach(() => {
oldWarn = console.warn;
console.warn = jest.fn();
});
afterEach(() => {
console.warn = oldWarn;
});
it("should convert boolean values to strings", () => {
const res = safeJSONPathValue({ bar: false }, ".bar");
expect(res).toBe("false");
});
it("should convert number values to strings", () => {
const res = safeJSONPathValue({ bar: 0 }, ".bar");
expect(res).toBe("0");
});
it("should join sliced entries with commas only", () => {
const res = safeJSONPathValue({
bar: [
{
foo: 1,
},
{
foo: "hello",
},
],
}, ".bar[].foo");
expect(res).toBe("1");
});
it("should join an array of values using JSON.stringify", () => {
const res = safeJSONPathValue({
bar: [
"world",
"hello",
],
}, ".bar");
expect(res).toBe(`["world","hello"]`);
});
it("should stringify an object value", () => {
const res = safeJSONPathValue({
foo: { bar: "bat" },
}, ".foo");
expect(res).toBe(`{"bar":"bat"}`);
});
it("should use convertKubectlJsonPathToNodeJsonPath", () => {
const res = safeJSONPathValue({
foo: { "hello.world": "bat" },
}, ".foo.hello\\.world");
expect(res).toBe("bat");
});
it("should not throw when given '.spec.metrics[*].external.highWatermark..'", () => {
const obj = {
spec: {
metrics: [
{
external: {
metricName: "cpu",
highWatermark: "100",
},
},
{
external: {
metricName: "memory",
highWatermark: "100",
},
},
],
},
};
const res = safeJSONPathValue(obj, ".spec.metrics[*].external.highWatermark..");
expect(res).toBe("100, 100");
});
it("should not throw if path is invalid jsonpath", () => {
const res = safeJSONPathValue({
foo: { "hello.world": "bat" },
}, "asd[");
expect(res).toBe("<unknown>");
});
}); });

View File

@ -3,28 +3,63 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
// Helper to convert strings used for jsonPath where \. or - is present to use indexed notation, import { JSONPath } from "@astronautlabs/jsonpath";
// for example: .metadata.labels.kubesphere\.io/alias-name -> .metadata.labels['kubesphere\.io/alias-name']
export function parseJsonPath(jsonPath: string) { const slashDashSearch = /[\\-]/g;
let pathExpression = jsonPath; const pathByBareDots = /(?<=\w)\./;
const textBeforeFirstSquare = /^.*(?=\[)/g;
const backSlash = /\\/g;
const kubectlOptionPrefix = /^\$?\.?(?<pathExpression>.*)/;
const sliceVersion = /\[]/g;
const tripleDotName = /\.\.\.(?<trailing>.)/g;
const trailingDotDot = /\.\.$/;
if (jsonPath.match(/[\\-]/g)) { // search for '\' and '-' /**
const [first, ...rest] = jsonPath.split(/(?<=\w)\./); // split jsonPath by '.' (\. cases are ignored) * The GO package that kubectl and kubernetes uses for its JSONpath implementation has some
* shorthand conveniences that are not part of the official spec. This function tries to convert
* those shorthands to the official spec.
*
* Known shorthands:
* - Leading `$` is optional (but implied)
* - The string `\.` is used to denote the "value of '.'" and not "next key"
* - The string `-` can be used while not in quotes
* - `[]` as shorthand for `[0]`
* - Remove `..` at the end of a path, we will just format it slightly differently
* - Allow `...foo` as well as `..foo`
*/
export function convertKubectlJsonPathToNodeJsonPath(jsonPath: string) {
const startMatch = jsonPath.match(kubectlOptionPrefix);
let start = "$";
if (!startMatch) {
return start;
}
let { pathExpression } = startMatch.groups;
if (pathExpression.match(slashDashSearch)) {
const [first, ...rest] = pathExpression.split(pathByBareDots);
pathExpression = `${convertToIndexNotation(first, true)}${rest.map(value => convertToIndexNotation(value)).join("")}`; pathExpression = `${convertToIndexNotation(first, true)}${rest.map(value => convertToIndexNotation(value)).join("")}`;
} }
pathExpression = pathExpression.replace(trailingDotDot, "");
pathExpression = pathExpression.replace(tripleDotName, "..$<trailing>");
if (!pathExpression.startsWith("[")) {
start += ".";
}
// strip '\' characters from the result // strip '\' characters from the result
return pathExpression.replace(/\\/g, ""); return `${start}${pathExpression.replace(backSlash, "").replace(sliceVersion, "[0]")}`;
} }
function convertToIndexNotation(key: string, firstItem = false) { function convertToIndexNotation(key: string, firstItem = false) {
if (key.match(/[\\-]/g)) { // check if found '\' and '-' in key if (key.match(slashDashSearch)) {
if (key.includes("[")) { // handle cases where key contains [...] if (key.includes("[")) { // handle cases where key contains [...]
const keyToConvert = key.match(/^.*(?=\[)/g); // get the text from the key before '[' const keyToConvert = key.match(textBeforeFirstSquare); // get the text from the key before '['
if (keyToConvert && keyToConvert[0].match(/[\\-]/g)) { // check if that part contains illegal characters if (keyToConvert && keyToConvert[0].match(slashDashSearch)) { // check if that part contains illegal characters
return key.replace(keyToConvert[0], `['${keyToConvert[0]}']`); // surround key with '[' and ']' return key.replace(keyToConvert[0], `['${keyToConvert[0]}']`); // surround key with '[' and ']'
} else { } else {
return `.${key}`; // otherwise return as is with leading '.' return `.${key}`; // otherwise return as is with leading '.'
@ -38,3 +73,35 @@ function convertToIndexNotation(key: string, firstItem = false) {
return `${prefix}${key}`; return `${prefix}${key}`;
} }
} }
function formatJSONValue(value: unknown) {
if (typeof value === "object") {
return JSON.stringify(value);
}
return String(value);
}
/**
* This function is a safer version of `JSONPath.value(obj, path)` with untrusted jsonpath strings
*
* This function will also stringify the value retreived from the object
*/
export function safeJSONPathValue(obj: object, path: string): string {
try {
const parsedPath = JSONPath.parse(convertKubectlJsonPathToNodeJsonPath(path));
const isSlice = parsedPath.some((exp: any) => exp.expression.type === "slice" || "wildcard");
const value = JSONPath.query(obj, JSONPath.stringify(parsedPath), isSlice ? Infinity : 1);
if (isSlice) {
return value.map(formatJSONValue).join(", ");
}
return formatJSONValue(value[0]);
} catch (error) {
// something failed
console.warn("[JSON-PATH]: failed to parse jsonpath", error);
return "<unknown>";
}
}

View File

@ -32,7 +32,11 @@
"types/*" "types/*"
] ]
}, },
"plugins": [{ "name": "typescript-plugin-css-modules" }] "plugins": [
{
"name": "typescript-plugin-css-modules"
}
]
}, },
"ts-node": { "ts-node": {
"transpileOnly": true, "transpileOnly": true,
@ -42,7 +46,7 @@
}, },
"include": [ "include": [
"src/**/*", "src/**/*",
"types/*.d.ts" "types/*.d.ts",
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",

3
types/mocks.d.ts vendored
View File

@ -8,9 +8,6 @@ declare module "win-ca/api"
declare module "@hapi/call" declare module "@hapi/call"
declare module "@hapi/subtext" declare module "@hapi/subtext"
// Global path to static assets
declare const __static: string;
// Support import for custom module extensions // Support import for custom module extensions
// https://www.typescriptlang.org/docs/handbook/modules.html#wildcard-module-declarations // https://www.typescriptlang.org/docs/handbook/modules.html#wildcard-module-declarations
declare module "*.module.scss" { declare module "*.module.scss" {

View File

@ -1,54 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import Webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import { webpackLensRenderer } from "./webpack.renderer";
import { buildDir, webpackDevServerPort } from "./src/common/vars";
import logger from "./src/common/logger";
/**
* Creates `webpack-dev-server`
* API docs:
* @url https://webpack.js.org/configuration/dev-server/
* @url https://github.com/chimurai/http-proxy-middleware
*/
function createDevServer(): WebpackDevServer {
const config = webpackLensRenderer({ showVars: false });
const compiler = Webpack(config);
const server = new WebpackDevServer({
setupExitSignals: true,
headers: {
"Access-Control-Allow-Origin": "*",
},
allowedHosts: "all",
host: "localhost",
port: webpackDevServerPort,
static: buildDir, // aka `devServer.contentBase` in webpack@4
hot: "only", // use HMR only without errors
liveReload: false,
devMiddleware: {
writeToDisk: false,
index: "OpenLensDev.html",
publicPath: "/build",
},
proxy: {
"^/$": "/build/",
},
client: {
overlay: false, // don't show warnings and errors on top of rendered app view
logging: "error",
},
}, compiler);
logger.info(`[WEBPACK-DEV-SERVER]: created with options`, server.options);
return server;
}
const server = createDevServer();
server.start();

47
webpack/dev-server.ts Normal file
View File

@ -0,0 +1,47 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import Webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import { webpackLensRenderer } from "./renderer";
import logger from "../src/common/logger";
import { buildDir, webpackDevServerPort } from "./vars";
/**
* API docs:
* @url https://webpack.js.org/configuration/dev-server/
* @url https://github.com/chimurai/http-proxy-middleware
*/
const config = webpackLensRenderer({ showVars: false });
const compiler = Webpack(config);
const server = new WebpackDevServer({
setupExitSignals: true,
headers: {
"Access-Control-Allow-Origin": "*",
},
allowedHosts: "all",
host: "localhost",
port: webpackDevServerPort,
static: buildDir, // aka `devServer.contentBase` in webpack@4
hot: "only", // use HMR only without errors
liveReload: false,
devMiddleware: {
writeToDisk: false,
index: "OpenLensDev.html",
publicPath: "/build",
},
proxy: {
"^/$": "/build/",
},
client: {
overlay: false, // don't show warnings and errors on top of rendered app view
logging: "error",
},
}, compiler);
logger.info(`[WEBPACK-DEV-SERVER]: created with options`, server.options);
server.start();

View File

@ -6,25 +6,21 @@
import path from "path"; import path from "path";
import type webpack from "webpack"; import type webpack from "webpack";
import * as vars from "./src/common/vars"; import { cssModulesWebpackRule, fontsLoaderWebpackRules, iconsAndImagesWebpackRules } from "./renderer";
import { cssModulesWebpackRule, fontsLoaderWebpackRules, iconsAndImagesWebpackRules } from "./webpack.renderer"; import { extensionEntry, extensionOutDir, isDevelopment } from "./vars";
export default function generateExtensionTypes(): webpack.Configuration { export default function generateExtensionTypes(): webpack.Configuration {
const { isDevelopment } = vars;
const entry = "./src/extensions/extension-api.ts";
const outDir = "./src/extensions/npm/extensions/dist";
return { return {
// Compile for Electron for renderer process // Compile for Electron for renderer process
// see <https://webpack.js.org/configuration/target/> // see <https://webpack.js.org/configuration/target/>
target: "electron-renderer", target: "electron-renderer",
entry, entry: extensionEntry,
// this is the default mode, so we should make it explicit to silence the warning // this is the default mode, so we should make it explicit to silence the warning
mode: isDevelopment ? "development" : "production", mode: isDevelopment ? "development" : "production",
output: { output: {
filename: "extension-api.js", filename: "extension-api.js",
// need to be an absolute path // need to be an absolute path
path: path.resolve(__dirname, `${outDir}/src/extensions`), path: path.resolve(extensionOutDir, "src", "extensions"),
// can be use in commonjs environments // can be use in commonjs environments
// e.g. require('@k8slens/extensions') // e.g. require('@k8slens/extensions')
libraryTarget: "commonjs", libraryTarget: "commonjs",
@ -54,7 +50,7 @@ export default function generateExtensionTypes(): webpack.Configuration {
compilerOptions: { compilerOptions: {
declaration: true, // output .d.ts declaration: true, // output .d.ts
sourceMap: false, // to override sourceMap: true in tsconfig.json sourceMap: false, // to override sourceMap: true in tsconfig.json
outDir, // where the .d.ts should be located outDir: extensionOutDir, // where the .d.ts should be located
}, },
}, },
}, },

View File

@ -11,7 +11,7 @@ import type { Options as TSLoaderOptions } from "ts-loader";
* depends on env LENS_DEV_USE_ESBUILD_LOADER to use esbuild-loader (faster) or good-old ts-loader * depends on env LENS_DEV_USE_ESBUILD_LOADER to use esbuild-loader (faster) or good-old ts-loader
* @returns ts/tsx webpack loader configuration object * @returns ts/tsx webpack loader configuration object
*/ */
const getTSLoader = (options: Partial<TSLoaderOptions> = {}, testRegExp?: RegExp) => { export default function getTypescriptLoader(options: Partial<TSLoaderOptions> = {}, testRegExp?: RegExp) {
testRegExp ??= /\.tsx?$/; // by default covers react/jsx-stuff testRegExp ??= /\.tsx?$/; // by default covers react/jsx-stuff
options.transpileOnly ??= true; options.transpileOnly ??= true;
@ -37,6 +37,4 @@ const getTSLoader = (options: Partial<TSLoaderOptions> = {}, testRegExp?: RegExp
options, options,
}, },
}; };
}; }
export default getTSLoader;

View File

@ -7,16 +7,20 @@ import path from "path";
import type webpack from "webpack"; import type webpack from "webpack";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"; import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
import nodeExternals from "webpack-node-externals"; import nodeExternals from "webpack-node-externals";
import * as vars from "./src/common/vars"; import getTypeScriptLoader from "./get-typescript-loader";
import getTSLoader from "./src/common/getTSLoader";
import CircularDependencyPlugin from "circular-dependency-plugin"; import CircularDependencyPlugin from "circular-dependency-plugin";
import { iconsAndImagesWebpackRules } from "./webpack.renderer"; import { iconsAndImagesWebpackRules } from "./renderer";
import type { WebpackPluginInstance } from "webpack";
import { buildDir, isDevelopment, mainDir } from "./vars";
const configs: { (): webpack.Configuration }[] = []; const configs: { (): webpack.Configuration }[] = [];
configs.push((): webpack.Configuration => { configs.push((): webpack.Configuration => {
console.info("WEBPACK:main", { ...vars }); console.info("WEBPACK:main", {
const { mainDir, buildDir, isDevelopment } = vars; isDevelopment,
mainDir,
buildDir,
});
return { return {
name: "lens-app-main", name: "lens-app-main",
@ -44,7 +48,7 @@ configs.push((): webpack.Configuration => {
test: /\.node$/, test: /\.node$/,
use: "node-loader", use: "node-loader",
}, },
getTSLoader({}, /\.ts$/), getTypeScriptLoader({}, /\.ts$/),
...iconsAndImagesWebpackRules(), ...iconsAndImagesWebpackRules(),
], ],
}, },
@ -54,8 +58,8 @@ configs.push((): webpack.Configuration => {
cwd: __dirname, cwd: __dirname,
exclude: /node_modules/, exclude: /node_modules/,
failOnError: true, failOnError: true,
}), }) as unknown as WebpackPluginInstance,
].filter(Boolean), ],
}; };
}); });

View File

@ -3,25 +3,31 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import * as vars from "./src/common/vars";
import path from "path"; import path from "path";
import type webpack from "webpack"; import type webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin"; import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin"; import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
import MonacoWebpackPlugin from "monaco-editor-webpack-plugin"; import MonacoWebpackPlugin from "monaco-editor-webpack-plugin";
import getTSLoader from "./src/common/getTSLoader";
import CircularDependencyPlugin from "circular-dependency-plugin"; import CircularDependencyPlugin from "circular-dependency-plugin";
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin"; import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
import type { WebpackPluginInstance } from "webpack";
import getTypescriptLoader from "./get-typescript-loader";
import { assetsFolderName, isDevelopment, rendererDir, buildDir, appName, htmlTemplate, publicPath, sassCommonVars } from "./vars";
export function webpackLensRenderer({ showVars = true } = {}): webpack.Configuration { export function webpackLensRenderer({ showVars = true } = {}): webpack.Configuration {
if (showVars) { if (showVars) {
console.info("WEBPACK:renderer", { ...vars }); console.info("WEBPACK:renderer", {
assetsFolderName,
isDevelopment,
rendererDir,
buildDir,
appName,
htmlTemplate,
publicPath,
});
} }
const assetsFolderName = "assets";
const { appName, buildDir, htmlTemplate, isDevelopment, publicPath, rendererDir } = vars;
return { return {
target: "electron-renderer", target: "electron-renderer",
name: "lens-app-renderer", name: "lens-app-renderer",
@ -67,7 +73,7 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
test: /\.node$/, test: /\.node$/,
use: "node-loader", use: "node-loader",
}, },
getTSLoader({ getTypescriptLoader({
getCustomTransformers: () => ({ getCustomTransformers: () => ({
before: isDevelopment ? [require("react-refresh-typescript")()] : [], before: isDevelopment ? [require("react-refresh-typescript")()] : [],
}), }),
@ -103,14 +109,18 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
cwd: __dirname, cwd: __dirname,
exclude: /node_modules/, exclude: /node_modules/,
failOnError: true, failOnError: true,
}), }) as unknown as WebpackPluginInstance,
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "[name].css", filename: "[name].css",
}), }),
isDevelopment && new ReactRefreshWebpackPlugin(), ...(
].filter(Boolean), isDevelopment
? [new ReactRefreshWebpackPlugin()]
: []
),
],
}; };
} }
@ -143,15 +153,17 @@ export function fontsLoaderWebpackRules(): webpack.RuleSetRule[] {
]; ];
} }
export interface CssModulesWebpackRuleOptions {
styleLoader?: string;
}
/** /**
* Import CSS or SASS styles with modules support (*.module.scss) * Import CSS or SASS styles with modules support (*.module.scss)
* @param {string} styleLoader
*/ */
export function cssModulesWebpackRule( export function cssModulesWebpackRule({ styleLoader }: CssModulesWebpackRuleOptions = {}): webpack.RuleSetRule {
{ styleLoader ??= isDevelopment
styleLoader = vars.isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader, ? "style-loader"
} = {}): webpack.RuleSetRule { : MiniCssExtractPlugin.loader;
const { isDevelopment, sassCommonVars } = vars;
return { return {
test: /\.s?css$/, test: /\.s?css$/,

3
webpack/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../tsconfig.json"
}

25
webpack/vars.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import assert from "assert";
import path from "path";
import packageInfo from "../package.json";
export const isDevelopment = process.env.NODE_ENV !== "production";
export const mainDir = path.join(process.cwd(), "src", "main");
export const buildDir = path.join(process.cwd(), "static", "build");
export const extensionEntry = path.join(process.cwd(), "src", "extensions", "extension-api.ts");
export const extensionOutDir = path.join(process.cwd(), "src", "extensions", "npm", "extensions", "dist");
export const assetsFolderName = "assets";
export const rendererDir = path.join(process.cwd(), "src", "renderer");
export const appName = isDevelopment
? `${packageInfo.productName}Dev`
: packageInfo.productName;
export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const publicPath = "/build/";
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
export const webpackDevServerPort = Number(process.env.WEBPACK_DEV_SERVER_PORT) || 9191;
assert(Number.isInteger(webpackDevServerPort), "WEBPACK_DEV_SERVER_PORT environment variable must only be an integer");

864
yarn.lock

File diff suppressed because it is too large Load Diff