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 = [
{
title: "In-App Surveys",
showInPreferencesTab: "telemetry",
components: {
Hint: () => <SurveyPreferenceHint/>,
Input: () => <SurveyPreferenceInput survey={surveyPreferencesStore}/>

View File

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

View File

@ -36,7 +36,7 @@ describe("Lens integration tests", () => {
it('shows "add cluster"', async () => {
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", () => {
@ -44,7 +44,17 @@ describe("Lens integration tests", () => {
const appName: string = process.platform === "darwin" ? "Lens" : "File";
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 () => {
@ -54,7 +64,8 @@ describe("Lens integration tests", () => {
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.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) {
await app.client.waitUntilTextExists("h1", "What's new?");
await app.client.click("button.primary");
await app.client.waitUntilTextExists("h2", "default");
await app.client.waitUntilTextExists("h5", "Clusters");
}
export async function clickWelcomeNotification(app: Application) {
@ -89,7 +89,7 @@ export async function clickWelcomeNotification(app: Application) {
if (itemsText === "0 item") {
// welcome notification should be present, dismiss it
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 {
title: string;
id?: string;
showInPreferencesTab?: string;
components: AppPreferenceComponents;
}

View File

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

View File

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

View File

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

View File

@ -6,22 +6,32 @@ import { disposeOnUnmount, observer } from "mobx-react";
import { userStore } from "../../../common/user-store";
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 { Checkbox } from "../checkbox";
import { Input } from "../input";
import { PageLayout } from "../layout/page-layout";
import { SubTitle } from "../layout/sub-title";
import { Select, SelectOption } from "../select";
import { HelmCharts } from "./helm-charts";
import { KubectlBinaries } from "./kubectl-binaries";
import { ScrollSpy } from "../scroll-spy/scroll-spy";
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
export class Preferences extends React.Component {
@observable httpProxy = userStore.preferences.httpsProxy || "";
@observable shell = userStore.preferences.shell || "";
@observable activeTab = Pages.Application;
@computed get themeOptions(): SelectOption<string>[] {
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() {
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;
if (!defaultShell) {
@ -59,29 +106,59 @@ export class Preferences extends React.Component {
}
return (
<ScrollSpy htmlFor="ScrollSpyRoot" render={navigation => (
<PageLayout
showOnTop
navigation={navigation}
navigation={this.renderNavigation()}
className="Preferences"
contentGaps={false}
header={header}
>
<section id="application" title="Application">
<section>
<h1>Application</h1>
</section>
{this.activeTab == Pages.Application && (
<section id="application">
<h2 data-testid="application-header">Application</h2>
<section id="appearance">
<h2>Appearance</h2>
<SubTitle title="Theme"/>
<Select
options={this.themeOptions}
value={preferences.colorTheme}
onChange={({ value }: SelectOption) => preferences.colorTheme = value}
themeName="lens"
/>
</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">
<h2>Proxy</h2>
<section>
<h2 data-testid="proxy-header">Proxy</h2>
<SubTitle title="HTTP Proxy"/>
<Input
theme="round-black"
@ -93,12 +170,21 @@ export class Preferences extends React.Component {
<small className="hint">
Proxy is used only for non-cluster communication.
</small>
</section>
<hr className="small"/>
<section className="small">
<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"
value={preferences.allowUntrustedCAs}
onChange={v => preferences.allowUntrustedCAs = v}
/>
<small className="hint">
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!
</small>
</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>
)}
{this.activeTab == Pages.Kubernetes && (
<section id="kubernetes">
<section>
<h1>Kubernetes</h1>
</section>
<section id="kubectl">
<h2>Kubectl binary</h2>
<h2 data-testid="kubernetes-header">Kubernetes</h2>
<KubectlBinaries preferences={preferences}/>
</section>
<hr/>
<section id="helm">
<h2>Helm Charts</h2>
<HelmCharts/>
</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>
<h1>Extensions</h1>
</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>
);
})}
<h2>Extensions</h2>
{extensions.filter(e => !e.showInPreferencesTab).map(this.renderExtension)}
</section>
)}
</PageLayout>
)}/>
);
}
}

View File

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

View File

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

View File

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

View File

@ -1,30 +1,22 @@
.PageLayout {
--width: 60%;
--width: 75%;
--nav-width: 180px;
--nav-column-width: 30vw;
--spacing: calc(var(--unit) * 2);
--wrapper-padding: calc(var(--spacing) * 2);
--header-height: 64px;
--header-height-mac: 80px;
position: relative;
width: 100%;
height: 100%;
display: grid !important;
grid-template-rows: min-content 1fr;
grid-template-columns: 1fr;
@include media("<1000px") {
--width: 85%;
}
&.showNavigation {
--width: 70%;
grid-template-columns: var(--nav-column-width) 1fr;
> .content-wrapper {
> .content {
width: 100%;
padding-left: 1px; // Fix visual content crop
padding-right: calc(var(--nav-column-width) - var(--nav-width));
}
> .contentRegion {
justify-content: flex-start;
}
}
@ -35,96 +27,172 @@
left: 0;
top: 0;
right: 0;
bottom: 24px;
bottom: 0;
height: unset;
background-color: var(--mainBackground);
// adds extra space for traffic-light top buttons (mac only)
.is-mac & > .header {
height: var(--header-height-mac);
padding-top: calc(var(--spacing) * 2);
}
background-color: var(--settingsBackground);
}
> .header {
position: sticky;
padding: var(--spacing);
background-color: var(--layoutTabsBackground);
height: var(--header-height);
grid-column-start: 1;
grid-column-end: 4;
}
> .content-navigation {
> .sidebarRegion {
display: flex;
justify-content: flex-end;
overflow-y: auto;
margin-top: 32px;
background-color: var(--secondaryBackground);
ul.TreeView {
width: var(--nav-width);
padding-right: 24px;
.sidebar {
width: 218px;
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 {
padding: 32px;
.Tab {
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;
justify-content: center;
> .content {
width: var(--width);
margin: 0 auto;
padding: 60px 40px 80px;
> section {
&:last-of-type {
margin-bottom: 80px;
}
}
}
p {
line-height: 140%;
> .toolsRegion {
.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 {
color: var(--colorInfo);
}
.SubTitle {
text-transform: none;
margin-bottom: 0 !important;
}
.Select {
&__control {
box-shadow: 0 0 0 1px var(--borderFaintColor);
}
}
section {
display: flex;
flex-direction: column;
margin-bottom: var(--spacing);
> :not(:last-child) {
margin-bottom: var(--spacing);
&:not(:first-of-type) {
margin-top: 40px;
&.small {
margin-top: 20px;
}
}
h1, h2 {
color: var(--textColorAccent);
}
h1 {
font-size: x-large;
border-bottom: 1px solid var(--borderFaintColor);
padding-bottom: var(--padding);
text-transform: uppercase;
}
h2 {
font-size: large;
font-size: 16px;
line-height: 20px;
font-weight: 600;
margin-bottom: 20px;
}
small.hint {
margin-top: calc(var(--unit) * -1.5);
.hint {
margin-top: 8px;
font-size: 14px;
}
.SubTitle {
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 { observer } from "mobx-react";
import { autobind, cssNames, IClassName } from "../../utils";
import { Icon } from "../icon";
import { navigation } from "../../navigation";
import { NavigationTree, RecursiveTreeView } from "../tree-view";
import { Icon } from "../icon";
export interface PageLayoutProps extends React.DOMAttributes<any> {
className?: IClassName;
header: React.ReactNode;
header?: React.ReactNode;
headerClass?: IClassName;
contentClass?: IClassName;
provideBackButtonNavigation?: boolean;
contentGaps?: boolean;
showOnTop?: boolean; // covers whole app view
navigation?: NavigationTree[];
navigation?: React.ReactNode;
back?: (evt: React.MouseEvent | KeyboardEvent) => void;
}
@ -58,32 +57,34 @@ export class PageLayout extends React.Component<PageLayoutProps> {
render() {
const {
contentClass, header, headerClass, provideBackButtonNavigation,
contentClass, headerClass, provideBackButtonNavigation,
contentGaps, showOnTop, navigation, children, ...elemProps
} = this.props;
const className = cssNames("PageLayout", { showOnTop, showNavigation: navigation }, this.props.className);
return (
<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 && (
<nav className="content-navigation">
<RecursiveTreeView data={navigation}/>
<nav className="sidebarRegion">
<div className="sidebar">
{navigation}
</div>
</nav>
)}
<div className="content-wrapper" id="ScrollSpyRoot">
<div className="contentRegion" id="ScrollSpyRoot">
<div className={cssNames("content", contentClass, contentGaps && "flex column gaps")}>
{children}
</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>
);

View File

@ -6,7 +6,7 @@ html {
--select-menu-bgc: #{$menuBackgroundColor};
--select-menu-border-color: #{$halfGray};
--select-option-selected-color: #{$selectOptionHoveredColor};
--select-option-selected-color: #{$inputOptionHoverColor};
--select-option-focused-bgc: #{$colorInfo};
--select-option-focused-color: #{$textColorAccent};
@ -14,6 +14,8 @@ html {
position: relative;
min-width: 220px;
* {
color: inherit;
}
@ -33,7 +35,7 @@ html {
cursor: pointer;
&--is-focused {
box-shadow: 0 0 0 2px $primary;
box-shadow: 0 0 0 1px $primary;
}
}
@ -73,8 +75,7 @@ html {
min-width: 100%;
&-list {
padding-right: 1px;
padding-left: 1px;
padding: 6px;
width: max-content;
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> {
value?: T;
themeName?: "dark" | "light" | "outlined";
themeName?: "dark" | "light" | "outlined" | "lens";
menuClass?: string;
isCreatable?: boolean;
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",
"halfGray": "#87909c80",
"primary": "#3d90ce",
"textColorPrimary": "#87909c",
"textColorPrimary": "#8e9297",
"textColorSecondary": "#a0a0a0",
"textColorAccent": "#ffffff",
"textColorDimmed": "#8e92978c",
"borderColor": "#4c5053",
"borderFaintColor": "#373a3e",
"mainBackground": "#1e2124",
"secondaryBackground": "#212427",
"contentColor": "#262b2f",
"layoutBackground": "#2e3136",
"layoutTabsBackground": "#252729",
@ -112,11 +114,19 @@
"chartStripesColor": "#ffffff08",
"chartCapacityColor": "#4c545f",
"pieChartDefaultColor": "#30353a",
"selectOptionHoveredColor": "#87909c",
"inputOptionHoverColor": "#87909c",
"inputControlBackground": "#00000021",
"inputControlBorder": "#202225bf",
"inputControlHoverBorder": "#07080880",
"lineProgressBackground": "#414448",
"radioActiveBackground": "#36393e",
"menuActiveBackground": "#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",
"textColorSecondary": "#51575d",
"textColorAccent": "#333333",
"textColorDimmed": "#5557598c",
"borderColor": "#c9cfd3",
"borderFaintColor": "#dfdfdf",
"mainBackground": "#f1f1f1",
"secondaryBackground": "#f2f3f5",
"contentColor": "#ffffff",
"layoutBackground": "#e8e8e8",
"layoutTabsBackground": "#f8f8f8",
@ -113,12 +115,20 @@
"chartStripesColor": "#00000009",
"chartCapacityColor": "#cccccc",
"pieChartDefaultColor": "#efefef",
"selectOptionHoveredColor": "#ffffff",
"inputOptionHoverColor": "#ffffff",
"inputControlBackground": "#f6f6f7",
"inputControlBorder": "#cccdcf",
"inputControlHoverBorder": "#b9bbbe",
"lineProgressBackground": "#e8e8e8",
"radioActiveBackground": "#f1f1f1",
"menuActiveBackground": "#e8e8e8",
"menuSelectedOptionBgc": "#e8e8e8",
"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);
$filterAreaBackground: var(--filterAreaBackground);
$selectOptionHoveredColor: var(--selectOptionHoveredColor);
$inputOptionHoverColor: var(--inputOptionHoverColor);
$lineProgressBackground: var(--lineProgressBackground);
$radioActiveBackground: var(--radioActiveBackground);
$menuActiveBackground: var(--menuActiveBackground);