diff --git a/.github/actions/add-card-to-project/Dockerfile b/.github/actions/add-card-to-project/Dockerfile new file mode 100644 index 0000000000..95c2fe1628 --- /dev/null +++ b/.github/actions/add-card-to-project/Dockerfile @@ -0,0 +1,11 @@ +# Container image that runs your code +FROM alpine:3.10 + +RUN apk add --no-cache --no-progress curl jq + +# Copies your code file from your action repository to the filesystem path `/` of the container +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Code file to execute when the docker container starts up (`entrypoint.sh`) +ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/actions/add-card-to-project/action.yml b/.github/actions/add-card-to-project/action.yml new file mode 100644 index 0000000000..e7fcba44c6 --- /dev/null +++ b/.github/actions/add-card-to-project/action.yml @@ -0,0 +1,23 @@ +name: 'add_card_to_project' +description: 'A GitHub Action to add a card to a project and set the card position' +author: 'Steve Richards' +branding: + icon: 'command' + color: 'blue' +inputs: + project: + description: 'The url of the project to be assigned to.' + required: true + column_name: + description: 'The column name of the project, defaults to "To do" for issues and "In progress" for pull requests.' + required: false + card_position: + description: 'The card position of the card in the column, defaults to "bottom". Valid values are "top", "bottom" and "after:".' + required: false +runs: + using: 'docker' + image: 'Dockerfile' + args: + - ${{ inputs.project }} + - ${{ inputs.column_name }} + - ${{ inputs.card_position }} diff --git a/.github/actions/add-card-to-project/entrypoint.sh b/.github/actions/add-card-to-project/entrypoint.sh new file mode 100644 index 0000000000..a67bfb1d96 --- /dev/null +++ b/.github/actions/add-card-to-project/entrypoint.sh @@ -0,0 +1,157 @@ +#!/bin/sh -l + +PROJECT_URL="$INPUT_PROJECT" +if [ -z "$PROJECT_URL" ]; then + echo "PROJECT_URL is not defined." >&2 + exit 1 +fi + +get_project_type() { + _PROJECT_URL="$1" + + case "$_PROJECT_URL" in + https://github.com/orgs/*) + echo "org" + ;; + https://github.com/users/*) + echo "user" + ;; + https://github.com/*/projects/*) + echo "repo" + ;; + *) + echo "Invalid PROJECT_URL: $_PROJECT_URL" >&2 + exit 1 + ;; + esac + + unset _PROJECT_URL +} + +find_project_id() { + _PROJECT_TYPE="$1" + _PROJECT_URL="$2" + + case "$_PROJECT_TYPE" in + org) + _ORG_NAME=$(echo "$_PROJECT_URL" | sed -e 's@https://github.com/orgs/\([^/]\+\)/projects/[0-9]\+@\1@') + _ENDPOINT="https://api.github.com/orgs/$_ORG_NAME/projects" + ;; + user) + _USER_NAME=$(echo "$_PROJECT_URL" | sed -e 's@https://github.com/users/\([^/]\+\)/projects/[0-9]\+@\1@') + _ENDPOINT="https://api.github.com/users/$_USER_NAME/projects" + ;; + repo) + _ENDPOINT="https://api.github.com/repos/$GITHUB_REPOSITORY/projects" + ;; + esac + + _PROJECTS=$(curl -s -X GET -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \ + -H 'Accept: application/vnd.github.inertia-preview+json' \ + "$_ENDPOINT") + + _PROJECTID=$(echo "$_PROJECTS" | jq -r ".[] | select(.html_url == \"$_PROJECT_URL\").id") + + if [ "$_PROJECTID" != "" ]; then + echo "$_PROJECTID" + else + echo "No project was found." >&2 + exit 1 + fi + + unset _PROJECT_TYPE _PROJECT_URL _ORG_NAME _USER_NAME _ENDPOINT _PROJECTS _PROJECTID +} + +find_column_id() { + _PROJECT_ID="$1" + _INITIAL_COLUMN_NAME="$2" + + _COLUMNS=$(curl -s -X GET -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \ + -H 'Accept: application/vnd.github.inertia-preview+json' \ + "https://api.github.com/projects/$_PROJECT_ID/columns") + + + echo "$_COLUMNS" | jq -r ".[] | select(.name == \"$_INITIAL_COLUMN_NAME\").id" + unset _PROJECT_ID _INITIAL_COLUMN_NAME _COLUMNS +} + +PROJECT_TYPE=$(get_project_type "${PROJECT_URL:? required this environment variable}") + +if [ "$PROJECT_TYPE" = org ] || [ "$PROJECT_TYPE" = user ]; then + if [ -z "$MY_GITHUB_TOKEN" ]; then + echo "MY_GITHUB_TOKEN not defined" >&2 + exit 1 + fi + + TOKEN="$MY_GITHUB_TOKEN" # It's User's personal access token. It should be secret. +else + if [ -z "$GITHUB_TOKEN" ]; then + echo "GITHUB_TOKEN not defined" >&2 + exit 1 + fi + + TOKEN="$GITHUB_TOKEN" # GitHub sets. The scope in only the repository containing the workflow file. +fi + +INITIAL_COLUMN_NAME="$INPUT_COLUMN_NAME" +if [ -z "$INITIAL_COLUMN_NAME" ]; then + # assing the column name by default + INITIAL_COLUMN_NAME='To do' + if [ "$GITHUB_EVENT_NAME" == "pull_request" ] || [ "$GITHUB_EVENT_NAME" == "pull_request_target" ]; then + echo "changing col name for PR event" + INITIAL_COLUMN_NAME='In progress' + fi +fi + + +PROJECT_ID=$(find_project_id "$PROJECT_TYPE" "$PROJECT_URL") +INITIAL_COLUMN_ID=$(find_column_id "$PROJECT_ID" "${INITIAL_COLUMN_NAME:? required this environment variable}") + +if [ -z "$INITIAL_COLUMN_ID" ]; then + echo "INITIAL_COLUMN_ID is not found." >&2 + exit 1 +fi + +case "$GITHUB_EVENT_NAME" in + issues) + ISSUE_ID=$(jq -r '.issue.id' < "$GITHUB_EVENT_PATH") + + # Add this issue to the project column + _CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \ + -H 'Accept: application/vnd.github.inertia-preview+json' \ + -d "{\"content_type\": \"Issue\", \"content_id\": $ISSUE_ID}" \ + "https://api.github.com/projects/columns/$INITIAL_COLUMN_ID/cards") + ;; + pull_request|pull_request_target) + PULL_REQUEST_ID=$(jq -r '.pull_request.id' < "$GITHUB_EVENT_PATH") + + # Add this pull_request to the project column + _CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \ + -H 'Accept: application/vnd.github.inertia-preview+json' \ + -d "{\"content_type\": \"PullRequest\", \"content_id\": $PULL_REQUEST_ID}" \ + "https://api.github.com/projects/columns/$INITIAL_COLUMN_ID/cards") + ;; + *) + echo "Nothing to be done on this action: $GITHUB_EVENT_NAME" >&2 + exit 1 + ;; +esac + +CARDS_ID=$(echo "$_CARDS" | jq -r .id) +unset _CARDS + +if [ -z "$CARDS_ID" ]; then + echo "CARDS_ID is not found." >&2 + exit 1 +fi + +INITIAL_CARD_POSITION="$INPUT_CARD_POSITION" +if [ -z "$INITIAL_CARD_POSITION" ]; then + # assign the card position by default + INITIAL_CARD_POSITION='bottom' +fi + +_CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \ + -H 'Accept: application/vnd.github.inertia-preview+json' \ + -d "{\"position\": \"$INITIAL_CARD_POSITION\"}" \ + "https://api.github.com/projects/columns/cards/$CARDS_ID/moves") diff --git a/.github/workflows/add-to-project-board.yaml b/.github/workflows/add-to-project-board.yaml new file mode 100644 index 0000000000..8f23cd406c --- /dev/null +++ b/.github/workflows/add-to-project-board.yaml @@ -0,0 +1,33 @@ +name: Add Card to Project(s) +on: + issues: + types: [opened] + pull_request_target: + types: [opened] +env: + MY_GITHUB_TOKEN: ${{ secrets.PROJECT_GITHUB_TOKEN }} + EVENT_TYPE: $GITHUB_EVENT_NAME + +jobs: + add_card_to_project: + runs-on: ubuntu-latest + name: Add Card to Project(s) + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Get Event Type + run: echo $GITHUB_EVENT_NAME + - name: Assign NEW issues to project 1 + uses: ./.github/actions/add-card-to-project + if: github.event_name == 'issues' && github.event.action == 'opened' + with: + project: 'https://github.com/orgs/lensapp/projects/1' + column_name: 'Backlog' + card_position: 'bottom' + - name: Assign NEW pull requests to project 1 + uses: ./.github/actions/add-card-to-project + if: github.event_name == 'pull_request_target' && github.event.action == 'opened' + with: + project: 'https://github.com/orgs/lensapp/projects/1' + column_name: 'PRs' + card_position: 'bottom' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4a43f6eb4..2ce53e59e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,15 +77,12 @@ jobs: sudo chown -R $USER $HOME/.kube $HOME/.minikube name: Install integration test dependencies if: runner.os == 'Linux' - - run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux + + - run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration name: Run Linux integration tests if: runner.os == 'Linux' - - run: make integration-win - name: Run Windows integration tests + - run: make integration + name: Run integration tests shell: bash - if: runner.os == 'Windows' - - - run: make integration-mac - name: Run macOS integration tests - if: runner.os == 'macOS' + if: runner.os != 'Linux' diff --git a/Makefile b/Makefile index d9a5fe1a83..8b2558e7f7 100644 --- a/Makefile +++ b/Makefile @@ -50,21 +50,8 @@ tag-release: test: binaries/client yarn run jest $(or $(CMD_ARGS), "src") -.PHONY: integration-linux -integration-linux: binaries/client build-extension-types build-extensions - yarn build:linux - yarn integration - -.PHONY: integration-mac -integration-mac: binaries/client build-extension-types build-extensions - # rm ${HOME}/Library/Application\ Support/Lens - yarn build:mac - yarn integration - -.PHONY: integration-win -integration-win: binaries/client build-extension-types build-extensions - # rm %APPDATA%/Lens - yarn build:win +.PHONY: integration +integration: build yarn integration .PHONY: build diff --git a/package.json b/package.json index 8cd525a365..4844f6f10a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "copyright": "© 2021 OpenLens Authors", "license": "MIT", "author": { - "name": "OpenLens Authors" + "name": "OpenLens Authors", + "email": "info@k8slens.dev" }, "scripts": { "dev": "concurrently -i -k \"yarn run dev-run -C\" yarn:dev:*", @@ -315,7 +316,7 @@ "concurrently": "^5.2.0", "css-loader": "^5.2.6", "deepdash": "^5.3.5", - "dompurify": "^2.0.11", + "dompurify": "^2.0.17", "electron": "^9.4.4", "electron-builder": "^22.10.5", "electron-notarize": "^0.3.0", diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index b1b3f60ce6..6e5a6433ef 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -41,6 +41,7 @@ import { SemVer } from "semver"; import electron from "electron"; import { stdout, stderr } from "process"; import { beforeEachWrapped } from "../../../integration/helpers/utils"; +import { ThemeStore } from "../../renderer/theme.store"; console = new Console(stdout, stderr); @@ -72,7 +73,7 @@ describe("user store tests", () => { us.httpsProxy = "abcd://defg"; expect(us.httpsProxy).toBe("abcd://defg"); - expect(us.colorTheme).toBe(UserStore.defaultTheme); + expect(us.colorTheme).toBe(ThemeStore.defaultTheme); us.colorTheme = "light"; expect(us.colorTheme).toBe("light"); @@ -83,7 +84,7 @@ describe("user store tests", () => { us.colorTheme = "some other theme"; us.resetTheme(); - expect(us.colorTheme).toBe(UserStore.defaultTheme); + expect(us.colorTheme).toBe(ThemeStore.defaultTheme); }); it("correctly calculates if the last seen version is an old release", () => { diff --git a/src/common/base-store.ts b/src/common/base-store.ts index d30725d6bb..095e0c1877 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -56,9 +56,15 @@ export abstract class BaseStore extends Singleton { cwd: this.cwd(), }); - logger.info(`[STORE]: LOADED from ${this.path}`); - this.fromStore(this.storeConfig.store); + const res: any = this.fromStore(this.storeConfig.store); + + if (res instanceof Promise || (typeof res === "object" && res && typeof res.then === "function")) { + console.error(`${this.name} extends BaseStore's fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`); + } + this.enableSync(); + + logger.info(`[STORE]: LOADED from ${this.path}`); } get name() { @@ -157,6 +163,9 @@ export abstract class BaseStore extends Singleton { /** * fromStore is called internally when a child class syncs with the file * system. + * + * Note: This function **must** be synchronous. + * * @param data the parsed information read from the stored JSON file */ protected abstract fromStore(data: T): void; diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index bad8b95a31..90694e9976 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -94,7 +94,7 @@ export class HotbarStore extends BaseStore { } @action - protected async fromStore(data: Partial = {}) { + protected fromStore(data: Partial = {}) { if (!data.hotbars || !data.hotbars.length) { this.hotbars = [{ id: uuid.v4(), diff --git a/src/common/user-store/index.ts b/src/common/user-store/index.ts new file mode 100644 index 0000000000..dc00ab6737 --- /dev/null +++ b/src/common/user-store/index.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export * from "./user-store"; +export type { KubeconfigSyncEntry, KubeconfigSyncValue } from "./preferences-helpers"; diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts new file mode 100644 index 0000000000..b35f7ddc51 --- /dev/null +++ b/src/common/user-store/preferences-helpers.ts @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import moment from "moment-timezone"; +import path from "path"; +import os from "os"; +import { ThemeStore } from "../../renderer/theme.store"; +import { ObservableToggleSet } from "../utils"; + +export interface KubeconfigSyncEntry extends KubeconfigSyncValue { + filePath: string; +} + +export interface KubeconfigSyncValue { } + +interface PreferenceDescription { + fromStore(val: T | undefined): R; + toStore(val: R): T | undefined; +} + +const httpsProxy: PreferenceDescription = { + fromStore(val) { + return val; + }, + toStore(val) { + return val || undefined; + }, +}; + +const shell: PreferenceDescription = { + fromStore(val) { + return val; + }, + toStore(val) { + return val || undefined; + }, +}; + +const colorTheme: PreferenceDescription = { + fromStore(val) { + return val || ThemeStore.defaultTheme; + }, + toStore(val) { + if (!val || val === ThemeStore.defaultTheme) { + return undefined; + } + + return val; + }, +}; + +const localeTimezone: PreferenceDescription = { + fromStore(val) { + return val || moment.tz.guess(true) || "UTC"; + }, + toStore(val) { + if (!val || val === moment.tz.guess(true) || val === "UTC") { + return undefined; + } + + return val; + }, +}; + +const allowUntrustedCAs: PreferenceDescription = { + fromStore(val) { + return val ?? false; + }, + toStore(val) { + if (!val) { + return undefined; + } + + return val; + }, +}; + +const allowTelemetry: PreferenceDescription = { + fromStore(val) { + return val ?? true; + }, + toStore(val) { + if (val === true) { + return undefined; + } + + return val; + }, +}; + +const downloadMirror: PreferenceDescription = { + fromStore(val) { + return val ?? "default"; + }, + toStore(val) { + if (!val || val === "default") { + return undefined; + } + + return val; + }, +}; + +const downloadKubectlBinaries: PreferenceDescription = { + fromStore(val) { + return val ?? true; + }, + toStore(val) { + if (val === true) { + return undefined; + } + + return val; + }, +}; + +const downloadBinariesPath: PreferenceDescription = { + fromStore(val) { + return val; + }, + toStore(val) { + if (!val) { + return undefined; + } + + return val; + }, +}; + +const kubectlBinariesPath: PreferenceDescription = { + fromStore(val) { + return val; + }, + toStore(val) { + if (!val) { + return undefined; + } + + return val; + }, +}; + +const openAtLogin: PreferenceDescription = { + fromStore(val) { + return val ?? false; + }, + toStore(val) { + if (!val) { + return undefined; + } + + return val; + }, +}; + +const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map>> = { + fromStore(val) { + return new Map( + (val ?? []).map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)]) + ); + }, + toStore(val) { + const res: [string, string[]][] = []; + + for (const [table, columnes] of val) { + if (columnes.size) { + res.push([table, Array.from(columnes)]); + } + } + + return res.length ? res : undefined; + }, +}; + +const mainKubeFolder = path.join(os.homedir(), ".kube"); + +const syncKubeconfigEntries: PreferenceDescription> = { + fromStore(val) { + return new Map( + val + ?.map(({ filePath, ...rest }) => [filePath, rest]) + ?? [[mainKubeFolder, {}]] + ); + }, + toStore(val) { + if (val.size === 1 && val.has(mainKubeFolder)) { + return undefined; + } + + return Array.from(val, ([filePath, rest]) => ({ filePath, ...rest })); + }, +}; + +type PreferencesModelType = typeof DESCRIPTORS[field] extends PreferenceDescription ? T : never; +type UserStoreModelType = typeof DESCRIPTORS[field] extends PreferenceDescription ? T : never; + +export type UserStoreFlatModel = { + [field in keyof typeof DESCRIPTORS]: UserStoreModelType; +}; + +export type UserPreferencesModel = { + [field in keyof typeof DESCRIPTORS]: PreferencesModelType; +}; + +export const DESCRIPTORS = { + httpsProxy, + shell, + colorTheme, + localeTimezone, + allowUntrustedCAs, + allowTelemetry, + downloadMirror, + downloadKubectlBinaries, + downloadBinariesPath, + kubectlBinariesPath, + openAtLogin, + hiddenTableColumns, + syncKubeconfigEntries, +}; diff --git a/src/common/user-store.ts b/src/common/user-store/user-store.ts similarity index 54% rename from src/common/user-store.ts rename to src/common/user-store/user-store.ts index c1e956b331..e4f9a8a007 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -19,50 +19,25 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { ThemeId } from "../renderer/theme.store"; import { app, remote } from "electron"; import semver from "semver"; import { action, computed, observable, reaction, makeObservable } from "mobx"; -import moment from "moment-timezone"; -import { BaseStore } from "./base-store"; -import migrations from "../migrations/user-store"; -import { getAppVersion } from "./utils/app-version"; -import { appEventBus } from "./event-bus"; +import { BaseStore } from "../base-store"; +import migrations from "../../migrations/user-store"; +import { getAppVersion } from "../utils/app-version"; +import { kubeConfigDefaultPath } from "../kube-helpers"; +import { appEventBus } from "../event-bus"; import path from "path"; -import os from "os"; -import { fileNameMigration } from "../migrations/user-store"; -import { ObservableToggleSet, toJS } from "../renderer/utils"; +import { fileNameMigration } from "../../migrations/user-store"; +import { ObservableToggleSet, toJS } from "../../renderer/utils"; +import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; export interface UserStoreModel { lastSeenAppVersion: string; preferences: UserPreferencesModel; } -export interface KubeconfigSyncEntry extends KubeconfigSyncValue { - filePath: string; -} - -export interface KubeconfigSyncValue { } - -export interface UserPreferencesModel { - httpsProxy?: string; - shell?: string; - colorTheme?: string; - localeTimezone?: string; - allowUntrustedCAs?: boolean; - allowTelemetry?: boolean; - downloadMirror?: string | "default"; - downloadKubectlBinaries?: boolean; - downloadBinariesPath?: string; - kubectlBinariesPath?: string; - openAtLogin?: boolean; - hiddenTableColumns?: [string, string[]][]; - syncKubeconfigEntries?: KubeconfigSyncEntry[]; -} - -export class UserStore extends BaseStore { - static readonly defaultTheme: ThemeId = "lens-dark"; - +export class UserStore extends BaseStore /* implements UserStoreFlatModel (when strict null is enabled) */ { constructor() { super({ configName: "lens-user-store", @@ -75,11 +50,18 @@ export class UserStore extends BaseStore { } @observable lastSeenAppVersion = "0.0.0"; - @observable allowTelemetry = true; - @observable allowUntrustedCAs = false; - @observable colorTheme = UserStore.defaultTheme; - @observable localeTimezone = moment.tz.guess(true) || "UTC"; - @observable downloadMirror = "default"; + + /** + * used in add-cluster page for providing context + */ + @observable kubeConfigPath = kubeConfigDefaultPath; + @observable seenContexts = observable.set(); + @observable newContexts = observable.set(); + @observable allowTelemetry: boolean; + @observable allowUntrustedCAs: boolean; + @observable colorTheme: string; + @observable localeTimezone: string; + @observable downloadMirror: string; @observable httpsProxy?: string; @observable shell?: string; @observable downloadBinariesPath?: string; @@ -88,8 +70,8 @@ export class UserStore extends BaseStore { /** * Download kubectl binaries matching cluster version */ - @observable downloadKubectlBinaries = true; - @observable openAtLogin = false; + @observable downloadKubectlBinaries: boolean; + @observable openAtLogin: boolean; /** * The column IDs under each configurable table ID that have been configured @@ -100,9 +82,7 @@ export class UserStore extends BaseStore { /** * The set of file/folder paths to be synced */ - syncKubeconfigEntries = observable.map([ - [path.join(os.homedir(), ".kube"), {}] - ]); + syncKubeconfigEntries = observable.map(); @computed get isNewVersion() { return semver.gt(getAppVersion(), this.lastSeenAppVersion); @@ -160,7 +140,7 @@ export class UserStore extends BaseStore { @action resetTheme() { - this.colorTheme = UserStore.defaultTheme; + this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined); } @action @@ -175,71 +155,45 @@ export class UserStore extends BaseStore { } @action - protected async fromStore(data: Partial = {}) { + protected fromStore(data: Partial = {}) { const { lastSeenAppVersion, preferences } = data; if (lastSeenAppVersion) { this.lastSeenAppVersion = lastSeenAppVersion; } - if (!preferences) { - return; - } - - this.httpsProxy = preferences.httpsProxy; - this.shell = preferences.shell; - this.colorTheme = preferences.colorTheme; - this.localeTimezone = preferences.localeTimezone; - this.allowUntrustedCAs = preferences.allowUntrustedCAs; - this.allowTelemetry = preferences.allowTelemetry; - this.downloadMirror = preferences.downloadMirror; - this.downloadKubectlBinaries = preferences.downloadKubectlBinaries; - this.downloadBinariesPath = preferences.downloadBinariesPath; - this.kubectlBinariesPath = preferences.kubectlBinariesPath; - this.openAtLogin = preferences.openAtLogin; - - if (preferences.hiddenTableColumns) { - this.hiddenTableColumns.replace( - preferences.hiddenTableColumns - .map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)]) - ); - } - - if (preferences.syncKubeconfigEntries) { - this.syncKubeconfigEntries.replace( - preferences.syncKubeconfigEntries.map(({ filePath, ...rest }) => [filePath, rest]) - ); - } + this.httpsProxy = DESCRIPTORS.httpsProxy.fromStore(preferences?.httpsProxy); + this.shell = DESCRIPTORS.shell.fromStore(preferences?.shell); + this.colorTheme = DESCRIPTORS.colorTheme.fromStore(preferences?.colorTheme); + this.localeTimezone = DESCRIPTORS.localeTimezone.fromStore(preferences?.localeTimezone); + this.allowUntrustedCAs = DESCRIPTORS.allowUntrustedCAs.fromStore(preferences?.allowUntrustedCAs); + this.allowTelemetry = DESCRIPTORS.allowTelemetry.fromStore(preferences?.allowTelemetry); + this.downloadMirror = DESCRIPTORS.downloadMirror.fromStore(preferences?.downloadMirror); + this.downloadKubectlBinaries = DESCRIPTORS.downloadKubectlBinaries.fromStore(preferences?.downloadKubectlBinaries); + this.downloadBinariesPath = DESCRIPTORS.downloadBinariesPath.fromStore(preferences?.downloadBinariesPath); + this.kubectlBinariesPath = DESCRIPTORS.kubectlBinariesPath.fromStore(preferences?.kubectlBinariesPath); + this.openAtLogin = DESCRIPTORS.openAtLogin.fromStore(preferences?.openAtLogin); + this.hiddenTableColumns.replace(DESCRIPTORS.hiddenTableColumns.fromStore(preferences?.hiddenTableColumns)); + this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries)); } toJSON(): UserStoreModel { - const hiddenTableColumns: [string, string[]][] = []; - const syncKubeconfigEntries: KubeconfigSyncEntry[] = []; - - for (const [key, values] of this.hiddenTableColumns.entries()) { - hiddenTableColumns.push([key, Array.from(values)]); - } - - for (const [filePath, rest] of this.syncKubeconfigEntries) { - syncKubeconfigEntries.push({ filePath, ...rest }); - } - const model: UserStoreModel = { lastSeenAppVersion: this.lastSeenAppVersion, preferences: { - httpsProxy: toJS(this.httpsProxy), - shell: toJS(this.shell), - colorTheme: toJS(this.colorTheme), - localeTimezone: toJS(this.localeTimezone), - allowUntrustedCAs: toJS(this.allowUntrustedCAs), - allowTelemetry: toJS(this.allowTelemetry), - downloadMirror: toJS(this.downloadMirror), - downloadKubectlBinaries: toJS(this.downloadKubectlBinaries), - downloadBinariesPath: toJS(this.downloadBinariesPath), - kubectlBinariesPath: toJS(this.kubectlBinariesPath), - openAtLogin: toJS(this.openAtLogin), - hiddenTableColumns, - syncKubeconfigEntries, + httpsProxy: DESCRIPTORS.httpsProxy.toStore(this.httpsProxy), + shell: DESCRIPTORS.shell.toStore(this.shell), + colorTheme: DESCRIPTORS.colorTheme.toStore(this.colorTheme), + localeTimezone: DESCRIPTORS.localeTimezone.toStore(this.localeTimezone), + allowUntrustedCAs: DESCRIPTORS.allowUntrustedCAs.toStore(this.allowUntrustedCAs), + allowTelemetry: DESCRIPTORS.allowTelemetry.toStore(this.allowTelemetry), + downloadMirror: DESCRIPTORS.downloadMirror.toStore(this.downloadMirror), + downloadKubectlBinaries: DESCRIPTORS.downloadKubectlBinaries.toStore(this.downloadKubectlBinaries), + downloadBinariesPath: DESCRIPTORS.downloadBinariesPath.toStore(this.downloadBinariesPath), + kubectlBinariesPath: DESCRIPTORS.kubectlBinariesPath.toStore(this.kubectlBinariesPath), + openAtLogin: DESCRIPTORS.openAtLogin.toStore(this.openAtLogin), + hiddenTableColumns: DESCRIPTORS.hiddenTableColumns.toStore(this.hiddenTableColumns), + syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries), }, }; diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts index 6b53a19537..ca8eb3eb61 100644 --- a/src/common/utils/singleton.ts +++ b/src/common/utils/singleton.ts @@ -46,12 +46,15 @@ export class Singleton { static createInstance(this: StaticThis, ...args: R): T { if (!Singleton.instances.has(this)) { if (Singleton.creating.length > 0) { - throw new TypeError("Cannot create a second singleton while creating a first"); + throw new TypeError(`Cannot create a second singleton (${this.name}) while creating a first (${Singleton.creating})`); } - Singleton.creating = this.name; - Singleton.instances.set(this, new this(...args)); - Singleton.creating = ""; + try { + Singleton.creating = this.name; + Singleton.instances.set(this, new this(...args)); + } finally { + Singleton.creating = ""; + } } return Singleton.instances.get(this) as T; diff --git a/src/common/weblink-store.ts b/src/common/weblink-store.ts index 7b3a62f946..a91f83afa2 100644 --- a/src/common/weblink-store.ts +++ b/src/common/weblink-store.ts @@ -58,7 +58,8 @@ export class WeblinkStore extends BaseStore { this.load(); } - @action protected async fromStore(data: Partial = {}) { + @action + protected fromStore(data: Partial = {}) { this.weblinks = data.weblinks || []; } diff --git a/src/migrations/helpers.ts b/src/migrations/helpers.ts index 623d46a63e..8ed4242652 100644 --- a/src/migrations/helpers.ts +++ b/src/migrations/helpers.ts @@ -44,8 +44,10 @@ export function joinMigrations(...declarations: MigrationDeclaration[]): Migrati return Object.fromEntries( iter.map( - migrations, + migrations, ([v, fns]) => [v, (store: Conf) => { + migrationLog(`Running ${v} migration for ${store.path}`); + for (const fn of fns) { fn(store); } diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts index 9a696ba36c..af6af5370c 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -75,6 +75,11 @@ export default { for (const workspaceId of cluster.workspaces ?? [cluster.workspace].filter(Boolean)) { const workspaceHotbar = workspaceHotbars.get(workspaceId); + if (!workspaceHotbar) { + migrationLog(`Cluster ${uid} has unknown workspace ID, skipping`); + continue; + } + migrationLog(`Adding cluster ${uid} to ${workspaceHotbar.name}`); if (workspaceHotbar?.items.length < defaultHotbarCells) { diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index 7fc5c56dcf..493d9bea70 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -35,16 +35,17 @@ import { clusterViewURL } from "../../../common/routes"; @observer export class CommandDialog extends React.Component { @observable menuIsOpen = true; + @observable searchValue: any = undefined; constructor(props: {}) { super(props); makeObservable(this); } - + @computed get activeEntity(): CatalogEntity | undefined { return catalogEntityRegistry.activeEntity; } - + @computed get options() { const registry = CommandRegistry.getInstance(); @@ -104,14 +105,24 @@ export class CommandDialog extends React.Component { return (