add settings page
This commit is contained in:
parent
cbc52b7e63
commit
d4e9865ada
|
@ -9,7 +9,6 @@
|
|||
"@babel/plugin-transform-runtime": "^7.16.8",
|
||||
"@babel/preset-env": "^7.16.8",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/hjson": "^2.4.3",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/prismjs": "^1.16.6",
|
||||
"@types/uikit": "^3.3.2",
|
||||
|
@ -41,7 +40,6 @@
|
|||
"core-js": "^3.20.3",
|
||||
"date-fns": "^2.28.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"hjson": "^3.2.2",
|
||||
"i18next": "^21.6.6",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.2.0",
|
||||
|
|
|
@ -29,6 +29,7 @@ import { PolicyView } from "./ui/pages/Policies/PolicyView";
|
|||
import { Secrets } from "./ui/pages/Secrets/SecretsHome";
|
||||
import { SetLanguage } from "./ui/pages/SetLanguage";
|
||||
import { SetVaultURL } from "./ui/pages/SetVaultURL";
|
||||
import { Settings } from "./ui/pages/Settings/Settings";
|
||||
import { TOTPDelete } from "./ui/pages/Secrets/TOTP/TOTPDelete";
|
||||
import { TOTPList } from "./ui/pages/Secrets/TOTP/TOTPList";
|
||||
import { TOTPNew } from "./ui/pages/Secrets/TOTP/TOTPNew";
|
||||
|
@ -59,6 +60,7 @@ export const Main = () => (
|
|||
<SetVaultURL path="/set_vault_url" settings={settings} api={api} />
|
||||
<Unseal path="/unseal" settings={settings} api={api} />
|
||||
<SetLanguage path="/set_language" settings={settings} api={api} />
|
||||
<Settings path="/settings" settings={settings} api={api} />
|
||||
|
||||
<Secrets path="/secrets" settings={settings} api={api} />
|
||||
<DeleteSecretsEngine path="/secrets/delete_engine/:mount" settings={settings} api={api} />
|
||||
|
|
|
@ -64,4 +64,24 @@ export class Settings {
|
|||
this.storage.setItem("theme", value);
|
||||
this.alertChange("theme");
|
||||
}
|
||||
|
||||
get kvEditorDefaultLanguage(): string {
|
||||
return this.storage.getItem("kvEditorDefaultLanguage") || "yaml";
|
||||
}
|
||||
|
||||
set kvEditorDefaultLanguage(value: string) {
|
||||
this.storage.setItem("kvEditorDefaultLanguage", value);
|
||||
this.alertChange("kvEditorDefaultLanguage");
|
||||
}
|
||||
|
||||
get kvEditorIndent(): number {
|
||||
const value = this.storage.getItem("kvEditorIndent");
|
||||
if (value) return parseInt(value);
|
||||
return 2;
|
||||
}
|
||||
|
||||
set kvEditorIndent(value: number) {
|
||||
this.storage.setItem("kvEditorIndent", String(value));
|
||||
this.alertChange("kvEditorIndent");
|
||||
}
|
||||
}
|
||||
|
|
26
src/translations/en.js
vendored
26
src/translations/en.js
vendored
|
@ -35,9 +35,9 @@ module.exports = {
|
|||
not_implemented: "Not Yet Implemented",
|
||||
|
||||
// Copyable Modal
|
||||
copy_box_download_btn: "Download",
|
||||
copy_box_copy_btn: "Copy",
|
||||
copy_box_close_btn: "Close",
|
||||
copy_modal_download_btn: "Download",
|
||||
copy_modal_copy_btn: "Copy",
|
||||
copy_modal_close_btn: "Close",
|
||||
|
||||
// Generic Loading Text
|
||||
content_loading: "Loading..",
|
||||
|
@ -54,8 +54,7 @@ module.exports = {
|
|||
me_seal_vault_btn: "Seal Vault",
|
||||
me_copy_token_btn: "Copy Token",
|
||||
me_renew_lease_btn: "Renew Token Lease",
|
||||
me_change_language_btn: "Change Language",
|
||||
me_set_vault_url_btn: "Set Vault URL",
|
||||
me_settings_btn: "Settings",
|
||||
|
||||
// Home Page
|
||||
home_page_title: "Home",
|
||||
|
@ -69,6 +68,23 @@ module.exports = {
|
|||
home_policies_title: "Policies",
|
||||
home_policies_description: "Manage policies and permissions.",
|
||||
|
||||
// Settings Page
|
||||
settings_title: "Settings",
|
||||
|
||||
// General Settings
|
||||
settings_general_title: "General",
|
||||
settings_general_theme: "Theme",
|
||||
settings_general_vault_url: "Vault API URL",
|
||||
settings_general_language: "Language",
|
||||
settings_general_page_direction: "Page Direction",
|
||||
settings_general_page_direction_ltr: "Left to Right",
|
||||
settings_general_page_direction_rtl: "Right to Left",
|
||||
|
||||
// Key/Value Editor Settings
|
||||
settings_kveditor_title: "Key/Value Editor",
|
||||
settings_kveditor_default_language: "Default Data Interchange Format",
|
||||
settings_kveditor_default_indent: "Default Indent",
|
||||
|
||||
// Set Vault URL Page
|
||||
set_vault_url_title: "Set Vault URL",
|
||||
set_vault_url_placeholder: "Vault URL",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Button } from "../elements/Button";
|
||||
import { Component, JSX, createRef } from "preact";
|
||||
import { DefaultPageProps } from "../../types/DefaultPageProps";
|
||||
import { InputWithTitle } from "../elements/InputWithTitle";
|
||||
import { PageTitle } from "../elements/PageTitle";
|
||||
import { addClipboardNotifications, setErrorText } from "../../pageUtils";
|
||||
import { route } from "preact-router";
|
||||
|
@ -103,30 +103,10 @@ export class Me extends Component<DefaultPageProps, MeState> {
|
|||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<a href="/set_language">{i18next.t("me_change_language_btn")}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/set_vault_url">{i18next.t("me_set_vault_url_btn")}</a>
|
||||
</li>
|
||||
|
||||
<InputWithTitle title="Theme">
|
||||
<select
|
||||
ref={this.themeSelectRef}
|
||||
class="uk-select uk-form-width-medium"
|
||||
onChange={() => {
|
||||
const newTheme = this.themeSelectRef.current.value;
|
||||
this.props.settings.theme = newTheme;
|
||||
}}
|
||||
>
|
||||
<option label="Dark" value="dark" selected={this.props.settings.theme == "dark"} />
|
||||
<option
|
||||
label="Light"
|
||||
value="light"
|
||||
selected={this.props.settings.theme == "light"}
|
||||
/>
|
||||
</select>
|
||||
</InputWithTitle>
|
||||
<br />
|
||||
|
||||
<Button text={i18next.t("me_settings_btn")} color="primary" route="/settings" />
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -6,38 +6,31 @@ import { InputWithTitle } from "../../../elements/InputWithTitle";
|
|||
import { SecretTitleElement } from "../SecretTitleElement";
|
||||
import { setErrorText } from "../../../../pageUtils";
|
||||
import { sortedObjectMap } from "../../../../utils";
|
||||
import Hjson from "hjson";
|
||||
import JSON5 from "json5";
|
||||
import i18next from "i18next";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
// todo: put this in settings
|
||||
const defaultIndent = 2;
|
||||
const defaultSyntax = "json";
|
||||
export const SupportedEditorLanguages = [
|
||||
{ name: "json", readable: "JSON" },
|
||||
{ name: "json5", readable: "JSON5" },
|
||||
{ name: "yaml", readable: "YAML" },
|
||||
];
|
||||
|
||||
function parseData(data: string, syntax = "json"): Record<string, unknown> {
|
||||
if (syntax == "json") {
|
||||
return JSON.parse(data) as Record<string, unknown>;
|
||||
} else if (syntax == "json5") {
|
||||
return JSON5.parse(data);
|
||||
} else if (syntax == "hjson") {
|
||||
return Hjson.parse(data) as Record<string, unknown>;
|
||||
} else if (syntax == "yaml") {
|
||||
return yaml.load(data) as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
function dumpData(
|
||||
data: Record<string, unknown>,
|
||||
space: number = defaultIndent,
|
||||
syntax = "json",
|
||||
): string {
|
||||
function dumpData(data: Record<string, unknown>, space = 4, syntax = "json"): string {
|
||||
if (syntax == "json") {
|
||||
return JSON.stringify(data, null, space);
|
||||
} else if (syntax == "json5") {
|
||||
return JSON5.stringify(data, null, space);
|
||||
} else if (syntax == "hjson") {
|
||||
return Hjson.stringify(data, { space: space });
|
||||
} else if (syntax == "yaml") {
|
||||
return yaml.dump(data, { indent: space });
|
||||
}
|
||||
|
@ -77,7 +70,7 @@ export class KVEditor extends Component<KVEditProps, KVEditState> {
|
|||
super();
|
||||
this.state = {
|
||||
dataLoaded: false,
|
||||
syntax: defaultSyntax,
|
||||
syntax: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -119,13 +112,18 @@ export class KVEditor extends Component<KVEditProps, KVEditState> {
|
|||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.setState({ syntax: this.props.settings.kvEditorDefaultLanguage });
|
||||
if (!this.state.dataLoaded) {
|
||||
await this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
getStringKVData(data: Record<string, unknown>): string {
|
||||
return dumpData(Object.fromEntries(sortedObjectMap(data)), defaultIndent, this.state.syntax);
|
||||
return dumpData(
|
||||
Object.fromEntries(sortedObjectMap(data)),
|
||||
this.props.settings.kvEditorIndent,
|
||||
this.state.syntax,
|
||||
);
|
||||
}
|
||||
|
||||
syntaxSelectRef = createRef<HTMLSelectElement>();
|
||||
|
@ -141,8 +139,6 @@ export class KVEditor extends Component<KVEditProps, KVEditState> {
|
|||
codeEditorLanguage = "json";
|
||||
} else if (this.state.syntax == "json5") {
|
||||
codeEditorLanguage = "json5";
|
||||
} else if (this.state.syntax == "hjson") {
|
||||
codeEditorLanguage = "js";
|
||||
} else if (this.state.syntax == "yaml") {
|
||||
codeEditorLanguage = "yaml";
|
||||
}
|
||||
|
@ -157,16 +153,21 @@ export class KVEditor extends Component<KVEditProps, KVEditState> {
|
|||
this.setState({ syntax: this.syntaxSelectRef.current.value });
|
||||
}}
|
||||
>
|
||||
<option label="JSON" value="json" />
|
||||
<option label="JSON5" value="json5" />
|
||||
<option label="Hjson" value="hjson" />
|
||||
<option label="Yaml" value="yaml" />
|
||||
{SupportedEditorLanguages.map((lang) => {
|
||||
return (
|
||||
<option
|
||||
label={lang.readable}
|
||||
value={lang.name}
|
||||
selected={this.props.settings.kvEditorDefaultLanguage == lang.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</InputWithTitle>
|
||||
<p class="uk-text-danger" id="errorText" />
|
||||
<CodeEditor
|
||||
language={codeEditorLanguage}
|
||||
tabSize={defaultIndent}
|
||||
tabSize={this.props.settings.kvEditorIndent}
|
||||
code={this.getStringKVData(this.state.kvData)}
|
||||
onUpdate={(code) => this.onCodeUpdate(code)}
|
||||
/>
|
||||
|
|
|
@ -25,12 +25,7 @@ function SecretsList(baseMount: string, secretPath: string[], secrets: string[])
|
|||
<a
|
||||
onClick={async () => {
|
||||
if (secret.endsWith("/")) {
|
||||
route(
|
||||
kvListURL(
|
||||
baseMount,
|
||||
[...secretPath, secret.replace("/", "")]
|
||||
),
|
||||
);
|
||||
route(kvListURL(baseMount, [...secretPath, secret.replace("/", "")]));
|
||||
} else {
|
||||
route(kvViewURL(baseMount, secretPath, secret));
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ export class KeyValueNew extends Component<DefaultPageProps> {
|
|||
const mountInfo = await this.props.api.getMount(baseMount);
|
||||
if (mountInfo.options.version == "1") {
|
||||
// Can't have a empty secret on KV V1
|
||||
keyData = { "placeholder_on_kv1": "placeholder_on_kv1" };
|
||||
keyData = { placeholder_on_kv1: "placeholder_on_kv1" };
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
|
@ -71,7 +71,7 @@ export class RefreshingTOTPGridItem extends Component<TOTPGridItemProps, { totpV
|
|||
type TOTPItem = {
|
||||
totpKey: string;
|
||||
canDelete: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
type TOTPListState = {
|
||||
capabilities?: CapabilitiesType;
|
||||
|
@ -98,19 +98,25 @@ export class TOTPList extends Component<DefaultPageProps, TOTPListState> {
|
|||
try {
|
||||
const totpKeys = await api.getTOTPKeys(baseMount);
|
||||
|
||||
let totpKeyPermissions = await Promise.all(Array.from(totpKeys.map(async (key) => {
|
||||
const totpKeyPermissions = await Promise.all(
|
||||
Array.from(
|
||||
totpKeys.map(async (key) => {
|
||||
const totpCaps = await api.getCapsPath(removeDoubleSlash(baseMount + "/code/" + key));
|
||||
return {key: key, caps: totpCaps};
|
||||
})));
|
||||
return { key: key, caps: totpCaps };
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
totpItems = Array.from(totpKeyPermissions.map((keyData) => {
|
||||
totpItems = Array.from(
|
||||
totpKeyPermissions.map((keyData) => {
|
||||
// Filter out all non-readable totp keys.
|
||||
if (!keyData.caps.includes("read")) return;
|
||||
return {
|
||||
totpKey: keyData.key,
|
||||
canDelete: keyData.caps.includes("delete"),
|
||||
};
|
||||
}));
|
||||
}),
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
const error = e as Error;
|
||||
if (error != DoesNotExistError) {
|
||||
|
@ -166,11 +172,7 @@ export class TOTPList extends Component<DefaultPageProps, TOTPListState> {
|
|||
} else {
|
||||
return this.state.totpItems.map((totpItem) => {
|
||||
return (
|
||||
<RefreshingTOTPGridItem
|
||||
{...this.props}
|
||||
baseMount={baseMount}
|
||||
{...totpItem}
|
||||
/>
|
||||
<RefreshingTOTPGridItem {...this.props} baseMount={baseMount} {...totpItem} />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,10 +9,8 @@ import { Form } from "../elements/Form";
|
|||
import { Margin } from "../elements/Margin";
|
||||
import { MarginInline } from "../elements/MarginInline";
|
||||
import { PageTitle } from "../elements/PageTitle";
|
||||
import i18next from "i18next";
|
||||
import { route } from "preact-router";
|
||||
|
||||
const languageIDs = Object.getOwnPropertyNames(translations);
|
||||
import i18next from "i18next";
|
||||
|
||||
export class SetLanguage extends Component<DefaultPageProps> {
|
||||
constructor() {
|
||||
|
@ -25,8 +23,8 @@ export class SetLanguage extends Component<DefaultPageProps> {
|
|||
<Form onSubmit={(data) => this.onSubmit(data)}>
|
||||
<Margin>
|
||||
<select class="uk-select uk-form-width-large" name="language">
|
||||
{languageIDs.map((languageID) => (
|
||||
<option value={languageID}>
|
||||
{Object.getOwnPropertyNames(translations).map((languageID) => (
|
||||
<option value={languageID} selected={this.props.settings.language == languageID}>
|
||||
{i18next.getFixedT(languageID, null)("language_name")}
|
||||
</option>
|
||||
))}
|
||||
|
|
105
src/ui/pages/Settings/GeneralSettings.tsx
Normal file
105
src/ui/pages/Settings/GeneralSettings.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { Component, createRef } from "preact";
|
||||
import { DefaultPageProps } from "../../../types/DefaultPageProps";
|
||||
import { InputWithTitle } from "../../elements/InputWithTitle";
|
||||
import i18next from "i18next";
|
||||
|
||||
// @ts-ignore
|
||||
import translations from "../../../translations/index.mjs";
|
||||
|
||||
const Themes = [
|
||||
{ name: "dark", readable: "Dark" },
|
||||
{ name: "light", readable: "Light" },
|
||||
];
|
||||
|
||||
export class GeneralSettings extends Component<DefaultPageProps> {
|
||||
themeSelectRef = createRef<HTMLSelectElement>();
|
||||
vaultURLInputRef = createRef<HTMLInputElement>();
|
||||
pageDirectionRef = createRef<HTMLSelectElement>();
|
||||
languageSelectRef = createRef<HTMLSelectElement>();
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h4>{i18next.t("settings_general_title")}</h4>
|
||||
|
||||
<InputWithTitle title={i18next.t("settings_general_theme")}>
|
||||
<select
|
||||
ref={this.themeSelectRef}
|
||||
class="uk-select uk-form-width-medium"
|
||||
onChange={() => {
|
||||
const newTheme = this.themeSelectRef.current.value;
|
||||
this.props.settings.theme = newTheme;
|
||||
}}
|
||||
>
|
||||
{Themes.map((theme) => {
|
||||
return (
|
||||
<option
|
||||
label={theme.readable}
|
||||
value={theme.name}
|
||||
selected={this.props.settings.theme == theme.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</InputWithTitle>
|
||||
|
||||
<InputWithTitle title={i18next.t("settings_general_vault_url")}>
|
||||
<input
|
||||
ref={this.vaultURLInputRef}
|
||||
class="uk-input uk-form-width-medium"
|
||||
value={this.props.settings.apiURL}
|
||||
onChange={() => {
|
||||
// TODO: check for api health to see if is valid api url.
|
||||
this.props.settings.apiURL = this.vaultURLInputRef.current.value;
|
||||
}}
|
||||
/>
|
||||
</InputWithTitle>
|
||||
|
||||
<InputWithTitle title={i18next.t("settings_general_language")}>
|
||||
<select
|
||||
ref={this.languageSelectRef}
|
||||
class="uk-select uk-form-width-medium"
|
||||
onChange={async () => {
|
||||
const language = this.languageSelectRef.current.value;
|
||||
this.props.settings.language = language;
|
||||
|
||||
const t = await i18next.changeLanguage(language);
|
||||
this.props.settings.pageDirection = t("language_direction");
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{Object.getOwnPropertyNames(translations).map((languageID) => (
|
||||
<option value={languageID} selected={this.props.settings.language == languageID}>
|
||||
{i18next.getFixedT(languageID, null)("language_name")}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</InputWithTitle>
|
||||
|
||||
<InputWithTitle title={i18next.t("settings_general_page_direction")}>
|
||||
<select
|
||||
ref={this.pageDirectionRef}
|
||||
class="uk-select uk-form-width-medium"
|
||||
onChange={() => {
|
||||
this.props.settings.pageDirection = this.pageDirectionRef.current.value;
|
||||
document.documentElement.dir = this.props.settings.pageDirection;
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ name: "ltr", readable: i18next.t("settings_general_page_direction_ltr") },
|
||||
{ name: "rtl", readable: i18next.t("settings_general_page_direction_rtl") },
|
||||
].map((direction) => {
|
||||
return (
|
||||
<option
|
||||
label={direction.readable}
|
||||
value={direction.name}
|
||||
selected={this.props.settings.pageDirection == direction.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</InputWithTitle>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
51
src/ui/pages/Settings/KeyValueEditorSettings.tsx
Normal file
51
src/ui/pages/Settings/KeyValueEditorSettings.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { Component, createRef } from "preact";
|
||||
import { DefaultPageProps } from "../../../types/DefaultPageProps";
|
||||
import { InputWithTitle } from "../../elements/InputWithTitle";
|
||||
import { SupportedEditorLanguages } from "../Secrets/KeyValue/KeyValueEdit";
|
||||
import i18next from "i18next";
|
||||
|
||||
export class KeyValueEditorSettings extends Component<DefaultPageProps> {
|
||||
syntaxSelectRef = createRef<HTMLSelectElement>();
|
||||
indentInputRef = createRef<HTMLInputElement>();
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<h4>{i18next.t("settings_kveditor_title")}</h4>
|
||||
|
||||
<InputWithTitle title={i18next.t("settings_kveditor_default_language")}>
|
||||
<select
|
||||
ref={this.syntaxSelectRef}
|
||||
class="uk-select uk-form-width-medium"
|
||||
onChange={() => {
|
||||
this.props.settings.kvEditorDefaultLanguage = this.syntaxSelectRef.current.value;
|
||||
}}
|
||||
>
|
||||
{SupportedEditorLanguages.map((lang) => {
|
||||
return (
|
||||
<option
|
||||
label={lang.readable}
|
||||
value={lang.name}
|
||||
selected={this.props.settings.kvEditorDefaultLanguage == lang.name}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</InputWithTitle>
|
||||
<InputWithTitle title={i18next.t("settings_kveditor_default_indent")}>
|
||||
<input
|
||||
ref={this.indentInputRef}
|
||||
class="uk-input uk-form-width-medium"
|
||||
type="number"
|
||||
value={this.props.settings.kvEditorIndent}
|
||||
onChange={() => {
|
||||
const value = this.indentInputRef.current.value;
|
||||
const indent = parseInt(value);
|
||||
this.props.settings.kvEditorIndent = indent;
|
||||
}}
|
||||
/>
|
||||
</InputWithTitle>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
22
src/ui/pages/Settings/Settings.tsx
Normal file
22
src/ui/pages/Settings/Settings.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Component } from "preact";
|
||||
import { DefaultPageProps } from "../../../types/DefaultPageProps";
|
||||
import { GeneralSettings } from "./GeneralSettings";
|
||||
import { KeyValueEditorSettings } from "./KeyValueEditorSettings";
|
||||
import { PageTitle } from "../../elements/PageTitle";
|
||||
import i18next from "i18next";
|
||||
|
||||
export class Settings extends Component<DefaultPageProps> {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={i18next.t("settings_title")} />
|
||||
<div>
|
||||
<GeneralSettings {...this.props} />
|
||||
</div>
|
||||
<div>
|
||||
<KeyValueEditorSettings {...this.props} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue