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

Preferences page redesign (#2446)

* Removing header part

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Restyling PageLayout

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Restyling .round-black Input

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding Tab navigation to Preferences

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Styling Application tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add esc button

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Add media queries

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Introducting Switcher component

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Styling Proxy tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Moving start-up switcher to Other tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Styling Kubernetes tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Styling Extensions tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Styling inputs and selects

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Styling helm chart section

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Create a telemetry tab with extensions

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Adding lens Select theme

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Remove Other tab

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fix mainBackground color

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Simplifying Tabs boilerplate

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Replacing button font

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing one-column settings layout

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixing integration tests

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Fixin tests harder

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>

* Showing bottom bar in workspaces

Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com>
This commit is contained in:
Alex Andreev 2021-04-06 15:45:23 +03:00 committed by GitHub
parent 33c405bdcf
commit 84cc0cdf55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 572 additions and 237 deletions

View File

@ -8,6 +8,7 @@ export default class SurveyRendererExtension extends LensRendererExtension {
appPreferences = [ appPreferences = [
{ {
title: "In-App Surveys", title: "In-App Surveys",
showInPreferencesTab: "telemetry",
components: { components: {
Hint: () => <SurveyPreferenceHint/>, Hint: () => <SurveyPreferenceHint/>,
Input: () => <SurveyPreferenceInput survey={surveyPreferencesStore}/> Input: () => <SurveyPreferenceInput survey={surveyPreferencesStore}/>

View File

@ -8,6 +8,7 @@ export default class TelemetryRendererExtension extends LensRendererExtension {
appPreferences = [ appPreferences = [
{ {
title: "Telemetry & Usage Tracking", title: "Telemetry & Usage Tracking",
showInPreferencesTab: "telemetry",
id: "telemetry-tracking", id: "telemetry-tracking",
components: { components: {
Hint: () => <TelemetryPreferenceHint/>, Hint: () => <TelemetryPreferenceHint/>,

View File

@ -36,7 +36,7 @@ describe("Lens integration tests", () => {
it('shows "add cluster"', async () => { it('shows "add cluster"', async () => {
await app.electron.ipcRenderer.send("test-menu-item-click", "File", "Add Cluster"); await app.electron.ipcRenderer.send("test-menu-item-click", "File", "Add Cluster");
await app.client.waitUntilTextExists("h2", "Add Cluster"); await app.client.waitUntilTextExists("h2", "Add Clusters from Kubeconfig");
}); });
describe("preferences page", () => { describe("preferences page", () => {
@ -44,7 +44,17 @@ describe("Lens integration tests", () => {
const appName: string = process.platform === "darwin" ? "Lens" : "File"; const appName: string = process.platform === "darwin" ? "Lens" : "File";
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences"); await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
await app.client.waitUntilTextExists("h2", "Preferences"); await app.client.waitUntilTextExists("[data-testid=application-header]", "APPLICATION");
});
it("shows all tabs and their contents", async () => {
await app.client.click("[data-testid=application-tab]");
await app.client.click("[data-testid=proxy-tab]");
await app.client.waitUntilTextExists("[data-testid=proxy-header]", "PROXY");
await app.client.click("[data-testid=kube-tab]");
await app.client.waitUntilTextExists("[data-testid=kubernetes-header]", "KUBERNETES");
await app.client.click("[data-testid=telemetry-tab]");
await app.client.waitUntilTextExists("[data-testid=telemetry-header]", "TELEMETRY");
}); });
it("ensures helm repos", async () => { it("ensures helm repos", async () => {
@ -54,7 +64,8 @@ describe("Lens integration tests", () => {
fail("Lens failed to add Bitnami repository"); fail("Lens failed to add Bitnami repository");
} }
await app.client.waitUntilTextExists("div.repos #message-bitnami", repos[0].name); // wait for the helm-cli to fetch the repo(s) await app.client.click("[data-testid=kube-tab]");
await app.client.waitUntilTextExists("div.repos .repoName", repos[0].name); // wait for the helm-cli to fetch the repo(s)
await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down await app.client.click("#HelmRepoSelect"); // click the repo select to activate the drop-down
await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text) await app.client.waitUntilTextExists("div.Select__option", ""); // wait for at least one option to appear (any text)
}); });

View File

@ -80,7 +80,7 @@ export async function appStart() {
export async function clickWhatsNew(app: Application) { export async function clickWhatsNew(app: Application) {
await app.client.waitUntilTextExists("h1", "What's new?"); await app.client.waitUntilTextExists("h1", "What's new?");
await app.client.click("button.primary"); await app.client.click("button.primary");
await app.client.waitUntilTextExists("h2", "default"); await app.client.waitUntilTextExists("h5", "Clusters");
} }
export async function clickWelcomeNotification(app: Application) { export async function clickWelcomeNotification(app: Application) {
@ -89,7 +89,7 @@ export async function clickWelcomeNotification(app: Application) {
if (itemsText === "0 item") { if (itemsText === "0 item") {
// welcome notification should be present, dismiss it // welcome notification should be present, dismiss it
await app.client.waitUntilTextExists("div.message", "Welcome!"); await app.client.waitUntilTextExists("div.message", "Welcome!");
await app.client.click("i.Icon.close"); await app.client.click(".notification i.Icon.close");
} }
} }

View File

@ -9,6 +9,7 @@ export interface AppPreferenceComponents {
export interface AppPreferenceRegistration { export interface AppPreferenceRegistration {
title: string; title: string;
id?: string; id?: string;
showInPreferencesTab?: string;
components: AppPreferenceComponents; components: AppPreferenceComponents;
} }

View File

@ -2,11 +2,9 @@
--width: 100%; --width: 100%;
--height: 100%; --height: 100%;
text-align: center; text-align: center;
bottom: 22px; // Making bottom bar visible
.content-wrapper { .content-wrapper {
.content { .content {
margin: unset; margin: unset;
max-width: unset; max-width: unset;

View File

@ -1,11 +1,21 @@
.HelmCharts { .HelmCharts {
.repos { .repos {
margin-top: var(--margin); margin-top: 20px;
.Badge { .repo {
display: flex; background: var(--inputControlBackground);
margin-bottom: 1px!important; border-radius: 4px;
padding: 6px 8px; padding: 12px 16px;
box-shadow: 0 0 0 1px var(--secondaryBackground);
.repoName {
font-weight: 500;
margin-bottom: 8px;
}
.repoUrl {
color: var(--textColorDimmed);
}
} }
} }
} }

View File

@ -4,12 +4,10 @@ import React from "react";
import { action, computed, observable } from "mobx"; import { action, computed, observable } from "mobx";
import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager"; import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager";
import { Badge } from "../badge";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { Tooltip } from "../tooltip";
import { AddHelmRepoDialog } from "./add-helm-repo-dialog"; import { AddHelmRepoDialog } from "./add-helm-repo-dialog";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@ -106,6 +104,7 @@ export class HelmCharts extends React.Component {
formatOptionLabel={this.formatOptionLabel} formatOptionLabel={this.formatOptionLabel}
controlShouldRenderValue={false} controlShouldRenderValue={false}
className="box grow" className="box grow"
themeName="lens"
/> />
<Button <Button
primary primary
@ -116,20 +115,18 @@ export class HelmCharts extends React.Component {
<AddHelmRepoDialog onAddRepo={() => this.loadRepos()}/> <AddHelmRepoDialog onAddRepo={() => this.loadRepos()}/>
<div className="repos flex gaps column"> <div className="repos flex gaps column">
{Array.from(this.addedRepos).map(([name, repo]) => { {Array.from(this.addedRepos).map(([name, repo]) => {
const tooltipId = `message-${name}`;
return ( return (
<Badge key={name} className="added-repo flex gaps align-center justify-space-between"> <div key={name} className="repo flex gaps align-center justify-space-between">
<span id={tooltipId} className="repo">{name}</span> <div>
<div className="repoName">{name}</div>
<div className="repoUrl">{repo.url}</div>
</div>
<Icon <Icon
material="delete" material="delete"
onClick={() => this.removeRepo(repo)} onClick={() => this.removeRepo(repo)}
tooltip="Remove" tooltip="Remove"
/> />
<Tooltip targetId={tooltipId} formatters={{ narrow: true }}> </div>
{repo.url}
</Tooltip>
</Badge>
); );
})} })}
</div> </div>

View File

@ -1,11 +1,11 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Checkbox } from "../checkbox";
import { Input, InputValidators } from "../input"; import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { UserPreferences, userStore } from "../../../common/user-store"; import { UserPreferences, userStore } from "../../../common/user-store";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { bundledKubectlPath } from "../../../main/kubectl"; import { bundledKubectlPath } from "../../../main/kubectl";
import { SelectOption, Select } from "../select"; import { SelectOption, Select } from "../select";
import { FormSwitch, Switcher } from "../switch";
export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => { export const KubectlBinaries = observer(({ preferences }: { preferences: UserPreferences }) => {
const [downloadPath, setDownloadPath] = useState(preferences.downloadBinariesPath || ""); const [downloadPath, setDownloadPath] = useState(preferences.downloadBinariesPath || "");
@ -24,12 +24,23 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
return ( return (
<> <>
<SubTitle title="Automatic kubectl binary download"/> <section className="small">
<Checkbox <SubTitle title="Kubectl binary download"/>
label="Download kubectl binaries matching the Kubernetes cluster version" <FormSwitch
value={preferences.downloadKubectlBinaries} control={
onChange={downloadKubectlBinaries => preferences.downloadKubectlBinaries = downloadKubectlBinaries} <Switcher
checked={preferences.downloadKubectlBinaries}
onChange={v => preferences.downloadKubectlBinaries = v.target.checked}
name="kubectl-download"
/> />
}
label="Download kubectl binaries matching the Kubernetes cluster version"
/>
</section>
<hr className="small"/>
<section className="small">
<SubTitle title="Download mirror" /> <SubTitle title="Download mirror" />
<Select <Select
placeholder="Download mirror for kubectl" placeholder="Download mirror for kubectl"
@ -37,7 +48,13 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
value={preferences.downloadMirror} value={preferences.downloadMirror}
onChange={({ value }: SelectOption) => preferences.downloadMirror = value} onChange={({ value }: SelectOption) => preferences.downloadMirror = value}
disabled={!preferences.downloadKubectlBinaries} disabled={!preferences.downloadKubectlBinaries}
themeName="lens"
/> />
</section>
<hr className="small"/>
<section className="small">
<SubTitle title="Directory for binaries" /> <SubTitle title="Directory for binaries" />
<Input <Input
theme="round-black" theme="round-black"
@ -48,9 +65,14 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
onBlur={save} onBlur={save}
disabled={!preferences.downloadKubectlBinaries} disabled={!preferences.downloadKubectlBinaries}
/> />
<small className="hint"> <div className="hint">
The directory to download binaries into. The directory to download binaries into.
</small> </div>
</section>
<hr className="small"/>
<section className="small">
<SubTitle title="Path to kubectl binary" /> <SubTitle title="Path to kubectl binary" />
<Input <Input
theme="round-black" theme="round-black"
@ -61,9 +83,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
onBlur={save} onBlur={save}
disabled={preferences.downloadKubectlBinaries} disabled={preferences.downloadKubectlBinaries}
/> />
<small className="hint"> </section>
The path to the kubectl binary on the system.
</small>
</> </>
); );
}); });

View File

@ -6,22 +6,32 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { userStore } from "../../../common/user-store"; import { userStore } from "../../../common/user-store";
import { isWindows } from "../../../common/vars"; import { isWindows } from "../../../common/vars";
import { appPreferenceRegistry } from "../../../extensions/registries/app-preference-registry"; import { appPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry";
import { themeStore } from "../../theme.store"; import { themeStore } from "../../theme.store";
import { Checkbox } from "../checkbox";
import { Input } from "../input"; import { Input } from "../input";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { Select, SelectOption } from "../select"; import { Select, SelectOption } from "../select";
import { HelmCharts } from "./helm-charts"; import { HelmCharts } from "./helm-charts";
import { KubectlBinaries } from "./kubectl-binaries"; import { KubectlBinaries } from "./kubectl-binaries";
import { ScrollSpy } from "../scroll-spy/scroll-spy";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { Tab, Tabs } from "../tabs";
import { FormSwitch, Switcher } from "../switch";
enum Pages {
Application = "application",
Proxy = "proxy",
Kubernetes = "kubernetes",
Telemetry = "telemetry",
Extensions = "extensions",
Other = "other"
}
@observer @observer
export class Preferences extends React.Component { export class Preferences extends React.Component {
@observable httpProxy = userStore.preferences.httpsProxy || ""; @observable httpProxy = userStore.preferences.httpsProxy || "";
@observable shell = userStore.preferences.shell || ""; @observable shell = userStore.preferences.shell || "";
@observable activeTab = Pages.Application;
@computed get themeOptions(): SelectOption<string>[] { @computed get themeOptions(): SelectOption<string>[] {
return themeStore.themes.map(theme => ({ return themeStore.themes.map(theme => ({
@ -45,9 +55,46 @@ export class Preferences extends React.Component {
]); ]);
} }
onTabChange = (tabId: Pages) => {
this.activeTab = tabId;
};
renderNavigation() {
const extensions = appPreferenceRegistry.getItems().filter(e => !e.showInPreferencesTab);
return (
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
<div className="header">Preferences</div>
<Tab value={Pages.Application} label="Application" data-testid="application-tab"/>
<Tab value={Pages.Proxy} label="Proxy" data-testid="proxy-tab"/>
<Tab value={Pages.Kubernetes} label="Kubernetes" data-testid="kube-tab"/>
<Tab value={Pages.Telemetry} label="Telemetry" data-testid="telemetry-tab"/>
{extensions.length > 0 &&
<Tab value={Pages.Extensions} label="Extensions" data-testid="extensions-tab"/>
}
</Tabs>
);
}
renderExtension({ title, id, components: { Hint, Input } }: RegisteredAppPreference) {
return (
<React.Fragment key={id}>
<section id={id} className="small">
<SubTitle title={title}/>
<Input/>
<div className="hint">
<Hint/>
</div>
</section>
<hr className="small"/>
</React.Fragment>
);
}
render() { render() {
const { preferences } = userStore; const { preferences } = userStore;
const header = <h2>Preferences</h2>; const extensions = appPreferenceRegistry.getItems();
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry);
let defaultShell = process.env.SHELL || process.env.PTYSHELL; let defaultShell = process.env.SHELL || process.env.PTYSHELL;
if (!defaultShell) { if (!defaultShell) {
@ -59,29 +106,59 @@ export class Preferences extends React.Component {
} }
return ( return (
<ScrollSpy htmlFor="ScrollSpyRoot" render={navigation => (
<PageLayout <PageLayout
showOnTop showOnTop
navigation={navigation} navigation={this.renderNavigation()}
className="Preferences" className="Preferences"
contentGaps={false} contentGaps={false}
header={header}
> >
<section id="application" title="Application"> {this.activeTab == Pages.Application && (
<section> <section id="application">
<h1>Application</h1> <h2 data-testid="application-header">Application</h2>
</section>
<section id="appearance"> <section id="appearance">
<h2>Appearance</h2>
<SubTitle title="Theme"/> <SubTitle title="Theme"/>
<Select <Select
options={this.themeOptions} options={this.themeOptions}
value={preferences.colorTheme} value={preferences.colorTheme}
onChange={({ value }: SelectOption) => preferences.colorTheme = value} onChange={({ value }: SelectOption) => preferences.colorTheme = value}
themeName="lens"
/> />
</section> </section>
<hr className="small"/>
<section id="shell" className="small">
<SubTitle title="Terminal Shell Path"/>
<Input
theme="round-black"
placeholder={defaultShell}
value={this.shell}
onChange={v => this.shell = v}
onBlur={() => preferences.shell = this.shell}
/>
</section>
<hr/>
<section id="other">
<SubTitle title="Start-up"/>
<FormSwitch
control={
<Switcher
checked={preferences.openAtLogin}
onChange={v => preferences.openAtLogin = v.target.checked}
name="startup"
/>
}
label="Automatically start Lens on login"
/>
</section>
</section>
)}
{this.activeTab == Pages.Proxy && (
<section id="proxy"> <section id="proxy">
<h2>Proxy</h2> <section>
<h2 data-testid="proxy-header">Proxy</h2>
<SubTitle title="HTTP Proxy"/> <SubTitle title="HTTP Proxy"/>
<Input <Input
theme="round-black" theme="round-black"
@ -93,12 +170,21 @@ export class Preferences extends React.Component {
<small className="hint"> <small className="hint">
Proxy is used only for non-cluster communication. Proxy is used only for non-cluster communication.
</small> </small>
</section>
<hr className="small"/>
<section className="small">
<SubTitle title="Certificate Trust"/> <SubTitle title="Certificate Trust"/>
<Checkbox <FormSwitch
control={
<Switcher
checked={preferences.allowUntrustedCAs}
onChange={v => preferences.allowUntrustedCAs = v.target.checked}
name="startup"
/>
}
label="Allow untrusted Certificate Authorities" label="Allow untrusted Certificate Authorities"
value={preferences.allowUntrustedCAs}
onChange={v => preferences.allowUntrustedCAs = v}
/> />
<small className="hint"> <small className="hint">
This will make Lens to trust ANY certificate authority without any validations.{" "} This will make Lens to trust ANY certificate authority without any validations.{" "}
@ -106,63 +192,37 @@ export class Preferences extends React.Component {
Does not affect cluster communications! Does not affect cluster communications!
</small> </small>
</section> </section>
<section id="shell">
<h2>Terminal Shell</h2>
<SubTitle title="Shell Path"/>
<Input
theme="round-black"
placeholder={defaultShell}
value={this.shell}
onChange={v => this.shell = v}
onBlur={() => preferences.shell = this.shell}
/>
<small className="hint">
The path of the shell that the terminal uses.
</small>
</section>
<section id="startup">
<h2>Start-up</h2>
<SubTitle title="Automatic Start-up"/>
<Checkbox
label="Automatically start Lens on login"
value={preferences.openAtLogin}
onChange={v => preferences.openAtLogin = v}
/>
</section>
</section> </section>
)}
{this.activeTab == Pages.Kubernetes && (
<section id="kubernetes"> <section id="kubernetes">
<section>
<h1>Kubernetes</h1>
</section>
<section id="kubectl"> <section id="kubectl">
<h2>Kubectl binary</h2> <h2 data-testid="kubernetes-header">Kubernetes</h2>
<KubectlBinaries preferences={preferences}/> <KubectlBinaries preferences={preferences}/>
</section> </section>
<hr/>
<section id="helm"> <section id="helm">
<h2>Helm Charts</h2> <h2>Helm Charts</h2>
<HelmCharts/> <HelmCharts/>
</section> </section>
</section> </section>
)}
{this.activeTab == Pages.Telemetry && (
<section id="telemetry">
<h2 data-testid="telemetry-header">Telemetry</h2>
{telemetryExtensions.map(this.renderExtension)}
</section>
)}
{this.activeTab == Pages.Extensions && (
<section id="extensions"> <section id="extensions">
<section> <h2>Extensions</h2>
<h1>Extensions</h1> {extensions.filter(e => !e.showInPreferencesTab).map(this.renderExtension)}
</section>
{appPreferenceRegistry.getItems().map(({ title, id, components: { Hint, Input } }, index) => {
return (
<section key={index} id={title}>
<h2 id={id}>{title}</h2>
<Input/>
<small className="hint">
<Hint/>
</small>
</section>
);
})}
</section> </section>
)}
</PageLayout> </PageLayout>
)}/>
); );
} }
} }

View File

@ -105,12 +105,6 @@ ol, ul {
list-style: none; list-style: none;
} }
hr {
margin: $margin 0 !important;
height: 1px;
background: $grey-800;
}
h1 { h1 {
color: $textColorPrimary; color: $textColorPrimary;
font-size: 28px; font-size: 28px;

View File

@ -3,6 +3,8 @@
position: relative; position: relative;
overflow: hidden; // required for transition effect on hover overflow: hidden; // required for transition effect on hover
color: white; color: white;
font-family: var(--font-main);
font-weight: var(--font-weight-bold);
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;

View File

@ -96,14 +96,17 @@
} }
label { label {
background: $mainBackground; background: var(--inputControlBackground);
border: 1px solid $borderFaintColor; border: 1px solid var(--inputControlBorder);
border-radius: $radius; border-radius: 4px;
padding: $padding; padding: $padding;
&:hover {
border-color: var(--inputControlHoverBorder);
}
&:focus-within { &:focus-within {
border: 2px solid $colorInfo; border-color: $colorInfo;
padding: $padding - 1;
} }
&:after { &:after {

View File

@ -1,30 +1,22 @@
.PageLayout { .PageLayout {
--width: 60%; --width: 75%;
--nav-width: 180px; --nav-width: 180px;
--nav-column-width: 30vw; --nav-column-width: 30vw;
--spacing: calc(var(--unit) * 2);
--wrapper-padding: calc(var(--spacing) * 2);
--header-height: 64px;
--header-height-mac: 80px;
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: grid !important; display: grid !important;
grid-template-rows: min-content 1fr;
grid-template-columns: 1fr; @include media("<1000px") {
--width: 85%;
}
&.showNavigation { &.showNavigation {
--width: 70%;
grid-template-columns: var(--nav-column-width) 1fr; grid-template-columns: var(--nav-column-width) 1fr;
> .content-wrapper { > .contentRegion {
> .content { justify-content: flex-start;
width: 100%;
padding-left: 1px; // Fix visual content crop
padding-right: calc(var(--nav-column-width) - var(--nav-width));
}
} }
} }
@ -35,96 +27,172 @@
left: 0; left: 0;
top: 0; top: 0;
right: 0; right: 0;
bottom: 24px; bottom: 0;
height: unset; height: unset;
background-color: var(--mainBackground); background-color: var(--settingsBackground);
// adds extra space for traffic-light top buttons (mac only)
.is-mac & > .header {
height: var(--header-height-mac);
padding-top: calc(var(--spacing) * 2);
}
} }
> .header { > .sidebarRegion {
position: sticky;
padding: var(--spacing);
background-color: var(--layoutTabsBackground);
height: var(--header-height);
grid-column-start: 1;
grid-column-end: 4;
}
> .content-navigation {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
overflow-y: auto; overflow-y: auto;
margin-top: 32px; background-color: var(--secondaryBackground);
ul.TreeView { .sidebar {
width: var(--nav-width); width: 218px;
padding-right: 24px; padding: 60px 10px 60px 20px;
.Tabs {
.header {
padding: 6px 10px;
font-size: 13px;
font-weight: 800;
line-height: 16px;
text-transform: uppercase;
&:first-child {
padding-top: 0;
} }
} }
> .content-wrapper { .Tab {
padding: 32px; padding: 6px 10px;
margin-bottom: 2px;
border-radius: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-weight: 500;
font-size: 15px;
line-height: 20px;
cursor: pointer;
color: var(--textColorSecondary);
&::after {
content: none;
}
&:hover {
background-color: var(--navHoverBackground);
color: var(--navHoverColor);
}
&.active {
background-color: var(--navSelectedBackground);
}
}
}
}
}
> .contentRegion {
display: flex;
overflow: auto; overflow: auto;
justify-content: center;
> .content { > .content {
width: var(--width); width: var(--width);
margin: 0 auto; padding: 60px 40px 80px;
> section {
&:last-of-type {
margin-bottom: 80px;
}
} }
} }
p { > .toolsRegion {
line-height: 140%; .fixedTools {
position: fixed;
top: 60px;
.closeBtn {
width: 35px;
height: 35px;
display: grid;
place-items: center;
border: 2px solid var(--textColorDimmed);
border-radius: 50%;
cursor: pointer;
&:hover {
background-color: #72767d4d;
}
&:active {
transform: translateY(1px);
}
.Icon {
color: var(--textColorSecondary);
}
}
.esc {
text-align: center;
margin-top: 4px;
font-weight: 600;
font-size: 14px;
color: var(--textColorDimmed);
pointer-events: none;
}
}
}
} }
a { a {
color: var(--colorInfo); color: var(--colorInfo);
} }
.SubTitle {
text-transform: none;
margin-bottom: 0 !important;
}
.Select {
&__control {
box-shadow: 0 0 0 1px var(--borderFaintColor);
}
}
section { section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: var(--spacing);
> :not(:last-child) { &:not(:first-of-type) {
margin-bottom: var(--spacing); margin-top: 40px;
&.small {
margin-top: 20px;
}
} }
h1, h2 { h1, h2 {
color: var(--textColorAccent); color: var(--textColorAccent);
} text-transform: uppercase;
h1 {
font-size: x-large;
border-bottom: 1px solid var(--borderFaintColor);
padding-bottom: var(--padding);
} }
h2 { h2 {
font-size: large; font-size: 16px;
line-height: 20px;
font-weight: 600;
margin-bottom: 20px;
} }
small.hint { .hint {
margin-top: calc(var(--unit) * -1.5); margin-top: 8px;
font-size: 14px;
} }
.SubTitle { .SubTitle {
margin-top: 0; margin-top: 0;
margin-bottom: 8px;
padding-bottom: 0;
font-size: 12px;
line-height: 1;
}
hr {
margin-top: 40px;
height: 1px;
border-top: thin solid var(--hrColor);
&.small {
margin-top: 20px;
}
&:last-child {
display: none;
}
} }
} }
} }

View File

@ -3,19 +3,18 @@ import "./page-layout.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { autobind, cssNames, IClassName } from "../../utils"; import { autobind, cssNames, IClassName } from "../../utils";
import { Icon } from "../icon";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { NavigationTree, RecursiveTreeView } from "../tree-view"; import { Icon } from "../icon";
export interface PageLayoutProps extends React.DOMAttributes<any> { export interface PageLayoutProps extends React.DOMAttributes<any> {
className?: IClassName; className?: IClassName;
header: React.ReactNode; header?: React.ReactNode;
headerClass?: IClassName; headerClass?: IClassName;
contentClass?: IClassName; contentClass?: IClassName;
provideBackButtonNavigation?: boolean; provideBackButtonNavigation?: boolean;
contentGaps?: boolean; contentGaps?: boolean;
showOnTop?: boolean; // covers whole app view showOnTop?: boolean; // covers whole app view
navigation?: NavigationTree[]; navigation?: React.ReactNode;
back?: (evt: React.MouseEvent | KeyboardEvent) => void; back?: (evt: React.MouseEvent | KeyboardEvent) => void;
} }
@ -58,32 +57,34 @@ export class PageLayout extends React.Component<PageLayoutProps> {
render() { render() {
const { const {
contentClass, header, headerClass, provideBackButtonNavigation, contentClass, headerClass, provideBackButtonNavigation,
contentGaps, showOnTop, navigation, children, ...elemProps contentGaps, showOnTop, navigation, children, ...elemProps
} = this.props; } = this.props;
const className = cssNames("PageLayout", { showOnTop, showNavigation: navigation }, this.props.className); const className = cssNames("PageLayout", { showOnTop, showNavigation: navigation }, this.props.className);
return ( return (
<div {...elemProps} className={className}> <div {...elemProps} className={className}>
<div className={cssNames("header flex gaps align-center", headerClass)}>
{header}
{provideBackButtonNavigation && (
<Icon
big material="close"
className="back box right"
onClick={this.back}
/>
)}
</div>
{ navigation && ( { navigation && (
<nav className="content-navigation"> <nav className="sidebarRegion">
<RecursiveTreeView data={navigation}/> <div className="sidebar">
{navigation}
</div>
</nav> </nav>
)} )}
<div className="content-wrapper" id="ScrollSpyRoot"> <div className="contentRegion" id="ScrollSpyRoot">
<div className={cssNames("content", contentClass, contentGaps && "flex column gaps")}> <div className={cssNames("content", contentClass, contentGaps && "flex column gaps")}>
{children} {children}
</div> </div>
<div className="toolsRegion">
<div className="fixedTools">
<div className="closeBtn" role="button" aria-label="Close" onClick={this.back}>
<Icon material="close"/>
</div>
<div className="esc" aria-hidden="true">
ESC
</div>
</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -6,7 +6,7 @@ html {
--select-menu-bgc: #{$menuBackgroundColor}; --select-menu-bgc: #{$menuBackgroundColor};
--select-menu-border-color: #{$halfGray}; --select-menu-border-color: #{$halfGray};
--select-option-selected-color: #{$selectOptionHoveredColor}; --select-option-selected-color: #{$inputOptionHoverColor};
--select-option-focused-bgc: #{$colorInfo}; --select-option-focused-bgc: #{$colorInfo};
--select-option-focused-color: #{$textColorAccent}; --select-option-focused-color: #{$textColorAccent};
@ -14,6 +14,8 @@ html {
position: relative; position: relative;
min-width: 220px; min-width: 220px;
* { * {
color: inherit; color: inherit;
} }
@ -33,7 +35,7 @@ html {
cursor: pointer; cursor: pointer;
&--is-focused { &--is-focused {
box-shadow: 0 0 0 2px $primary; box-shadow: 0 0 0 1px $primary;
} }
} }
@ -73,8 +75,7 @@ html {
min-width: 100%; min-width: 100%;
&-list { &-list {
padding-right: 1px; padding: 6px;
padding-left: 1px;
width: max-content; width: max-content;
min-width: 100%; min-width: 100%;
} }
@ -183,5 +184,54 @@ html {
} }
} }
} }
&.theme-lens {
:hover {
&.Select__control {
box-shadow: 0 0 0 1px var(--inputControlHoverBorder);
}
}
:focus-within {
&.Select__control {
box-shadow: 0 0 0 1px $colorInfo;
}
}
&.Select__menu {
box-shadow: inset 0 0 0 1px var(--inputControlBorder);
}
.Select {
&__control {
box-shadow: 0 0 0 1px var(--inputControlBorder);
background: var(--inputControlBackground);
}
&__menu {
&-list {
padding: 6px;
}
}
&__option {
border-radius: 4px;
&:active {
background: var(--inputControlBackground);
}
&--is-selected {
background: var(--inputControlBackground);
color: var(--textColorAccent);
}
&--is-focused {
color: var(--textColorPrimary);
background: var(--inputControlBackground);
}
}
}
}
} }
} }

View File

@ -24,7 +24,7 @@ export interface SelectOption<T = any> {
export interface SelectProps<T = any> extends ReactSelectProps<T>, CreatableProps<T> { export interface SelectProps<T = any> extends ReactSelectProps<T>, CreatableProps<T> {
value?: T; value?: T;
themeName?: "dark" | "light" | "outlined"; themeName?: "dark" | "light" | "outlined" | "lens";
menuClass?: string; menuClass?: string;
isCreatable?: boolean; isCreatable?: boolean;
autoConvertOptions?: boolean; // to internal format (i.e. {value: T, label: string}[]), not working with groups autoConvertOptions?: boolean; // to internal format (i.e. {value: T, label: string}[]), not working with groups

View File

@ -0,0 +1,28 @@
import React from "react";
import FormControlLabel, { FormControlLabelProps } from "@material-ui/core/FormControlLabel";
import { makeStyles } from "@material-ui/styles";
const useStyles = makeStyles({
root: {
margin: 0,
"& .MuiTypography-root": {
fontSize: 14,
fontWeight: 500,
flex: 1,
color: "var(--textColorAccent)"
}
},
});
export function FormSwitch(props: FormControlLabelProps) {
const classes = useStyles();
return (
<FormControlLabel
control={props.control}
labelPlacement="start"
label={props.label}
className={classes.root}
/>
);
}

View File

@ -0,0 +1,2 @@
export * from "./switcher";
export * from "./form-switcher";

View File

@ -0,0 +1,68 @@
import React from "react";
import { createStyles, withStyles, Theme } from "@material-ui/core/styles";
import Switch, { SwitchClassKey, SwitchProps } from "@material-ui/core/Switch";
interface Styles extends Partial<Record<SwitchClassKey, string>> {
focusVisible?: string;
}
interface Props extends SwitchProps {
classes: Styles;
}
export const Switcher = withStyles((theme: Theme) =>
createStyles({
root: {
width: 40,
height: 24,
padding: 0,
margin: "0 0 0 8px",
},
switchBase: {
padding: 1,
paddingLeft: 4,
"&$checked": {
transform: "translateX(14px)",
color: "white",
"& + $track": {
backgroundColor: "#52d869",
opacity: 1,
border: "none",
},
},
"&$focusVisible $thumb": {
color: "#52d869",
border: "6px solid #fff",
},
},
thumb: {
width: 18,
height: 18,
marginTop: 2,
boxShadow: "none"
},
track: {
borderRadius: 26 / 2,
backgroundColor: "#72767b",
opacity: 1,
transition: theme.transitions.create(["background-color", "border"]),
},
checked: {},
focusVisible: {},
}),
)(({ classes, ...props }: Props) => {
return (
<Switch
focusVisibleClassName={classes.focusVisible}
disableRipple
classes={{
root: classes.root,
switchBase: classes.switchBase,
thumb: classes.thumb,
track: classes.track,
checked: classes.checked,
}}
{...props}
/>
);
});

View File

@ -9,12 +9,14 @@
"golden": "#ffc63d", "golden": "#ffc63d",
"halfGray": "#87909c80", "halfGray": "#87909c80",
"primary": "#3d90ce", "primary": "#3d90ce",
"textColorPrimary": "#87909c", "textColorPrimary": "#8e9297",
"textColorSecondary": "#a0a0a0", "textColorSecondary": "#a0a0a0",
"textColorAccent": "#ffffff", "textColorAccent": "#ffffff",
"textColorDimmed": "#8e92978c",
"borderColor": "#4c5053", "borderColor": "#4c5053",
"borderFaintColor": "#373a3e", "borderFaintColor": "#373a3e",
"mainBackground": "#1e2124", "mainBackground": "#1e2124",
"secondaryBackground": "#212427",
"contentColor": "#262b2f", "contentColor": "#262b2f",
"layoutBackground": "#2e3136", "layoutBackground": "#2e3136",
"layoutTabsBackground": "#252729", "layoutTabsBackground": "#252729",
@ -112,11 +114,19 @@
"chartStripesColor": "#ffffff08", "chartStripesColor": "#ffffff08",
"chartCapacityColor": "#4c545f", "chartCapacityColor": "#4c545f",
"pieChartDefaultColor": "#30353a", "pieChartDefaultColor": "#30353a",
"selectOptionHoveredColor": "#87909c", "inputOptionHoverColor": "#87909c",
"inputControlBackground": "#00000021",
"inputControlBorder": "#202225bf",
"inputControlHoverBorder": "#07080880",
"lineProgressBackground": "#414448", "lineProgressBackground": "#414448",
"radioActiveBackground": "#36393e", "radioActiveBackground": "#36393e",
"menuActiveBackground": "#36393e", "menuActiveBackground": "#36393e",
"menuSelectedOptionBgc": "#36393e", "menuSelectedOptionBgc": "#36393e",
"scrollBarColor": "#5f6064" "scrollBarColor": "#5f6064",
"settingsBackground": "#2b3035",
"navSelectedBackground": "#4f545c52",
"navHoverBackground": "#4f545c29",
"navHoverColor": "#dcddde",
"hrColor": "#ffffff0f"
} }
} }

View File

@ -12,9 +12,11 @@
"textColorPrimary": "#555555", "textColorPrimary": "#555555",
"textColorSecondary": "#51575d", "textColorSecondary": "#51575d",
"textColorAccent": "#333333", "textColorAccent": "#333333",
"textColorDimmed": "#5557598c",
"borderColor": "#c9cfd3", "borderColor": "#c9cfd3",
"borderFaintColor": "#dfdfdf", "borderFaintColor": "#dfdfdf",
"mainBackground": "#f1f1f1", "mainBackground": "#f1f1f1",
"secondaryBackground": "#f2f3f5",
"contentColor": "#ffffff", "contentColor": "#ffffff",
"layoutBackground": "#e8e8e8", "layoutBackground": "#e8e8e8",
"layoutTabsBackground": "#f8f8f8", "layoutTabsBackground": "#f8f8f8",
@ -113,12 +115,20 @@
"chartStripesColor": "#00000009", "chartStripesColor": "#00000009",
"chartCapacityColor": "#cccccc", "chartCapacityColor": "#cccccc",
"pieChartDefaultColor": "#efefef", "pieChartDefaultColor": "#efefef",
"selectOptionHoveredColor": "#ffffff", "inputOptionHoverColor": "#ffffff",
"inputControlBackground": "#f6f6f7",
"inputControlBorder": "#cccdcf",
"inputControlHoverBorder": "#b9bbbe",
"lineProgressBackground": "#e8e8e8", "lineProgressBackground": "#e8e8e8",
"radioActiveBackground": "#f1f1f1", "radioActiveBackground": "#f1f1f1",
"menuActiveBackground": "#e8e8e8", "menuActiveBackground": "#e8e8e8",
"menuSelectedOptionBgc": "#e8e8e8", "menuSelectedOptionBgc": "#e8e8e8",
"scrollBarColor": "#bbbbbb", "scrollBarColor": "#bbbbbb",
"canvasBackground": "#24292e" "canvasBackground": "#24292e",
"settingsBackground": "#ffffff",
"navSelectedBackground": "#747f8d3d",
"navHoverBackground": "#747f8d14",
"navHoverColor": "#2e3135",
"hrColor": "#06060714"
} }
} }

View File

@ -128,7 +128,7 @@ $iconActiveColor: var(--iconActiveColor);
$iconActiveBackground: var(--iconActiveBackground); $iconActiveBackground: var(--iconActiveBackground);
$filterAreaBackground: var(--filterAreaBackground); $filterAreaBackground: var(--filterAreaBackground);
$selectOptionHoveredColor: var(--selectOptionHoveredColor); $inputOptionHoverColor: var(--inputOptionHoverColor);
$lineProgressBackground: var(--lineProgressBackground); $lineProgressBackground: var(--lineProgressBackground);
$radioActiveBackground: var(--radioActiveBackground); $radioActiveBackground: var(--radioActiveBackground);
$menuActiveBackground: var(--menuActiveBackground); $menuActiveBackground: var(--menuActiveBackground);