diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index c863903259..3ffb922dea 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -448,8 +448,9 @@ describe("Lens cluster pages", () => { await app.client.click(".Icon.new-dock-tab"); await app.client.waitUntilTextExists("li.MenuItem.create-resource-tab", "Create resource"); await app.client.click("li.MenuItem.create-resource-tab"); - await app.client.waitForVisible(".CreateResource div.ace_content"); + await app.client.waitForVisible(".CreateResource div.react-monaco-editor-container"); // Write pod manifest to editor + await app.client.click(".CreateResource div.react-monaco-editor-container"); await app.client.keys("apiVersion: v1\n"); await app.client.keys("kind: Pod\n"); await app.client.keys("metadata:\n"); diff --git a/package.json b/package.json index f500810e98..1bad46ade7 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,7 @@ "moment": "^2.29.1", "moment-timezone": "^0.5.33", "node-fetch": "^2.6.1", + "monaco-editor": "^0.26.1", "node-pty": "^0.10.1", "npm": "^6.14.8", "openid-client": "^3.15.2", @@ -230,6 +231,7 @@ "proper-lockfile": "^4.1.2", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-monaco-editor": "^0.44.0", "react-router": "^5.2.0", "react-virtualized-auto-sizer": "^1.0.5", "readable-stream": "^3.6.0", @@ -317,7 +319,6 @@ "@types/webpack-node-externals": "^1.7.1", "@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/parser": "^4.29.1", - "ace-builds": "^1.4.12", "ansi_up": "^5.0.0", "chart.js": "^2.9.4", "circular-dependency-plugin": "^5.2.2", diff --git a/src/common/__tests__/search-store.test.ts b/src/common/__tests__/search-store.test.ts index b4c99b7e19..abd6cd1510 100644 --- a/src/common/__tests__/search-store.test.ts +++ b/src/common/__tests__/search-store.test.ts @@ -23,6 +23,8 @@ import { SearchStore } from "../search-store"; import { Console } from "console"; import { stdout, stderr } from "process"; +jest.mock("react-monaco-editor", () => null); + jest.mock("electron", () => ({ app: { getPath: () => "/foo", diff --git a/src/common/routes/preferences.ts b/src/common/routes/preferences.ts index bae7e6257e..1c19e6e733 100644 --- a/src/common/routes/preferences.ts +++ b/src/common/routes/preferences.ts @@ -38,6 +38,10 @@ export const kubernetesRoute: RouteProps = { path: `${preferencesRoute.path}/kubernetes` }; +export const editorRoute: RouteProps = { + path: `${preferencesRoute.path}/editor` +}; + export const telemetryRoute: RouteProps = { path: `${preferencesRoute.path}/telemetry` }; @@ -50,5 +54,6 @@ export const preferencesURL = buildURL(preferencesRoute.path); export const appURL = buildURL(appRoute.path); export const proxyURL = buildURL(proxyRoute.path); export const kubernetesURL = buildURL(kubernetesRoute.path); +export const editorURL = buildURL(editorRoute.path); export const telemetryURL = buildURL(telemetryRoute.path); export const extensionURL = buildURL(extensionRoute.path); diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index 4363511120..59f201f55f 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -24,6 +24,8 @@ import path from "path"; import os from "os"; import { ThemeStore } from "../../renderer/theme.store"; import { ObservableToggleSet } from "../utils"; +import type {monaco} from "react-monaco-editor"; +import merge from "lodash/merge"; export interface KubeconfigSyncEntry extends KubeconfigSyncValue { filePath: string; @@ -31,6 +33,20 @@ export interface KubeconfigSyncEntry extends KubeconfigSyncValue { export interface KubeconfigSyncValue { } +export interface EditorConfiguration { + miniMap?: monaco.editor.IEditorMinimapOptions; + lineNumbers?: monaco.editor.LineNumbersType; + tabSize?: number; +} + +export const defaultEditorConfig: EditorConfiguration = { + lineNumbers: "on", + miniMap: { + enabled: true + }, + tabSize: 2 +}; + interface PreferenceDescription { fromStore(val: T | undefined): R; toStore(val: R): T | undefined; @@ -222,6 +238,15 @@ const syncKubeconfigEntries: PreferenceDescription = { + fromStore(val) { + return merge(defaultEditorConfig, val); + }, + toStore(val) { + return val; + }, +}; + type PreferencesModelType = typeof DESCRIPTORS[field] extends PreferenceDescription ? T : never; type UserStoreModelType = typeof DESCRIPTORS[field] extends PreferenceDescription ? T : never; @@ -248,4 +273,5 @@ export const DESCRIPTORS = { openAtLogin, hiddenTableColumns, syncKubeconfigEntries, + editorConfiguration, }; diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 9cc5cd06e9..75e207a2ca 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -30,8 +30,9 @@ import { appEventBus } from "../event-bus"; import path from "path"; import { fileNameMigration } from "../../migrations/user-store"; import { ObservableToggleSet, toJS } from "../../renderer/utils"; -import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; +import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel, EditorConfiguration } from "./preferences-helpers"; import logger from "../../main/logger"; +import type {monaco} from "react-monaco-editor"; export interface UserStoreModel { lastSeenAppVersion: string; @@ -68,7 +69,7 @@ export class UserStore extends BaseStore /* implements UserStore @observable shell?: string; @observable downloadBinariesPath?: string; @observable kubectlBinariesPath?: string; - + /** * Download kubectl binaries matching cluster version */ @@ -81,6 +82,11 @@ export class UserStore extends BaseStore /* implements UserStore */ hiddenTableColumns = observable.map>(); + /** + * Monaco editor configs + */ + @observable editorConfiguration:EditorConfiguration = {tabSize: null, miniMap: null, lineNumbers: null}; + /** * The set of file/folder paths to be synced */ @@ -109,7 +115,29 @@ export class UserStore extends BaseStore /* implements UserStore }); }, { fireImmediately: true, - }); + }); + } + + // Returns monaco editor options for selected editor type (the place, where a particular instance of the editor is mounted) + getEditorOptions(): monaco.editor.IStandaloneEditorConstructionOptions { + return { + automaticLayout: true, + tabSize: this.editorConfiguration.tabSize, + minimap: this.editorConfiguration.miniMap, + lineNumbers: this.editorConfiguration.lineNumbers + }; + } + + setEditorLineNumbers(lineNumbers: monaco.editor.LineNumbersType) { + this.editorConfiguration.lineNumbers = lineNumbers; + } + + setEditorTabSize(tabSize: number) { + this.editorConfiguration.tabSize = tabSize; + } + + enableEditorMinimap(miniMap: boolean ) { + this.editorConfiguration.miniMap.enabled = miniMap; } /** @@ -182,6 +210,7 @@ export class UserStore extends BaseStore /* implements UserStore this.openAtLogin = DESCRIPTORS.openAtLogin.fromStore(preferences?.openAtLogin); this.hiddenTableColumns.replace(DESCRIPTORS.hiddenTableColumns.fromStore(preferences?.hiddenTableColumns)); this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries)); + this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration); } toJSON(): UserStoreModel { @@ -202,6 +231,7 @@ export class UserStore extends BaseStore /* implements UserStore openAtLogin: DESCRIPTORS.openAtLogin.toStore(this.openAtLogin), hiddenTableColumns: DESCRIPTORS.hiddenTableColumns.toStore(this.hiddenTableColumns), syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries), + editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration), }, }; diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 9a64d95b6b..5aa1b53a36 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -29,6 +29,8 @@ import { ThemeStore } from "../../../renderer/theme.store"; import { TerminalStore } from "../../renderer-api/components"; import { UserStore } from "../../../common/user-store"; +jest.mock("react-monaco-editor", () => null); + jest.mock("electron", () => ({ app: { getPath: () => "tmp", diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 3e7daa5404..b6b97cfc6b 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -28,6 +28,7 @@ import * as ReactRouter from "react-router"; import * as ReactRouterDom from "react-router-dom"; import * as LensExtensionsCommonApi from "../extensions/common-api"; import * as LensExtensionsRendererApi from "../extensions/renderer-api"; +import { monaco } from "react-monaco-editor"; import { render, unmountComponentAtNode } from "react-dom"; import { delay } from "../common/utils"; import { isMac, isDevelopment } from "../common/vars"; @@ -49,6 +50,7 @@ import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import { ThemeStore } from "./theme.store"; import { SentryInit } from "../common/sentry"; import { TerminalStore } from "./components/dock/terminal.store"; +import cloudsMidnight from "./monaco-themes/Clouds Midnight.json"; configurePackages(); @@ -102,6 +104,12 @@ export async function bootstrap(App: AppComponent) { ExtensionsStore.createInstance(); FilesystemProvisionerStore.createInstance(); + // define Monaco Editor themes + const { base, ...params } = cloudsMidnight; + const baseTheme = base as monaco.editor.BuiltinTheme; + + monaco.editor.defineTheme("clouds-midnight", {base: baseTheme, ...params}); + // ThemeStore depends on: UserStore ThemeStore.createInstance(); diff --git a/src/renderer/components/+add-cluster/add-cluster.scss b/src/renderer/components/+add-cluster/add-cluster.scss index a8eaebf410..faac757c40 100644 --- a/src/renderer/components/+add-cluster/add-cluster.scss +++ b/src/renderer/components/+add-cluster/add-cluster.scss @@ -23,7 +23,7 @@ --flex-gap: #{$unit * 2}; $spacing: $padding * 2; - .AceEditor { + .MonacoEditor { min-height: 600px; max-height: 600px; border: 1px solid var(--colorVague); diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 5e59767081..e987c1d4bb 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -34,11 +34,13 @@ import { appEventBus } from "../../../common/event-bus"; import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers"; import { docsUrl } from "../../../common/vars"; import { navigate } from "../../navigation"; -import { getCustomKubeConfigPath, iter } from "../../utils"; -import { AceEditor } from "../ace-editor"; +import { getCustomKubeConfigPath, cssNames, iter } from "../../utils"; import { Button } from "../button"; import { Notifications } from "../notifications"; import { SettingLayout } from "../layout/setting-layout"; +import MonacoEditor from "react-monaco-editor"; +import { ThemeStore } from "../../theme.store"; +import { UserStore } from "../../../common/user-store"; interface Option { config: KubeConfig; @@ -114,10 +116,11 @@ export class AddCluster extends React.Component { Read more about adding clusters here.

- { this.customConfig = value; diff --git a/src/renderer/components/+apps-releases/release-details.scss b/src/renderer/components/+apps-releases/release-details.scss index a4bd53610c..112ee6b578 100644 --- a/src/renderer/components/+apps-releases/release-details.scss +++ b/src/renderer/components/+apps-releases/release-details.scss @@ -82,11 +82,11 @@ } .values { - .AceEditor { + .MonacoEditor { min-height: 300px; } - .AceEditor + .Button { + .MonacoEditor + .Button { align-self: flex-start; } } diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx index e3e68ffb53..ae66171932 100644 --- a/src/renderer/components/+apps-releases/release-details.tsx +++ b/src/renderer/components/+apps-releases/release-details.tsx @@ -35,7 +35,6 @@ import { cssNames, stopPropagation } from "../../utils"; import { disposeOnUnmount, observer } from "mobx-react"; import { Spinner } from "../spinner"; import { Table, TableCell, TableHead, TableRow } from "../table"; -import { AceEditor } from "../ace-editor"; import { Button } from "../button"; import { releaseStore } from "./release.store"; import { Notifications } from "../notifications"; @@ -47,6 +46,8 @@ import { secretsStore } from "../+config-secrets/secrets.store"; import { Secret } from "../../../common/k8s-api/endpoints"; import { getDetailsUrl } from "../kube-detail-params"; import { Checkbox } from "../checkbox"; +import MonacoEditor from "react-monaco-editor"; +import { UserStore } from "../../../common/user-store"; interface Props { release: HelmRelease; @@ -158,15 +159,16 @@ export class ReleaseDetails extends Component { onChange={value => this.showOnlyUserSuppliedValues = value} disabled={valuesLoading} /> - this.values = text} - className={cssNames({ loading: valuesLoading })} - readOnly={valuesLoading || this.showOnlyUserSuppliedValues} + theme={ThemeStore.getInstance().activeTheme.monacoTheme} + className={cssNames("MonacoEditor", {loading: valuesLoading})} + options={{readOnly: valuesLoading || this.showOnlyUserSuppliedValues, ...UserStore.getInstance().getEditorOptions()}} > {valuesLoading && } - +