[FEATURE] Add theme support (#1584)
* [FEATURE] Add theme support This change allows users to select a theme for Authelia on start-up. The default will continue to be the existing theme which is known as `light`. Three new options are now also provided: * `dark` * `grey` * `custom` The `custom` theme allows users to specify a primary and secondary hex color code to be utilised to style the portal. Co-authored-by: BankaiNoJutsu <lbegert@gmail.com> * Add themes to integration tests * Remove custom theme * Fix linting issue in access_control_test.go Co-authored-by: BankaiNoJutsu <lbegert@gmail.com>pull/1620/head
parent
b74e65fc48
commit
daa30f3aa3
|
@ -8,6 +8,9 @@ port: 9091
|
||||||
# tls_key: /config/ssl/key.pem
|
# tls_key: /config/ssl/key.pem
|
||||||
# tls_cert: /config/ssl/cert.pem
|
# tls_cert: /config/ssl/cert.pem
|
||||||
|
|
||||||
|
# The theme to display: light, dark, grey
|
||||||
|
theme: light
|
||||||
|
|
||||||
# Configuration options specific to the internal http server
|
# Configuration options specific to the internal http server
|
||||||
server:
|
server:
|
||||||
# Buffers usually should be configured to be the same value.
|
# Buffers usually should be configured to be the same value.
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Theme
|
||||||
|
parent: Configuration
|
||||||
|
nav_order: 11
|
||||||
|
---
|
||||||
|
|
||||||
|
# Theme
|
||||||
|
|
||||||
|
The theme section configures the theme and style Authelia uses.
|
||||||
|
|
||||||
|
There are currently 3 available themes for Authelia:
|
||||||
|
* light (default)
|
||||||
|
* dark
|
||||||
|
* grey
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# The theme to display: light, dark, grey
|
||||||
|
theme: light
|
||||||
|
```
|
|
@ -4,6 +4,7 @@ package schema
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Host string `mapstructure:"host"`
|
Host string `mapstructure:"host"`
|
||||||
Port int `mapstructure:"port"`
|
Port int `mapstructure:"port"`
|
||||||
|
Theme string `mapstructure:"theme"`
|
||||||
TLSCert string `mapstructure:"tls_cert"`
|
TLSCert string `mapstructure:"tls_cert"`
|
||||||
TLSKey string `mapstructure:"tls_key"`
|
TLSKey string `mapstructure:"tls_key"`
|
||||||
CertificatesDirectory string `mapstructure:"certificates_directory"`
|
CertificatesDirectory string `mapstructure:"certificates_directory"`
|
||||||
|
|
|
@ -29,7 +29,7 @@ func (suite *AccessControl) TestShouldValidateCompleteConfiguration() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() {
|
func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() {
|
||||||
suite.configuration.DefaultPolicy = "invalid"
|
suite.configuration.DefaultPolicy = testInvalidPolicy
|
||||||
|
|
||||||
ValidateAccessControl(suite.configuration, suite.validator)
|
ValidateAccessControl(suite.configuration, suite.validator)
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidPolicy() {
|
||||||
suite.configuration.Rules = []schema.ACLRule{
|
suite.configuration.Rules = []schema.ACLRule{
|
||||||
{
|
{
|
||||||
Domains: []string{"public.example.com"},
|
Domains: []string{"public.example.com"},
|
||||||
Policy: "invalid",
|
Policy: testInvalidPolicy,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,12 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if configuration.Theme == "" {
|
||||||
|
configuration.Theme = "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateTheme(configuration, validator)
|
||||||
|
|
||||||
if configuration.TOTP == nil {
|
if configuration.TOTP == nil {
|
||||||
configuration.TOTP = &schema.DefaultTOTPConfiguration
|
configuration.TOTP = &schema.DefaultTOTPConfiguration
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ var validKeys = []string{
|
||||||
"log_file_path",
|
"log_file_path",
|
||||||
"default_redirection_url",
|
"default_redirection_url",
|
||||||
"jwt_secret",
|
"jwt_secret",
|
||||||
|
"theme",
|
||||||
"tls_key",
|
"tls_key",
|
||||||
"tls_cert",
|
"tls_cert",
|
||||||
"certificates_directory",
|
"certificates_directory",
|
||||||
|
@ -177,6 +178,7 @@ const schemeLDAP = "ldap"
|
||||||
const schemeLDAPS = "ldaps"
|
const schemeLDAPS = "ldaps"
|
||||||
|
|
||||||
const testBadTimer = "-1"
|
const testBadTimer = "-1"
|
||||||
|
const testInvalidPolicy = "invalid"
|
||||||
const testJWTSecret = "a_secret"
|
const testJWTSecret = "a_secret"
|
||||||
const testLDAPBaseDN = "base_dn"
|
const testLDAPBaseDN = "base_dn"
|
||||||
const testLDAPPassword = "password"
|
const testLDAPPassword = "password"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateTheme validates and update Theme configuration.
|
||||||
|
func ValidateTheme(configuration *schema.Configuration, validator *schema.StructValidator) {
|
||||||
|
validThemes := regexp.MustCompile("light|dark|grey")
|
||||||
|
if !validThemes.MatchString(configuration.Theme) {
|
||||||
|
validator.Push(fmt.Errorf("Theme: %s is not valid, valid themes are: \"light\", \"dark\" or \"grey\"", configuration.Theme))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/internal/configuration/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Theme struct {
|
||||||
|
suite.Suite
|
||||||
|
configuration *schema.Configuration
|
||||||
|
validator *schema.StructValidator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *Theme) SetupTest() {
|
||||||
|
suite.validator = schema.NewStructValidator()
|
||||||
|
suite.configuration = &schema.Configuration{
|
||||||
|
Theme: "light",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *Theme) TestShouldValidateCompleteConfiguration() {
|
||||||
|
ValidateTheme(suite.configuration, suite.validator)
|
||||||
|
|
||||||
|
suite.Assert().False(suite.validator.HasWarnings())
|
||||||
|
suite.Assert().False(suite.validator.HasErrors())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() {
|
||||||
|
suite.configuration.Theme = "invalid"
|
||||||
|
|
||||||
|
ValidateTheme(suite.configuration, suite.validator)
|
||||||
|
|
||||||
|
suite.Assert().False(suite.validator.HasWarnings())
|
||||||
|
suite.Require().Len(suite.validator.Errors(), 1)
|
||||||
|
|
||||||
|
suite.Assert().EqualError(suite.validator.Errors()[0], "Theme: invalid is not valid, valid themes are: \"light\", \"dark\" or \"grey\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThemes(t *testing.T) {
|
||||||
|
suite.Run(t, new(Theme))
|
||||||
|
}
|
|
@ -33,9 +33,9 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
|
||||||
|
|
||||||
rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"}
|
rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"}
|
||||||
|
|
||||||
serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword)
|
serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.Path, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme)
|
||||||
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword)
|
serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.Path, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme)
|
||||||
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword)
|
serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.Path, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme)
|
||||||
|
|
||||||
r := router.New()
|
r := router.New()
|
||||||
r.GET("/", serveIndexHandler)
|
r.GET("/", serveIndexHandler)
|
||||||
|
|
|
@ -19,7 +19,7 @@ var alphaNumericRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV
|
||||||
// this is utilised to pass information between the backend and frontend
|
// this is utilised to pass information between the backend and frontend
|
||||||
// and generate a nonce to support a restrictive CSP while using material-ui.
|
// and generate a nonce to support a restrictive CSP while using material-ui.
|
||||||
//go:generate broccoli -src ../../public_html -o public_html
|
//go:generate broccoli -src ../../public_html -o public_html
|
||||||
func ServeTemplatedFile(publicDir, file, base, session, rememberMe, resetPassword string) fasthttp.RequestHandler {
|
func ServeTemplatedFile(publicDir, file, base, rememberMe, resetPassword, session, theme string) fasthttp.RequestHandler {
|
||||||
logger := logging.Logger()
|
logger := logging.Logger()
|
||||||
|
|
||||||
f, err := br.Open(publicDir + file)
|
f, err := br.Open(publicDir + file)
|
||||||
|
@ -56,7 +56,7 @@ func ServeTemplatedFile(publicDir, file, base, session, rememberMe, resetPasswor
|
||||||
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", nonce))
|
ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", nonce))
|
||||||
}
|
}
|
||||||
|
|
||||||
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, CSPNonce, Session, RememberMe, ResetPassword string }{Base: base, CSPNonce: nonce, Session: session, RememberMe: rememberMe, ResetPassword: resetPassword})
|
err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, CSPNonce, RememberMe, ResetPassword, Session, Theme string }{Base: base, CSPNonce: nonce, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error("An error occurred", 503)
|
ctx.Error("An error occurred", 503)
|
||||||
logger.Errorf("Unable to execute template: %v", err)
|
logger.Errorf("Unable to execute template: %v", err)
|
||||||
|
|
|
@ -6,6 +6,8 @@ port: 9091
|
||||||
tls_cert: /config/ssl/cert.pem
|
tls_cert: /config/ssl/cert.pem
|
||||||
tls_key: /config/ssl/key.pem
|
tls_key: /config/ssl/key.pem
|
||||||
|
|
||||||
|
theme: grey
|
||||||
|
|
||||||
log_level: debug
|
log_level: debug
|
||||||
|
|
||||||
default_redirection_url: https://home.example.com:8080/
|
default_redirection_url: https://home.example.com:8080/
|
||||||
|
|
|
@ -6,6 +6,8 @@ port: 9091
|
||||||
tls_cert: /config/ssl/cert.pem
|
tls_cert: /config/ssl/cert.pem
|
||||||
tls_key: /config/ssl/key.pem
|
tls_key: /config/ssl/key.pem
|
||||||
|
|
||||||
|
theme: dark
|
||||||
|
|
||||||
log_level: debug
|
log_level: debug
|
||||||
|
|
||||||
default_redirection_url: https://home.example.com:8080/
|
default_redirection_url: https://home.example.com:8080/
|
||||||
|
|
|
@ -2,3 +2,4 @@ HOST=authelia-frontend
|
||||||
PUBLIC_URL=""
|
PUBLIC_URL=""
|
||||||
REACT_APP_REMEMBER_ME=true
|
REACT_APP_REMEMBER_ME=true
|
||||||
REACT_APP_RESET_PASSWORD=true
|
REACT_APP_RESET_PASSWORD=true
|
||||||
|
REACT_APP_THEME=light
|
|
@ -1,3 +1,4 @@
|
||||||
PUBLIC_URL={{.Base}}
|
PUBLIC_URL={{.Base}}
|
||||||
REACT_APP_REMEMBER_ME={{.RememberMe}}
|
REACT_APP_REMEMBER_ME={{.RememberMe}}
|
||||||
REACT_APP_RESET_PASSWORD={{.ResetPassword}}
|
REACT_APP_RESET_PASSWORD={{.ResetPassword}}
|
||||||
|
REACT_APP_THEME={{.Theme}}
|
|
@ -25,7 +25,7 @@
|
||||||
<title>Login - Authelia</title>
|
<title>Login - Authelia</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-basepath="%PUBLIC_URL%" data-rememberme="%REACT_APP_REMEMBER_ME%" data-resetpassword="%REACT_APP_RESET_PASSWORD%">
|
<body data-basepath="%PUBLIC_URL%" data-rememberme="%REACT_APP_REMEMBER_ME%" data-resetpassword="%REACT_APP_RESET_PASSWORD%" data-theme="%REACT_APP_THEME%">
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
<!--
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { config as faConfig } from "@fortawesome/fontawesome-svg-core";
|
import { config as faConfig } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import { CssBaseline, ThemeProvider } from "@material-ui/core";
|
||||||
import { BrowserRouter as Router, Route, Switch, Redirect } from "react-router-dom";
|
import { BrowserRouter as Router, Route, Switch, Redirect } from "react-router-dom";
|
||||||
|
|
||||||
import NotificationBar from "./components/NotificationBar";
|
import NotificationBar from "./components/NotificationBar";
|
||||||
|
@ -14,8 +15,9 @@ import {
|
||||||
RegisterOneTimePasswordRoute,
|
RegisterOneTimePasswordRoute,
|
||||||
LogoutRoute,
|
LogoutRoute,
|
||||||
} from "./Routes";
|
} from "./Routes";
|
||||||
|
import * as themes from "./themes";
|
||||||
import { getBasePath } from "./utils/BasePath";
|
import { getBasePath } from "./utils/BasePath";
|
||||||
import { getRememberMe, getResetPassword } from "./utils/Configuration";
|
import { getRememberMe, getResetPassword, getTheme } from "./utils/Configuration";
|
||||||
import RegisterOneTimePassword from "./views/DeviceRegistration/RegisterOneTimePassword";
|
import RegisterOneTimePassword from "./views/DeviceRegistration/RegisterOneTimePassword";
|
||||||
import RegisterSecurityKey from "./views/DeviceRegistration/RegisterSecurityKey";
|
import RegisterSecurityKey from "./views/DeviceRegistration/RegisterSecurityKey";
|
||||||
import LoginPortal from "./views/LoginPortal/LoginPortal";
|
import LoginPortal from "./views/LoginPortal/LoginPortal";
|
||||||
|
@ -27,10 +29,23 @@ import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||||
|
|
||||||
faConfig.autoAddCss = false;
|
faConfig.autoAddCss = false;
|
||||||
|
|
||||||
|
function Theme() {
|
||||||
|
switch (getTheme()) {
|
||||||
|
case "dark":
|
||||||
|
return themes.Dark;
|
||||||
|
case "grey":
|
||||||
|
return themes.Grey;
|
||||||
|
default:
|
||||||
|
return themes.Light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [notification, setNotification] = useState(null as Notification | null);
|
const [notification, setNotification] = useState(null as Notification | null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider theme={Theme()}>
|
||||||
|
<CssBaseline />
|
||||||
<NotificationsContext.Provider value={{ notification, setNotification }}>
|
<NotificationsContext.Provider value={{ notification, setNotification }}>
|
||||||
<Router basename={getBasePath()}>
|
<Router basename={getBasePath()}>
|
||||||
<NotificationBar onClose={() => setNotification(null)} />
|
<NotificationBar onClose={() => setNotification(null)} />
|
||||||
|
@ -59,6 +74,7 @@ const App: React.FC = () => {
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
</NotificationsContext.Provider>
|
</NotificationsContext.Provider>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
<svg version="1.1" id="UserSvg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
viewBox="0 0 55 55" style="enable-background:new 0 0 55 55;" xml:space="preserve">
|
viewBox="0 0 55 55" style="enable-background:new 0 0 55 55;" xml:space="preserve">
|
||||||
<path d="M55,27.5C55,12.337,42.663,0,27.5,0S0,12.337,0,27.5c0,8.009,3.444,15.228,8.926,20.258l-0.026,0.023l0.892,0.752
|
<path d="M55,27.5C55,12.337,42.663,0,27.5,0S0,12.337,0,27.5c0,8.009,3.444,15.228,8.926,20.258l-0.026,0.023l0.892,0.752
|
||||||
c0.058,0.049,0.121,0.089,0.179,0.137c0.474,0.393,0.965,0.766,1.465,1.127c0.162,0.117,0.324,0.234,0.489,0.348
|
c0.058,0.049,0.121,0.089,0.179,0.137c0.474,0.393,0.965,0.766,1.465,1.127c0.162,0.117,0.324,0.234,0.489,0.348
|
||||||
|
@ -18,34 +18,4 @@
|
||||||
s-7.024,1.065-8.867,3.168c-2.119,2.416-1.935,5.346-1.883,5.864v4.667c-0.568,0.661-0.887,1.502-0.887,2.369v3.545
|
s-7.024,1.065-8.867,3.168c-2.119,2.416-1.935,5.346-1.883,5.864v4.667c-0.568,0.661-0.887,1.502-0.887,2.369v3.545
|
||||||
c0,1.101,0.494,2.128,1.34,2.821c0.81,3.173,2.477,5.575,3.093,6.389v2.894c0,0.816-0.445,1.566-1.162,1.958l-7.907,4.313
|
c0,1.101,0.494,2.128,1.34,2.821c0.81,3.173,2.477,5.575,3.093,6.389v2.894c0,0.816-0.445,1.566-1.162,1.958l-7.907,4.313
|
||||||
c-0.252,0.137-0.502,0.297-0.752,0.476C5.276,41.792,2,35.022,2,27.5z"/>
|
c-0.252,0.137-0.502,0.297-0.752,0.476C5.276,41.792,2,35.022,2,27.5z"/>
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
@ -1,4 +1,5 @@
|
||||||
import "./utils/AssetPath";
|
import "./utils/AssetPath";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
|
|
@ -52,7 +52,6 @@ const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
minHeight: "90vh",
|
minHeight: "90vh",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
// marginTop: theme.spacing(10),
|
|
||||||
},
|
},
|
||||||
rootContainer: {
|
rootContainer: {
|
||||||
paddingLeft: 32,
|
paddingLeft: 32,
|
||||||
|
@ -62,6 +61,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
icon: {
|
icon: {
|
||||||
margin: theme.spacing(),
|
margin: theme.spacing(),
|
||||||
width: "64px",
|
width: "64px",
|
||||||
|
fill: theme.custom.icon,
|
||||||
},
|
},
|
||||||
body: {},
|
body: {},
|
||||||
poweredBy: {
|
poweredBy: {
|
||||||
|
|
|
@ -3,4 +3,5 @@ import Adapter from "enzyme-adapter-react-16";
|
||||||
document.body.setAttribute("data-basepath", "");
|
document.body.setAttribute("data-basepath", "");
|
||||||
document.body.setAttribute("data-rememberme", "true");
|
document.body.setAttribute("data-rememberme", "true");
|
||||||
document.body.setAttribute("data-resetpassword", "true");
|
document.body.setAttribute("data-resetpassword", "true");
|
||||||
|
document.body.setAttribute("data-theme", "light");
|
||||||
configure({ adapter: new Adapter() });
|
configure({ adapter: new Adapter() });
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { createMuiTheme } from "@material-ui/core/styles";
|
||||||
|
|
||||||
|
const Dark = createMuiTheme({
|
||||||
|
custom: {
|
||||||
|
icon: "#fff",
|
||||||
|
loadingBar: "#fff",
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
type: "dark",
|
||||||
|
primary: {
|
||||||
|
main: "#1976d2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Dark;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { createMuiTheme } from "@material-ui/core/styles";
|
||||||
|
|
||||||
|
const Grey = createMuiTheme({
|
||||||
|
custom: {
|
||||||
|
icon: "#929aa5",
|
||||||
|
loadingBar: "#929aa5",
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: "#929aa5",
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: "#2f343e",
|
||||||
|
paper: "#2f343e",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: {
|
||||||
|
MuiCssBaseline: {
|
||||||
|
"@global": {
|
||||||
|
body: {
|
||||||
|
backgroundColor: "#2f343e",
|
||||||
|
color: "#929aa5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiOutlinedInput: {
|
||||||
|
root: {
|
||||||
|
"& $notchedOutline": {
|
||||||
|
borderColor: "#929aa5",
|
||||||
|
},
|
||||||
|
"&:hover:not($disabled):not($focused):not($error) $notchedOutline": {
|
||||||
|
borderColor: "#929aa5",
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
"&$focused $notchedOutline": {
|
||||||
|
borderColor: "#929aa5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notchedOutline: {},
|
||||||
|
},
|
||||||
|
MuiCheckbox: {
|
||||||
|
root: {
|
||||||
|
color: "#929aa5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiInputBase: {
|
||||||
|
input: {
|
||||||
|
color: "#929aa5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiInputLabel: {
|
||||||
|
root: {
|
||||||
|
color: "#929aa5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Grey;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { createMuiTheme } from "@material-ui/core/styles";
|
||||||
|
|
||||||
|
const Light = createMuiTheme({
|
||||||
|
custom: {
|
||||||
|
icon: "#000",
|
||||||
|
loadingBar: "#000",
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: "#1976d2",
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: "#fff",
|
||||||
|
paper: "#fff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Light;
|
|
@ -0,0 +1,18 @@
|
||||||
|
declare module "@material-ui/core/styles/createMuiTheme" {
|
||||||
|
interface Theme {
|
||||||
|
custom: {
|
||||||
|
icon: React.CSSProperties["color"];
|
||||||
|
loadingBar: React.CSSProperties["color"];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface ThemeOptions {
|
||||||
|
custom: {
|
||||||
|
icon: React.CSSProperties["color"];
|
||||||
|
loadingBar: React.CSSProperties["color"];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { default as Light } from "./Light";
|
||||||
|
export { default as Dark } from "./Dark";
|
||||||
|
export { default as Grey } from "./Grey";
|
|
@ -14,3 +14,7 @@ export function getRememberMe() {
|
||||||
export function getResetPassword() {
|
export function getResetPassword() {
|
||||||
return getEmbeddedVariable("resetpassword") === "true";
|
return getEmbeddedVariable("resetpassword") === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTheme() {
|
||||||
|
return getEmbeddedVariable("theme");
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Typography, Grid } from "@material-ui/core";
|
import { useTheme, Typography, Grid } from "@material-ui/core";
|
||||||
import ReactLoading from "react-loading";
|
import ReactLoading from "react-loading";
|
||||||
|
|
||||||
const LoadingPage = function () {
|
const LoadingPage = function () {
|
||||||
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Grid container alignItems="center" justify="center" style={{ minHeight: "100vh" }}>
|
<Grid container alignItems="center" justify="center" style={{ minHeight: "100vh" }}>
|
||||||
<Grid item style={{ textAlign: "center", display: "inline-block" }}>
|
<Grid item style={{ textAlign: "center", display: "inline-block" }}>
|
||||||
<ReactLoading width={64} height={64} color="black" type="bars" />
|
<ReactLoading width={64} height={64} color={theme.custom.loadingBar} type="bars" />
|
||||||
<Typography>Loading...</Typography>
|
<Typography>Loading...</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
Loading…
Reference in New Issue