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
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
id: minikube
uses: medyagh/setup-minikube@5a9a7104d7322fa40424de8855c84685e89cefd7

View File

@ -28,6 +28,9 @@ compile-dev: node_modules
yarn compile:main --cache
yarn compile:renderer --cache
.PHONY: validate-dev
ci-validate-dev: binaries/client build-extensions compile-dev
.PHONY: dev
dev: binaries/client build-extensions
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 monacoEditor = await frame.waitForSelector(
`.Dock.isOpen [data-test-component="monaco-editor"]`,
);
const monacoEditor = await frame.waitForSelector(`.Dock.isOpen [data-test-id="monaco-editor"]`);
await monacoEditor.click();
await monacoEditor.type("apiVersion: v1", { delay: 10 });

View File

@ -17,11 +17,11 @@
"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: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:main": "yarn run webpack --config webpack.main.ts",
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
"compile:extension-types": "yarn run webpack --config webpack.extensions.ts",
"compile:main": "yarn run webpack --config webpack/main.ts",
"compile:renderer": "yarn run webpack --config webpack/renderer.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-package-version": "yarn run ts-node build/set_npm_version.ts",
"build:linux": "yarn run compile && electron-builder --linux --dir",
@ -199,6 +199,7 @@
}
},
"dependencies": {
"@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.3",
@ -229,7 +230,6 @@
"joi": "^17.6.0",
"js-yaml": "^4.1.0",
"jsdom": "^16.7.0",
"jsonpath": "^1.1.1",
"lodash": "^4.17.15",
"mac-ca": "^1.0.6",
"marked": "^4.0.12",
@ -239,7 +239,7 @@
"mobx-react": "^7.3.0",
"mobx-utils": "^6.0.4",
"mock-fs": "^5.1.2",
"moment": "^2.29.1",
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"monaco-editor": "^0.29.1",
"monaco-editor-webpack-plugin": "^5.0.0",
@ -259,7 +259,7 @@
"request-promise-native": "^1.0.9",
"rfc6902": "^4.0.2",
"selfsigned": "^2.0.1",
"semver": "^7.3.2",
"semver": "^7.3.7",
"shell-env": "^3.0.1",
"spdy": "^4.0.2",
"tar": "^6.1.11",
@ -278,7 +278,7 @@
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@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",
"@testing-library/jest-dom": "^5.16.3",
"@testing-library/react": "^12.1.4",
@ -299,7 +299,6 @@
"@types/jest": "^26.0.24",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^16.2.14",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.181",
"@types/marked": "^4.0.3",
"@types/md5-file": "^4.0.2",
@ -313,7 +312,7 @@
"@types/react": "^17.0.43",
"@types/react-beautiful-dnd": "^13.1.2",
"@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-table": "^7.7.9",
"@types/react-virtualized-auto-sizer": "^1.0.1",
@ -347,15 +346,15 @@
"deepdash": "^5.3.9",
"dompurify": "^2.3.6",
"electron": "^14.2.9",
"electron-builder": "^22.14.13",
"electron-builder": "^23.0.3",
"electron-notarize": "^0.3.0",
"esbuild": "^0.14.34",
"esbuild": "^0.14.38",
"esbuild-loader": "^2.18.0",
"eslint": "^8.11.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.26.0",
"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",
"flex.box": "^3.4.4",
"fork-ts-checker-webpack-plugin": "^6.5.0",
@ -380,9 +379,9 @@
"postcss-loader": "^6.2.1",
"randomcolor": "^0.6.2",
"react-beautiful-dnd": "^13.1.0",
"react-refresh": "^0.11.0",
"react-refresh": "^0.12.0",
"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-event": "^5.3.0",
"react-table": "^7.7.0",
@ -403,7 +402,7 @@
"typeface-roboto": "^1.1.13",
"typescript": "^4.5.5",
"typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.69.0",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1",
"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 "./convertMemory";
export * from "./debouncePromise";
export * from "./defineGlobal";
export * from "./delay";
export * from "./disposer";
export * from "./downloadFile";

View File

@ -7,7 +7,6 @@
import path from "path";
import { SemVer } from "semver";
import packageInfo from "../../package.json";
import { defineGlobal } from "./utils/defineGlobal";
import { lazyInitialized } from "./utils/lazy-initialized";
export const isMac = process.platform === "darwin";
@ -101,26 +100,12 @@ export const kubectlBinaryName = getBinaryName("kubectl");
* @deprecated for being explicit side effect.
*/
export const kubectlBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), kubectlBinaryName));
// Webpack build paths
export const contextDir = process.cwd();
export const buildDir = path.join(contextDir, "static", publicPath);
export const preloadEntrypoint = path.join(contextDir, "src/preload.ts");
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");
},
});
export const staticFilesDirectory = path.resolve(
!isProduction
? process.cwd()
: process.resourcesPath,
"static",
);
// 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 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 { ExtensionLoader } from "../extension-loader";
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 { PackageJson } from "type-fest";
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
@ -112,7 +112,7 @@ export class ExtensionDiscovery {
}
get inTreeFolderPath(): string {
return path.resolve(__static, "../extensions");
return path.resolve(staticFilesDirectory, "../extensions");
}
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/chart/bar-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
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 LensExtensionsMainApi from "../extensions/main-api";
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 { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
@ -198,7 +198,7 @@ async function main(di: DiContainer) {
powerMonitor.on("shutdown", () => app.exit());
registerFileProtocol("static", __static);
registerFileProtocol("static", staticFilesDirectory);
PrometheusProviderRegistry.createInstance();
initializers.initPrometheusProviderRegistry();

View File

@ -11,9 +11,6 @@ import type { Cluster } from "../../common/cluster/cluster";
import type { LensApiResultContentType } 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 {
req: http.IncomingMessage;
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 logger from "../logger";
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 isDevelopmentInjectable from "../../common/vars/is-development.injectable";
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 type { JoinPaths } from "../../common/path/join-paths.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import { webpackDevServerPort } from "../../../webpack/vars";
interface ProductionDependencies {
readFileBuffer: (path: string) => Promise<Buffer>;
@ -27,7 +28,7 @@ interface ProductionDependencies {
const handleStaticFileInProduction =
({ readFileBuffer, getAbsolutePath, joinPaths }: ProductionDependencies) =>
async ({ params }: LensApiRequest) => {
const staticPath = getAbsolutePath(__static);
const staticPath = getAbsolutePath(staticFilesDirectory);
let filePath = params.path;
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 type { WindowManager } from "../window-manager";
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 { toJS } from "../../common/utils";
import type { TrayMenuRegistration } from "./tray-menu-registration";
@ -25,7 +25,7 @@ export let tray: Tray;
export function getTrayIcon(): string {
return path.resolve(
__static,
staticFilesDirectory,
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
"trayIconTemplate.png",
);

View File

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

View File

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

View File

@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
import { hasCorrectExtension } from "./has-correct-extension";
import "../../../../common/vars";
import { staticFilesDirectory } from "../../../../common/vars";
import readFileInjectable from "../../../../common/fs/read-file.injectable";
import readDirInjectable from "../../../../common/fs/read-dir.injectable";
import type { RawTemplates } from "./create-resource-templates.injectable";
@ -19,7 +19,7 @@ interface 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

View File

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

View File

@ -9,12 +9,12 @@ import { observer } from "mobx-react";
import { action, computed, makeObservable, observable, reaction } from "mobx";
import { editor, Uri } from "monaco-editor";
import type { MonacoTheme } from "./monaco-themes";
import type { MonacoValidator } from "./monaco-validators";
import { monacoValidators } from "./monaco-validators";
import { type MonacoValidator, monacoValidators } from "./monaco-validators";
import { debounce, merge } from "lodash";
import { cssNames, disposer } from "../../utils";
import { autoBind, cssNames, disposer } from "../../utils";
import { UserStore } from "../../../common/user-store";
import { ThemeStore } from "../../theme.store";
import logger from "../../../main/logger";
export type MonacoEditorId = string;
@ -45,32 +45,28 @@ export const defaultEditorProps: Partial<MonacoEditorProps> = {
@observer
export class MonacoEditor extends React.Component<MonacoEditorProps> {
static defaultProps = defaultEditorProps as object;
static viewStates = new WeakMap<Uri, editor.ICodeEditorViewState>();
static readonly defaultProps = defaultEditorProps as object;
static readonly viewStates = new WeakMap<Uri, editor.ICodeEditorViewState>();
static createUri(id: MonacoEditorId): Uri {
return Uri.file(`/monaco-editor/${id}`);
}
public staticId = `editor-id#${Math.round(1e7 * Math.random())}`;
public 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;
private staticId = `editor-id#${Math.round(1e7 * Math.random())}`;
private dispose = disposer();
@observable.ref containerElem: HTMLElement;
@observable.ref editor: editor.IStandaloneCodeEditor;
@observable dimensions: { width?: number; height?: number } = {};
@observable unmounting = false;
@observable readonly dimensions: { width?: number; height?: number } = {};
@observable private unmounting = false;
constructor(props: MonacoEditorProps) {
super(props);
makeObservable(this);
autoBind(this);
}
@computed get id() {
@computed get id(): MonacoEditorId {
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 {
editorId: this.id,
model: this.model,
@ -121,8 +118,8 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
return () => resizeObserver.unobserve(containerElem);
}
onModelChange = (model: editor.ITextModel, oldModel?: editor.ITextModel) => {
this.logger?.info("[MONACO]: model change", { model, oldModel }, this.logMetadata);
protected onModelChange(model: editor.ITextModel, oldModel?: editor.ITextModel) {
logger.info("[MONACO]: model change", { model, oldModel }, this.logMetadata);
if (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.props.onModelChange?.(model, oldModel);
this.validateLazy();
};
}
/**
* Save current view-model state in the editor.
* 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());
}
private restoreViewState(model: editor.ITextModel) {
protected restoreViewState(model: editor.ITextModel) {
const viewState = MonacoEditor.viewStates.get(model.uri);
if (viewState) {
@ -156,9 +152,9 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
componentDidMount() {
try {
this.createEditor();
this.logger?.info(`[MONACO]: editor did mount`, this.logMetadata);
logger.info(`[MONACO]: editor did mount`, this.logMetadata);
} 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();
}
private createEditor() {
protected createEditor() {
if (!this.containerElem || this.editor || this.unmounting) {
return;
}
@ -184,7 +180,7 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
...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.restoreViewState(this.model); // restore previous state if any
@ -250,7 +246,8 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
this.editor?.focus();
}
validate = action((value = this.getValue()) => {
@action
validate(value = this.getValue()) {
const validators: MonacoValidator[] = [
monacoValidators[this.props.language], // parsing syntax check
].filter(Boolean);
@ -262,19 +259,21 @@ export class MonacoEditor extends React.Component<MonacoEditorProps> {
this.props.onError?.(error); // emit error outside
}
}
});
}
// avoid excessive validations during typing
validateLazy = debounce(this.validate, 250);
bindRef = (elem: HTMLElement) => this.containerElem = elem;
protected bindRef(elem: HTMLElement) {
this.containerElem = elem;
}
render() {
const { className, style } = this.props;
return (
<div
data-test-component="monaco-editor"
data-test-id="monaco-editor"
className={cssNames(styles.MonacoEditor, className)}
style={style}
ref={this.bindRef}

View File

@ -3,44 +3,166 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { parseJsonPath } from "../jsonPath";
import { convertKubectlJsonPathToNodeJsonPath, safeJSONPathValue } from "../jsonPath";
describe("parseJsonPath", () => {
test("should convert \\. to use indexed notation", () => {
const res = parseJsonPath(".metadata.labels.kubesphere\\.io/alias-name");
it("should convert \\. to use indexed notation", () => {
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", () => {
const res = parseJsonPath(".metadata.labels.kubesphere\\\"io/alias-name");
it("should convert keys with escaped characters to use indexed notation", () => {
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", () => {
const res = parseJsonPath(".metadata.labels.alias-name");
it("should convert '-' to use indexed notation", () => {
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", () => {
const rest = parseJsonPath(".metadata.labels\\.serving['some.other.item']");
it("should drop leading dot if first group is converted to index notation", () => {
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", () => {
const res = parseJsonPath(".status.conditions[?(@.type=='Ready')].status");
expect(res).toBe(".status.conditions[?(@.type=='Ready')].status");
expect(rest).toBe("$.metadata['labels.serving']['some.other.item']");
});
test("strips '\\' away from the result", () => {
const res = parseJsonPath(".metadata.labels['serving\\.knative\\.dev/configuration']");
it("should not touch given jsonPath if no invalid characters present", () => {
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.
*/
// Helper to convert strings used for jsonPath where \. or - is present to use indexed notation,
// for example: .metadata.labels.kubesphere\.io/alias-name -> .metadata.labels['kubesphere\.io/alias-name']
import { JSONPath } from "@astronautlabs/jsonpath";
export function parseJsonPath(jsonPath: string) {
let pathExpression = jsonPath;
const slashDashSearch = /[\\-]/g;
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 = pathExpression.replace(trailingDotDot, "");
pathExpression = pathExpression.replace(tripleDotName, "..$<trailing>");
if (!pathExpression.startsWith("[")) {
start += ".";
}
// strip '\' characters from the result
return pathExpression.replace(/\\/g, "");
return `${start}${pathExpression.replace(backSlash, "").replace(sliceVersion, "[0]")}`;
}
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 [...]
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 ']'
} else {
return `.${key}`; // otherwise return as is with leading '.'
@ -38,3 +73,35 @@ function convertToIndexNotation(key: string, firstItem = false) {
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/*"
]
},
"plugins": [{ "name": "typescript-plugin-css-modules" }]
"plugins": [
{
"name": "typescript-plugin-css-modules"
}
]
},
"ts-node": {
"transpileOnly": true,
@ -42,7 +46,7 @@
},
"include": [
"src/**/*",
"types/*.d.ts"
"types/*.d.ts",
],
"exclude": [
"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/subtext"
// Global path to static assets
declare const __static: string;
// Support import for custom module extensions
// https://www.typescriptlang.org/docs/handbook/modules.html#wildcard-module-declarations
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 type webpack from "webpack";
import * as vars from "./src/common/vars";
import { cssModulesWebpackRule, fontsLoaderWebpackRules, iconsAndImagesWebpackRules } from "./webpack.renderer";
import { cssModulesWebpackRule, fontsLoaderWebpackRules, iconsAndImagesWebpackRules } from "./renderer";
import { extensionEntry, extensionOutDir, isDevelopment } from "./vars";
export default function generateExtensionTypes(): webpack.Configuration {
const { isDevelopment } = vars;
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,
entry: extensionEntry,
// this is the default mode, so we should make it explicit to silence the warning
mode: isDevelopment ? "development" : "production",
output: {
filename: "extension-api.js",
// 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
// e.g. require('@k8slens/extensions')
libraryTarget: "commonjs",
@ -54,7 +50,7 @@ export default function generateExtensionTypes(): webpack.Configuration {
compilerOptions: {
declaration: true, // output .d.ts
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
* @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
options.transpileOnly ??= true;
@ -37,6 +37,4 @@ const getTSLoader = (options: Partial<TSLoaderOptions> = {}, testRegExp?: RegExp
options,
},
};
};
export default getTSLoader;
}

View File

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

View File

@ -3,25 +3,31 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as vars from "./src/common/vars";
import path from "path";
import type webpack from "webpack";
import HtmlWebpackPlugin from "html-webpack-plugin";
import MiniCssExtractPlugin from "mini-css-extract-plugin";
import ForkTsCheckerPlugin from "fork-ts-checker-webpack-plugin";
import MonacoWebpackPlugin from "monaco-editor-webpack-plugin";
import getTSLoader from "./src/common/getTSLoader";
import CircularDependencyPlugin from "circular-dependency-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 {
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 {
target: "electron-renderer",
name: "lens-app-renderer",
@ -67,7 +73,7 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
test: /\.node$/,
use: "node-loader",
},
getTSLoader({
getTypescriptLoader({
getCustomTransformers: () => ({
before: isDevelopment ? [require("react-refresh-typescript")()] : [],
}),
@ -103,14 +109,18 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura
cwd: __dirname,
exclude: /node_modules/,
failOnError: true,
}),
}) as unknown as WebpackPluginInstance,
new MiniCssExtractPlugin({
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)
* @param {string} styleLoader
*/
export function cssModulesWebpackRule(
{
styleLoader = vars.isDevelopment ? "style-loader" : MiniCssExtractPlugin.loader,
} = {}): webpack.RuleSetRule {
const { isDevelopment, sassCommonVars } = vars;
export function cssModulesWebpackRule({ styleLoader }: CssModulesWebpackRuleOptions = {}): webpack.RuleSetRule {
styleLoader ??= isDevelopment
? "style-loader"
: MiniCssExtractPlugin.loader;
return {
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