preferences page -- part 2

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman committed 2020-07-23 16:01:32 +03:00
1 parent 24f754697b
commit 6fd9b7cf76
14 files changed
+452 -535

No files matched your search

+22 -5
View File
@@ -1,11 +1,28 @@
import request from "request"
import requestPromise from "request-promise-native"
import { userStore } from "./user-store"
export function globalRequestOpts(requestOpts: request.Options) {
// todo: get rid of "request" (deprecated)
// https://github.com/lensapp/lens/issues/459
function getDefaultRequestOpts(): Partial<request.Options> {
const { httpsProxy, allowUntrustedCAs } = userStore.preferences
if (httpsProxy) {
requestOpts.proxy = httpsProxy
return {
proxy: httpsProxy || undefined,
rejectUnauthorized: !allowUntrustedCAs,
}
requestOpts.rejectUnauthorized = !allowUntrustedCAs;
return requestOpts
}
/**
* @deprecated
*/
export function customRequest(opts: request.Options) {
return request.defaults(getDefaultRequestOpts())(opts)
}
/**
* @deprecated
*/
export function customRequestPromise(opts: requestPromise.Options) {
return requestPromise.defaults(getDefaultRequestOpts())(opts)
}
+6
View File
@@ -13,6 +13,12 @@ export enum ThemeType {
DARK = "dark",
}
export interface Theme {
name: string;
type: ThemeType;
colors?: Record<string, string>;
}
export interface UserStoreModel {
lastSeenAppVersion: string;
seenContexts: string[];
+3 -2
View File
@@ -6,12 +6,13 @@ import { promiseExec } from "../promise-exec"
import { helmCli } from "./helm-cli"
type CachedYaml = {
entries: any;
entries: any; // todo: types
}
export class HelmChartManager {
protected cache: any
protected cache: any = {}
protected repo: HelmRepo
constructor(repo: HelmRepo){
this.cache = HelmRepoManager.cache
this.repo = repo
+57 -37
View File
@@ -1,28 +1,51 @@
import fs from "fs";
import logger from "../logger";
import * as yaml from "js-yaml";
import yaml from "js-yaml";
import { readFile } from "fs-extra";
import { promiseExec } from "../promise-exec";
import { helmCli } from "./helm-cli";
import { Singleton } from "../../common/utils/singleton";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy";
import logger from "../logger";
export type HelmEnv = Record<string, string> & {
HELM_REPOSITORY_CACHE?: string;
HELM_REPOSITORY_CONFIG?: string;
}
export type HelmRepo = {
export interface HelmRepoConfig {
repositories: HelmRepo[]
}
export interface HelmRepo {
name: string;
url: string;
cacheFilePath?: string;
cacheFilePath?: string
caFile?: string,
certFile?: string,
insecure_skip_tls_verify?: boolean,
keyFile?: string,
username?: string,
password?: string,
}
export class HelmRepoManager extends Singleton {
static cache = {}
static cache = {} // todo: remove implicit updates in helm-chart-manager.ts
protected repos: HelmRepo[];
protected helmEnv: HelmEnv
protected initialized: boolean
public async init() {
async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
uri: "https://hub.helm.sh/assets/js/repos.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
return orderBy<HelmRepo>(res.body.data, repo => repo.name);
}
async init() {
await helmCli.ensureBinary();
if (!this.initialized) {
this.helmEnv = await this.parseHelmEnv()
@@ -47,60 +70,57 @@ export class HelmRepoManager extends Singleton {
return env
}
public async repositories(): Promise<Array<HelmRepo>> {
public async repositories(): Promise<HelmRepo[]> {
if (!this.initialized) {
await this.init()
}
const repositoryFilePath = this.helmEnv.HELM_REPOSITORY_CONFIG
const repoFile = await fs.promises.readFile(repositoryFilePath, 'utf8').catch(async (error) => {
return null
})
if (!repoFile) {
await this.addRepo({ name: "stable", url: "https://kubernetes-charts.storage.googleapis.com/" })
return await this.repositories()
}
try {
const repositories = yaml.safeLoad(repoFile)
const result = repositories['repositories'].map((repository: HelmRepo) => {
return {
name: repository.name,
url: repository.url,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repository['name']}-index.yaml`
}
});
if (result.length == 0) {
await this.addRepo({ name: "stable", url: "https://kubernetes-charts.storage.googleapis.com/" })
return await this.repositories()
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, 'utf8')
.then((yamlContent: string) => yaml.safeLoad(yamlContent))
.catch(() => ({
repositories: []
}));
if (!repositories.length) {
await this.addRepo({ name: "stable", url: "https://kubernetes-charts.storage.googleapis.com/" });
return await this.repositories();
}
return result
return repositories.map(repo => ({
...repo,
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
}));
} catch (error) {
logger.debug(error)
logger.error(`[HELM]: repositories listing error "${error}"`)
return []
}
}
public async repository(name: string) {
const repositories = await this.repositories()
return repositories.find((repo: HelmRepo) => {
return repo.name == name
})
return repositories.find(repo => repo.name == name);
}
public async update() {
const helm = await helmCli.binaryPath()
logger.debug(`${helm} repo update`)
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
return { stdout: error.stdout }
})
return stdout
}
public async addRepo(repository: HelmRepo) {
public async addRepo({ name, url }: HelmRepo) {
logger.info(`[HELM]: adding repo "${name}" from ${url}`);
const helm = await helmCli.binaryPath()
logger.debug(`${helm} repo add ${repository.name} ${repository.url}`)
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
throw(error.stderr)
})
return stdout
}
const { stdout } = await promiseExec(`"${helm}" repo add ${repository.name} ${repository.url}`).catch((error) => {
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo "${name}" from ${url}`);
const helm = await helmCli.binaryPath()
const { stdout, stderr } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
throw(error.stderr)
})
return stdout
+35 -37
View File
@@ -1,15 +1,15 @@
import { app, remote } from "electron"
import path from "path"
import fs from "fs"
import request from "request"
import { promiseExec} from "./promise-exec"
import { promiseExec } from "./promise-exec"
import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra"
import { globalRequestOpts } from "../common/request"
import * as lockFile from "proper-lockfile"
import { helmCli } from "./helm/helm-cli"
import { userStore } from "../common/user-store"
import { getBundledKubectlVersion} from "../common/utils/app-version"
import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version"
import { isDevelopment, isWindows } from "../common/vars";
const bundledVersion = getBundledKubectlVersion()
const kubectlMap: Map<string, string> = new Map([
@@ -32,18 +32,18 @@ const packageMirrors: Map<string, string> = new Map([
["china", "https://mirror.azure.cn/kubernetes/kubectl"]
])
let bundledPath: string
const initScriptVersionString = "# lens-initscript v3\n"
const isDevelopment = process.env.NODE_ENV !== "production"
let bundledPath: string = null
if(isDevelopment) {
if (isDevelopment) {
bundledPath = path.join(process.cwd(), "binaries", "client", process.platform, process.arch, "kubectl")
} else {
bundledPath = path.join(process.resourcesPath, process.arch, "kubectl")
}
if(process.platform === "win32") bundledPath = `${bundledPath}.exe`
if (isWindows) {
bundledPath = `${bundledPath}.exe`
}
export class Kubectl {
public kubectlVersion: string
@@ -52,7 +52,7 @@ export class Kubectl {
protected path: string
protected dirname: string
static get kubectlDir(){
static get kubectlDir() {
return path.join((app || remote.app).getPath("userData"), "binaries", "kubectl")
}
@@ -62,7 +62,7 @@ export class Kubectl {
// Returns the single bundled Kubectl instance
public static bundled() {
if(!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion)
if (!Kubectl.bundledInstance) Kubectl.bundledInstance = new Kubectl(Kubectl.bundledKubectlVersion)
return Kubectl.bundledInstance
}
@@ -71,7 +71,7 @@ export class Kubectl {
const minorVersion = versionParts[1]
/* minorVersion is the first two digits of kube server version
if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */
if(kubectlMap.has(minorVersion)) {
if (kubectlMap.has(minorVersion)) {
this.kubectlVersion = kubectlMap.get(minorVersion)
logger.debug("Set kubectl version " + this.kubectlVersion + " for cluster version " + clusterVersion + " using version map")
} else {
@@ -81,16 +81,16 @@ export class Kubectl {
let arch = null
if(process.arch == "x64") {
if (process.arch == "x64") {
arch = "amd64"
} else if(process.arch == "x86" || process.arch == "ia32") {
} else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386"
} else {
arch = process.arch
}
const platformName = process.platform === "win32" ? "windows" : process.platform
const binaryName = process.platform === "win32" ? "kubectl.exe" : "kubectl"
const platformName = isWindows ? "windows" : process.platform
const binaryName = isWindows ? "kubectl.exe" : "kubectl"
this.url = `${this.getDownloadMirror()}/v${this.kubectlVersion}/bin/${platformName}/${arch}/${binaryName}`
@@ -102,7 +102,7 @@ export class Kubectl {
try {
await this.ensureKubectl()
return this.path
} catch(err) {
} catch (err) {
logger.error("Failed to ensure kubectl, fallback to the bundled version")
logger.error(err)
return Kubectl.bundledKubectlPath
@@ -113,7 +113,7 @@ export class Kubectl {
try {
await this.ensureKubectl()
return this.dirname
} catch(err) {
} catch (err) {
logger.error(err)
return ""
}
@@ -138,8 +138,7 @@ export class Kubectl {
return true
}
logger.error(`Local kubectl is version ${version}, expected ${this.kubectlVersion}, unlinking`)
}
catch(err) {
} catch (err) {
logger.error(`Local kubectl failed to run properly (${err.message}), unlinking`)
}
await fs.promises.unlink(this.path)
@@ -148,7 +147,7 @@ export class Kubectl {
}
protected async checkBundled(): Promise<boolean> {
if(this.kubectlVersion === Kubectl.bundledKubectlVersion) {
if (this.kubectlVersion === Kubectl.bundledKubectlVersion) {
try {
const exist = await pathExists(this.path)
if (!exist) {
@@ -156,7 +155,7 @@ export class Kubectl {
await fs.promises.chmod(this.path, 0o755)
}
return true
} catch(err) {
} catch (err) {
logger.error("Could not copy the bundled kubectl to app-data: " + err)
return false
}
@@ -171,10 +170,15 @@ export class Kubectl {
logger.debug(`Acquired a lock for ${this.kubectlVersion}`)
const bundled = await this.checkBundled()
const isValid = await this.checkBinary(!bundled)
if(!isValid) {
await this.downloadKubectl().catch((error) => { logger.error(error) });
if (!isValid) {
await this.downloadKubectl().catch((error) => {
logger.error(error)
});
}
await this.writeInitScripts().catch((error) => { logger.error("Failed to write init scripts"); logger.error(error) })
await this.writeInitScripts().catch((error) => {
logger.error("Failed to write init scripts");
logger.error(error)
})
logger.debug(`Releasing lock for ${this.kubectlVersion}`)
release()
return true
@@ -190,10 +194,10 @@ export class Kubectl {
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`)
return new Promise((resolve, reject) => {
const stream = request({
const stream = customRequest({
url: this.url,
gzip: true,
...this.getRequestOpts()
})
});
const file = fs.createWriteStream(this.path)
stream.on("complete", () => {
logger.debug("kubectl binary download finished")
@@ -215,7 +219,7 @@ export class Kubectl {
protected async scriptIsLatest(scriptPath: string) {
const scriptExists = await pathExists(scriptPath)
if(!scriptExists) return false
if (!scriptExists) return false
try {
const filehandle = await fs.promises.open(scriptPath, 'r')
@@ -234,7 +238,7 @@ export class Kubectl {
const fsPromises = fs.promises;
const bashScriptPath = path.join(this.dirname, '.bash_set_path')
const bashScriptIsLatest = await this.scriptIsLatest(bashScriptPath)
if(!bashScriptIsLatest) {
if (!bashScriptIsLatest) {
let bashScript = "" + initScriptVersionString
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n"
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n"
@@ -253,7 +257,7 @@ export class Kubectl {
const zshScriptPath = path.join(this.dirname, '.zlogin')
const zshScriptIsLatest = await this.scriptIsLatest(zshScriptPath)
if(!zshScriptIsLatest) {
if (!zshScriptIsLatest) {
let zshScript = "" + initScriptVersionString
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n"
@@ -280,12 +284,6 @@ export class Kubectl {
}
}
protected getRequestOpts() {
return globalRequestOpts({
url: this.url
})
}
protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences.downloadMirror)
if (mirror) {
@@ -1,259 +0,0 @@
<template>
<div class="content">
<ClosePageButton />
<div class="container-fluid lens-preferences">
<div class="header sticky-top">
<h2>Preferences</h2>
</div>
<div class="row">
<div class="col-3" />
<div ref="content" class="global-settings col-6">
<h2>Color Theme</h2>
<b-form-group
label="Cluster Color theme:"
label-for="input-color-theme"
>
<b-form-select
id="input-color-theme"
v-model="preferences.colorTheme"
@change="onSave"
:options="colorThemeOptions"
/>
</b-form-group>
</div>
<div class="col-3" />
</div>
<div class="row">
<div class="col-3" />
<div ref="content" class="global-settings col-6">
<h2>Download Mirror</h2>
<b-form-group
label="Download mirror for kubectl:"
label-for="input-download-mirror"
>
<b-form-select
id="input-download-mirror"
v-model="preferences.downloadMirror"
@change="onSave"
:options="downloadMirrorOptions"
/>
</b-form-group>
</div>
<div class="col-3" />
</div>
<div class="row">
<div class="col-3" />
<div ref="content" class="global-settings col-6">
<h2>Helm</h2>
<b-form-group
label="Repositories:"
label-for="textarea-helm-repositories"
>
<b-input-group>
<b-form-select
:options="hubRepositories"
v-model="selectedHubRepo"
>
<template v-slot:first>
<option value="">
Select Helm repository
</option>
</template>
</b-form-select>
<b-input-group-append>
<b-button :disabled="!selectedHubRepo" @click="addHubRepo" variant="primary">
Add <b-spinner small v-if="isHelmProcessing" label="Small Spinner" />
</b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<b-list-group>
<b-list-group-item v-for="repo in helmRepos" :key="repo.name">
<span v-b-tooltip.hover v-b-tooltip.right :title="repo.url">{{ repo.name }}</span>
<button @click="removeRepo(repo)" :data-repo="repo.name" class="close">
<i class="material-icons">delete</i>
</button>
</b-list-group-item>
</b-list-group>
</div>
<div class="col-3" />
</div>
<div class="row">
<div class="col-3" />
<div ref="content" class="global-settings col-6">
<h2>HTTP Proxy</h2>
<b-form-group
label="Proxy URL:"
label-for="input-https-proxy"
description="Proxy is used only for non-cluster communication."
>
<b-form-input
id="input-https-proxy"
v-model="preferences.httpsProxy"
trim
@blur="onSave"
placeholder="Type HTTP proxy url (example: http://proxy.acme.org:8080)"
/>
</b-form-group>
<h2>Certificate Trust</h2>
<b-form-group
label-for="checkbox-allow-untrusted-cas"
description="This will make Lens to trust ANY certificate authority without any validations. Needed with some corporate proxies that do certificate re-writing. Does not affect cluster communications!"
>
<b-form-checkbox
id="checkbox-allow-untrusted-cas"
switch
v-model="preferences.allowUntrustedCAs"
@input="onSave"
>
Allow untrusted Certificate Authorities
</b-form-checkbox>
</b-form-group>
<h2>Telemetry & Usage Tracking</h2>
<b-form-group
label-for="checkbox-allow-telemetry"
description="Telemetry & usage data is collected to continuously improve the Lens experience."
>
<b-form-checkbox
id="checkbox-allow-telemetry"
switch
v-model="preferences.allowTelemetry"
@input="onSave"
>
Allow telemetry & usage tracking
</b-form-checkbox>
</b-form-group>
</div>
<div class="col-3" />
</div>
</div>
</div>
</template>
<script>
import request from 'request-promise-native'
import { globalRequestOpts } from "../../../common/request"
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
export default {
name: 'PreferencesPage',
components: {
ClosePageButton
},
data(){
return {
hubRepositories: [],
selectedHubRepo: "",
colorThemeOptions: [
{ value: "dark", text: "Dark" },
{ value: "light", text: "Light" },
],
downloadMirrorOptions: [
{ value: "default", text: "Default (Google)" },
{ value: "china", text: "China (Azure)" },
],
isHelmProcessing: null,
}
},
computed: {
helmRepos: function() {
return this.$store.getters.repos;
},
preferences: function() {
return this.$store.getters.preferences;
},
},
methods: {
onSave: function() {
this.$store.commit("savePreferences", this.preferences);
},
request: async function(opts) {
const o = Object.assign({
timeout: 10000,
headers: {},
resolveWithFullResponse: true,
json: true
}, globalRequestOpts(opts))
return await request(o)
},
repoCompare: function( a, b ) {
if ( a.name < b.name ){
return -1;
}
if ( a.name > b.name ){
return 1;
}
return 0;
},
addHubRepo: async function() {
if(!this.selectedHubRepo || this.selectedHubRepo === "") {
return
}
this.isHelmProcessing = true
const [name, url ] = this.selectedHubRepo.split("|")
await this.$store.dispatch('addHelmRepo', { name, url })
this.selectedHubRepo = ""
this.isHelmProcessing = false
},
removeRepo: async function(repo) {
await this.$store.dispatch('removeHelmRepo', repo)
},
loadHubRepositories: async function() {
const res = await this.request({ uri: "https://hub.helm.sh/assets/js/repos.json"}).catch((error) => { this.hubRepositories = [] })
this.hubRepositories = res.body.data.sort(this.repoCompare).map((repo) => {
return {
text: repo.name,
value: repo.name+"|"+repo.url
}
})
}
},
created() {
this.$store.dispatch('refreshHelmRepos')
},
mounted: async function() {
this.$store.commit("hideMenu");
this.loadHubRepositories()
},
destroyed: function() {
this.$store.commit("showMenu");
}
}
</script>
<style lang="scss" scoped>
#app > .main-view > .content {
left: 70px;
}
.lens-preferences {
height: 100%;
overflow-y: scroll;
& input {
background-color: #252729 !important;
border: 0px !important;
color: #87909c !important;
}
.header {
padding-top: 15px;
}
.close {
text-shadow: none;
}
.list-group-item {
background-color: #252729 !important;
color: #87909c !important;
}
& i {
color: #87909c !important;
}
}
.global-settings {
padding-top: 0.75em;
}
</style>
@@ -1,9 +1,23 @@
.Preferences {
--flex-gap: #{$padding};
h2 {
&:not(:first-child) {
margin-top: $padding * 3;
}
}
.info-block {
--flex-gap: #{$padding};
}
.repos {
--flex-gap: #{$padding};
> .title {
font-style: italic;
}
.Badge {
margin: $padding / 2;
}
}
}
@@ -1,45 +1,121 @@
import "./preferences.scss"
import React, { Fragment } from "react";
import { observer } from "mobx-react";
import { action, computed, observable } from "mobx";
import { t, Trans } from "@lingui/macro";
import { _i18n } from "../../i18n";
import { WizardLayout } from "../layout/wizard-layout";
import { Icon } from "../icon";
import { Select, SelectOption } from "../select";
import { ThemeType, userStore } from "../../../common/user-store";
import { userStore } from "../../../common/user-store";
import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager";
import { Input } from "../input";
import { Checkbox } from "../checkbox";
type ThemeSelectOption = SelectOption & { type: ThemeType }
import { Notifications } from "../notifications";
import { Badge } from "../badge";
import { Spinner } from "../spinner";
@observer
export class Preferences extends React.Component {
themeOptions: ThemeSelectOption[] = [
{ value: "kontena-dark", label: <Trans>Dark</Trans>, type: ThemeType.DARK },
{ value: "kontena-light", label: <Trans>Light</Trans>, type: ThemeType.LIGHT },
@observable helmLoading = false;
@observable helmUpdating = false;
@observable helmRepos: HelmRepo[] = [];
@observable helmAddedRepos = observable.map<string, HelmRepo>();
@observable themeOptions: SelectOption<string>[] = [
{ value: "kontena-dark", label: <Trans>Dark</Trans> },
{ value: "kontena-light", label: <Trans>Light</Trans> },
]
downloadMirrorOptions: SelectOption[] = [
@observable downloadMirrorOptions: SelectOption<string>[] = [
{ value: "default", label: "Default (Google)" },
{ value: "china", label: "China (Azure)" },
]
onThemeChange = ({ value }: ThemeSelectOption) => {
@computed get helmOptions(): SelectOption<HelmRepo>[] {
return this.helmRepos.map(repo => ({
value: repo,
label: repo.name,
}))
}
async componentDidMount() {
await this.loadHelmRepos();
}
@action
async loadHelmRepos() {
this.helmLoading = true;
try {
if (!this.helmRepos.length) {
this.helmRepos = await repoManager.loadAvailableRepos(); // via https://helm.sh
}
const repos = await repoManager.repositories(); // via helm-cli
this.helmAddedRepos.clear();
repos.forEach(repo => this.helmAddedRepos.set(repo.name, repo));
} catch (err) {
Notifications.error(err);
}
this.helmLoading = false;
}
async addRepo(repo: HelmRepo) {
try {
await repoManager.addRepo(repo);
this.helmAddedRepos.set(repo.name, repo);
} catch (err) {
Notifications.error(<Trans>Adding helm branch <b>{repo.name}</b> has failed: {String(err)}</Trans>)
}
}
async removeRepo(repo: HelmRepo) {
try {
await repoManager.removeRepo(repo);
this.helmAddedRepos.delete(repo.name);
} catch (err) {
Notifications.error(
<Trans>Removing helm branch <b>{repo.name}</b> has failed: {String(err)}</Trans>
)
}
}
onThemeChange = ({ value }: SelectOption<string>) => {
// themeStore.setTheme(value); // fixme: apply theme on the fly for current view
userStore.preferences.colorTheme = value;
}
onRepoSelect = async ({ value: repo }: SelectOption<HelmRepo>) => {
const isAdded = this.helmAddedRepos.has(repo.name);
if (isAdded) {
Notifications.ok(<Trans>Helm branch <b>{repo.name}</b> already in use</Trans>)
return;
}
this.helmUpdating = false;
await this.addRepo(repo);
this.helmUpdating = false;
}
formatHelmOptionLabel = ({ value: repo }: SelectOption<HelmRepo>) => {
const isAdded = this.helmAddedRepos.has(repo.name);
return (
<div className="flex gaps">
<span>{repo.name}</span>
{isAdded && <Icon small material="check" className="box right"/>}
</div>
)
}
renderInfo() {
return (
<Fragment>
<h2>
<Trans>Preferences</Trans>
</h2>
<div className="info-block flex gaps align-center">
<div className="info-block flex gaps align-flex-start">
<Icon small material="info"/>
<small>
<p>
<Trans>Lens Global Settings</Trans> (<Trans>applicable to all clusters</Trans>)
</small>
</p>
</div>
</Fragment>
)
@@ -58,15 +134,41 @@ export class Preferences extends React.Component {
<h2><Trans>Download Mirror</Trans></h2>
<Select
placeholder={_i18n._(t`Download mirror for kubectl`)}
placeholder={<Trans>Download mirror for kubectl</Trans>}
options={this.downloadMirrorOptions}
value={preferences.downloadMirror}
onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
/>
<h2><Trans>Helm</Trans></h2>
<div className="helm">
// todo: added helm repos
<Select
placeholder={<Trans>Repositories</Trans>}
isLoading={this.helmLoading}
isDisabled={this.helmUpdating}
options={this.helmOptions}
onChange={this.onRepoSelect}
formatOptionLabel={this.formatHelmOptionLabel}
controlShouldRenderValue={false}
/>
<div className="repos flex gaps align-center">
<div className="title">
<Trans>Added repos:</Trans>
</div>
<div className="repos-list">
{this.helmLoading && <Spinner/>}
{Array.from(this.helmAddedRepos).map(([name, repo]) => {
return (
<Badge key={name} className="added-repo flex gaps align-center">
<span className="repo">{name}</span>
<Icon
material="remove_circle_outline"
onClick={() => this.removeRepo(repo)}
tooltip={<Trans>Remove</Trans>}
/>
</Badge>
)
})}
</div>
</div>
<h2><Trans>HTTP Proxy</Trans></h2>
+1 -1
View File
@@ -5,8 +5,8 @@ import { cssNames } from "../../utils/cssNames";
import { TooltipDecoratorProps, withTooltip } from "../tooltip";
interface Props extends React.HTMLAttributes<any>, TooltipDecoratorProps {
label: React.ReactNode;
small?: boolean;
label?: React.ReactNode;
}
@withTooltip
@@ -7,9 +7,13 @@
display: grid;
grid-template-columns: 1fr 40%;
> .content-col {
> * {
@include hidden-scrollbar;
--flex-gap: #{$spacing};
padding: $spacing;
}
> .content-col {
margin-right: $spacing;
background-color: var(--clusters-menu-bgc);
border-radius: $radius;
@@ -22,9 +26,6 @@
}
> .info-col {
@include hidden-scrollbar;
--flex-gap: #{$spacing};
padding: $spacing;
border-left: 1px solid #353a3e;
}
+8 -114
View File
@@ -1,126 +1,20 @@
import { action, autorun, computed, observable, reaction } from "mobx";
import { autobind, createStorage } from "./utils";
import { Notifications } from "./components/notifications";
interface ITheme {
name: string;
type: "dark" | "light";
author?: string;
colors?: {
[key: string]: string;
blue: string;
magenta: string;
golden: string;
halfGray: string;
primary: string;
textColorPrimary: string;
textColorSecondary: string;
textColorAccent: string;
borderColor: string;
borderFaintColor: string;
mainBackground: string;
contentColor: string;
layoutBackground: string;
layoutTabsBackground: string;
layoutTabsActiveColor: string;
layoutTabsLineColor: string;
sidebarLogoBackground: string;
sidebarActiveColor: string;
sidebarSubmenuActiveColor: string;
sidebarBackground: string;
buttonPrimaryBackground: string;
buttonDefaultBackground: string;
buttonAccentBackground: string;
buttonDisabledBackground: string;
tableBgcStripe: string;
tableBgcSelected: string;
tableHeaderBackground: string;
tableHeaderBorderWidth: string;
tableHeaderBorderColor: string;
tableHeaderColor: string;
tableSelectedRowColor: string;
helmLogoBackground: string;
helmImgBackground: string;
helmStableRepo: string;
helmIncubatorRepo: string;
helmDescriptionHr: string;
helmDescriptionBlockqouteColor: string;
helmDescriptionBlockqouteBorder: string;
helmDescriptionBlockquoteBackground: string;
helmDescriptionHeaders: string;
helmDescriptionH6: string;
helmDescriptionTdBorder: string;
helmDescriptionTrBackground: string;
helmDescriptionCodeBackground: string;
helmDescriptionPreBackground: string;
helmDescriptionPreColor: string;
colorSuccess: string;
colorOk: string;
colorInfo: string;
colorError: string;
colorSoftError: string;
colorWarning: string;
colorVague: string;
colorTerminated: string;
dockHeadBackground: string;
dockInfoBackground: string;
dockInfoBorderColor: string;
terminalBackground: string;
terminalForeground: string;
terminalCursor: string;
terminalCursorAccent: string;
terminalSelection: string;
terminalBlack: string;
terminalRed: string;
terminalGreen: string;
terminalYellow: string;
terminalBlue: string;
terminalMagenta: string;
terminalCyan: string;
terminalWhite: string;
terminalBrightBlack: string;
terminalBrightRed: string;
terminalBrightGreen: string;
terminalBrightYellow: string;
terminalBrightBlue: string;
terminalBrightMagenta: string;
terminalBrightCyan: string;
terminalBrightWhite: string;
dialogTextColor: string;
dialogBackground: string;
dialogHeaderBackground: string;
dialogFooterBackground: string;
drawerTogglerBackground: string;
drawerTitleText: string;
drawerSubtitleBackground: string;
drawerItemNameColor: string;
drawerItemValueColor: string;
boxShadow: string;
iconActiveColor: string;
iconActiveBackground: string;
filterAreaBackground: string;
chartLiveBarBackgound: string;
chartStripesColor: string;
chartCapacityColor: string;
pieChartDefaultColor: string;
selectOptionHoveredColor: string;
lineProgressBackground: string;
radioActiveBackground: string;
};
}
import { Theme, ThemeType } from "../common/user-store";
@autobind()
export class ThemeStore {
protected style = document.createElement("style");
readonly defaultTheme: ITheme = {
readonly defaultTheme: Theme = {
name: "kontena-dark",
type: "dark",
colors: {} as any,
type: ThemeType.DARK,
colors: {},
};
@observable activeThemeId = this.defaultTheme.name; // theme's filename without extension
@observable themes = observable.map<string, ITheme>([], { deep: false });
@observable themes = observable.map<string, Theme>([], { deep: false });
@computed get activeTheme() {
return this.themes.get(this.activeThemeId) || this.defaultTheme;
@@ -157,7 +51,7 @@ export class ThemeStore {
})
}
protected onChange = (theme: ITheme) => {
protected onChange = (theme: Theme) => {
let cssText = "\n"
Object.entries(theme.colors).forEach(([propName, color]) => {
cssText += `--${propName}: ${color} !important;\n`
@@ -165,12 +59,12 @@ export class ThemeStore {
this.style.textContent = `:root {${cssText}} `;
}
async load(themeId: string, { showErrorNotification = true } = {}): Promise<ITheme> {
async load(themeId: string, { showErrorNotification = true } = {}): Promise<Theme> {
if (this.themes.has(themeId)) {
return this.themes.get(themeId);
}
try {
const theme: ITheme = require( // eslint-disable-line @typescript-eslint/no-var-requires
const theme: Theme = require( // eslint-disable-line @typescript-eslint/no-var-requires
/* webpackMode: "lazy", webpackChunkName: "theme/[request]" */
`./themes/${themeId}.json`
);