mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(react/settings): port etapi tokens
This commit is contained in:
parent
68086ec3f1
commit
c9dcbef014
@ -40,6 +40,7 @@ import ImageSettings from "./options/images.jsx";
|
||||
import AdvancedSettings from "./options/advanced.jsx";
|
||||
import InternationalizationOptions from "./options/i18n.jsx";
|
||||
import SyncOptions from "./options/sync.jsx";
|
||||
import EtapiSettings from "./options/etapi.js";
|
||||
|
||||
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
|
||||
<style>
|
||||
@ -96,9 +97,7 @@ const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextA
|
||||
ProtectedSessionTimeoutOptions
|
||||
],
|
||||
_optionsMFA: [MultiFactorAuthenticationOptions],
|
||||
_optionsEtapi: [
|
||||
EtapiOptions
|
||||
],
|
||||
_optionsEtapi: <EtapiSettings />,
|
||||
_optionsBackup: [
|
||||
BackupOptions
|
||||
],
|
||||
|
@ -1,157 +0,0 @@
|
||||
import { formatDateTime } from "../../../utils/formatters.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import dialogService from "../../../services/dialog.js";
|
||||
import OptionsWidget from "./options_widget.js";
|
||||
import server from "../../../services/server.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="etapi-options-section options-section">
|
||||
<h4>${t("etapi.title")}</h4>
|
||||
|
||||
<p class="form-text">${t("etapi.description")} <br/>
|
||||
${t("etapi.see_more", {
|
||||
link_to_wiki: `<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a>`,
|
||||
// TODO: We use window.open src/public/app/services/link.ts -> prevents regular click behavior on "a" element here because it's a relative path
|
||||
link_to_openapi_spec: `<a class="tn-link" onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>`,
|
||||
link_to_swagger_ui: `<a class="tn-link" href="#_help_f3xpgx6H01PW">${t("etapi.swagger_ui")}</a>`
|
||||
})}
|
||||
</p>
|
||||
|
||||
<button type="button" class="create-etapi-token btn btn-sm">
|
||||
<span class="bx bx-plus"></span>
|
||||
${t("etapi.create_token")}
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>${t("etapi.existing_tokens")}</h5>
|
||||
|
||||
<div class="no-tokens-yet">${t("etapi.no_tokens_yet")}</div>
|
||||
|
||||
<div style="overflow: auto; height: 500px;">
|
||||
<table class="tokens-table table table-stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${t("etapi.token_name")}</th>
|
||||
<th>${t("etapi.created")}</th>
|
||||
<th>${t("etapi.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.token-table-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 3px;
|
||||
margin-right: 20px;
|
||||
font-size: large;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--button-border-radius);
|
||||
}
|
||||
|
||||
.token-table-button:hover {
|
||||
border: 1px solid var(--button-border-color);
|
||||
}
|
||||
</style>`;
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface PostTokensResponse {
|
||||
authToken: string;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface Token {
|
||||
name: string;
|
||||
utcDateCreated: number;
|
||||
etapiTokenId: string;
|
||||
}
|
||||
|
||||
export default class EtapiOptions extends OptionsWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$widget.find(".create-etapi-token").on("click", async () => {
|
||||
const tokenName = await dialogService.prompt({
|
||||
title: t("etapi.new_token_title"),
|
||||
message: t("etapi.new_token_message"),
|
||||
defaultValue: t("etapi.default_token_name")
|
||||
});
|
||||
|
||||
if (!tokenName?.trim()) {
|
||||
toastService.showError(t("etapi.error_empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
const { authToken } = await server.post<PostTokensResponse>("etapi-tokens", { tokenName });
|
||||
|
||||
await dialogService.prompt({
|
||||
title: t("etapi.token_created_title"),
|
||||
message: t("etapi.token_created_message"),
|
||||
defaultValue: authToken
|
||||
});
|
||||
|
||||
this.refreshTokens();
|
||||
});
|
||||
|
||||
this.refreshTokens();
|
||||
}
|
||||
|
||||
async refreshTokens() {
|
||||
const $noTokensYet = this.$widget.find(".no-tokens-yet");
|
||||
const $tokensTable = this.$widget.find(".tokens-table");
|
||||
|
||||
const tokens = await server.get<Token[]>("etapi-tokens");
|
||||
|
||||
$noTokensYet.toggle(tokens.length === 0);
|
||||
$tokensTable.toggle(tokens.length > 0);
|
||||
|
||||
const $tokensTableBody = $tokensTable.find("tbody");
|
||||
$tokensTableBody.empty();
|
||||
|
||||
for (const token of tokens) {
|
||||
$tokensTableBody.append(
|
||||
$("<tr>")
|
||||
.append($("<td>").text(token.name))
|
||||
.append($("<td>").text(formatDateTime(token.utcDateCreated)))
|
||||
.append(
|
||||
$("<td>").append(
|
||||
$(`<span class="bx bx-edit-alt token-table-button icon-action" title="${t("etapi.rename_token")}"></span>`).on("click", () => this.renameToken(token.etapiTokenId, token.name)),
|
||||
$(`<span class="bx bx-trash token-table-button icon-action" title="${t("etapi.delete_token")}"></span>`).on("click", () => this.deleteToken(token.etapiTokenId, token.name))
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async renameToken(etapiTokenId: string, oldName: string) {
|
||||
const tokenName = await dialogService.prompt({
|
||||
title: t("etapi.rename_token_title"),
|
||||
message: t("etapi.rename_token_message"),
|
||||
defaultValue: oldName
|
||||
});
|
||||
|
||||
if (!tokenName?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await server.patch(`etapi-tokens/${etapiTokenId}`, { name: tokenName });
|
||||
|
||||
this.refreshTokens();
|
||||
}
|
||||
|
||||
async deleteToken(etapiTokenId: string, name: string) {
|
||||
if (!(await dialogService.confirm(t("etapi.delete_token_confirmation", { name })))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await server.remove(`etapi-tokens/${etapiTokenId}`);
|
||||
|
||||
this.refreshTokens();
|
||||
}
|
||||
}
|
139
apps/client/src/widgets/type_widgets/options/etapi.tsx
Normal file
139
apps/client/src/widgets/type_widgets/options/etapi.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import Button from "../../react/Button";
|
||||
import FormText from "../../react/FormText";
|
||||
import RawHtml from "../../react/RawHtml";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import { EtapiToken, PostTokensResponse } from "@triliumnext/commons";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { formatDateTime } from "../../../utils/formatters";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
|
||||
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
|
||||
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;
|
||||
|
||||
export default function EtapiSettings() {
|
||||
const [ tokens, setTokens ] = useState<EtapiToken[]>([]);
|
||||
|
||||
function refreshTokens() {
|
||||
server.get<EtapiToken[]>("etapi-tokens").then(setTokens);
|
||||
}
|
||||
|
||||
useEffect(refreshTokens, []);
|
||||
|
||||
const createTokenCallback = useCallback(async () => {
|
||||
const tokenName = await dialog.prompt({
|
||||
title: t("etapi.new_token_title"),
|
||||
message: t("etapi.new_token_message"),
|
||||
defaultValue: t("etapi.default_token_name")
|
||||
});
|
||||
|
||||
if (!tokenName?.trim()) {
|
||||
toast.showError(t("etapi.error_empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
const { authToken } = await server.post<PostTokensResponse>("etapi-tokens", { tokenName });
|
||||
|
||||
await dialog.prompt({
|
||||
title: t("etapi.token_created_title"),
|
||||
message: t("etapi.token_created_message"),
|
||||
defaultValue: authToken
|
||||
});
|
||||
|
||||
refreshTokens();
|
||||
}, []);
|
||||
|
||||
const renameTokenCallback = useCallback<RenameTokenCallback>(async (tokenId: string, oldName: string) => {
|
||||
const tokenName = await dialog.prompt({
|
||||
title: t("etapi.rename_token_title"),
|
||||
message: t("etapi.rename_token_message"),
|
||||
defaultValue: oldName
|
||||
});
|
||||
|
||||
if (!tokenName?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await server.patch(`etapi-tokens/${tokenId}`, { name: tokenName });
|
||||
|
||||
refreshTokens();
|
||||
}, []);
|
||||
|
||||
const deleteTokenCallback = useCallback<DeleteTokenCallback>(async (tokenId: string, name: string) => {
|
||||
if (!(await dialog.confirm(t("etapi.delete_token_confirmation", { name })))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await server.remove(`etapi-tokens/${tokenId}`);
|
||||
refreshTokens();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("etapi.title")}>
|
||||
<FormText>
|
||||
{t("etapi.description")}<br />
|
||||
<RawHtml
|
||||
html={t("etapi.see_more", {
|
||||
link_to_wiki: `<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a>`,
|
||||
// TODO: We use window.open src/public/app/services/link.ts -> prevents regular click behavior on "a" element here because it's a relative path
|
||||
link_to_openapi_spec: `<a class="tn-link" onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>`,
|
||||
link_to_swagger_ui: `<a class="tn-link" href="#_help_f3xpgx6H01PW">${t("etapi.swagger_ui")}</a>`
|
||||
})} />
|
||||
</FormText>
|
||||
|
||||
<Button
|
||||
size="small" icon="bx bx-plus"
|
||||
text={t("etapi.create_token")}
|
||||
onClick={createTokenCallback}
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<h5>{t("etapi.existing_tokens")}</h5>
|
||||
<TokenList tokens={tokens} renameCallback={renameTokenCallback} deleteCallback={deleteTokenCallback} />
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
function TokenList({ tokens, renameCallback, deleteCallback }: { tokens: EtapiToken[], renameCallback: RenameTokenCallback, deleteCallback: DeleteTokenCallback }) {
|
||||
if (!tokens.length) {
|
||||
return <div>{t("etapi.no_tokens_yet")}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ overflow: "auto", height: "500px"}}>
|
||||
<table className="table table-stripped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("etapi.token_name")}</th>
|
||||
<th>{t("etapi.created")}</th>
|
||||
<th>{t("etapi.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.map(({ etapiTokenId, name, utcDateCreated}) => (
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td>{formatDateTime(utcDateCreated)}</td>
|
||||
<td>
|
||||
<ActionButton
|
||||
icon="bx bx-edit-alt"
|
||||
text={t("etapi.rename_token")}
|
||||
onClick={() => renameCallback(etapiTokenId!, name)}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
icon="bx bx-trash"
|
||||
text={t("etapi.delete_token")}
|
||||
onClick={() => deleteCallback(etapiTokenId!, name)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -86,7 +86,7 @@ export function SyncTest() {
|
||||
if (result.success) {
|
||||
toast.showMessage(result.message);
|
||||
} else {
|
||||
toast.showError(t("sync_2.handshake_failed", { message: result.error }));
|
||||
toast.showError(t("sync_2.handshake_failed", { message: result.message }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1,16 +1,17 @@
|
||||
import type { Request } from "express";
|
||||
import etapiTokenService from "../../services/etapi_tokens.js";
|
||||
import { EtapiToken, PostTokensResponse } from "@triliumnext/commons";
|
||||
|
||||
function getTokens() {
|
||||
const tokens = etapiTokenService.getTokens();
|
||||
|
||||
tokens.sort((a, b) => (a.utcDateCreated < b.utcDateCreated ? -1 : 1));
|
||||
|
||||
return tokens;
|
||||
return tokens satisfies EtapiToken[];
|
||||
}
|
||||
|
||||
function createToken(req: Request) {
|
||||
return etapiTokenService.createToken(req.body.tokenName);
|
||||
return etapiTokenService.createToken(req.body.tokenName) satisfies PostTokensResponse;
|
||||
}
|
||||
|
||||
function patchToken(req: Request) {
|
||||
|
@ -35,7 +35,7 @@ async function testSync(): Promise<SyncTestResponse> {
|
||||
const [errMessage] = safeExtractMessageAndStackFromError(e);
|
||||
return {
|
||||
success: false,
|
||||
error: errMessage
|
||||
message: errMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { AttributeRow, NoteType } from "./rows.js";
|
||||
|
||||
type Response = {
|
||||
success: true,
|
||||
message: string;
|
||||
} | {
|
||||
success: false;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AppInfo {
|
||||
appVersion: string;
|
||||
dbVersion: number;
|
||||
@ -78,10 +86,14 @@ export interface AnonymizedDbResponse {
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export type SyncTestResponse = {
|
||||
success: true;
|
||||
message: string;
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
export type SyncTestResponse = Response;
|
||||
|
||||
export interface EtapiToken {
|
||||
name: string;
|
||||
utcDateCreated: string;
|
||||
etapiTokenId?: string;
|
||||
}
|
||||
|
||||
export interface PostTokensResponse {
|
||||
authToken: string;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user