From fd8b1875e000373234da94bb213313f626f6faa3 Mon Sep 17 00:00:00 2001 From: Benoit Devos Date: Wed, 15 May 2024 10:53:39 +0200 Subject: [PATCH 1/3] feat: add secret management. logion-network/logion-internal#1253 --- package.json | 2 +- src/__mocks__/@logion/client.ts | 2 + src/__mocks__/LogionClientMock.ts | 9 ++ src/loc/CertificateAndDetailsButtons.tsx | 3 +- src/loc/RequestVoteButton.test.tsx | 8 +- src/loc/RequestVoteButton.tsx | 4 +- src/loc/TestData.ts | 3 +- .../ContextualizedLocDetails.test.tsx.snap | 7 ++ ...UserContextualizedLocDetails.test.tsx.snap | 12 +++ src/loc/issuer/Nominate.tsx | 6 +- src/loc/secrets/AddSecretDialog.tsx | 98 +++++++++++++++++++ src/loc/secrets/RemoveSecretDialog.tsx | 49 ++++++++++ src/loc/secrets/SecretsButton.tsx | 18 ++++ src/loc/secrets/SecretsPane.tsx | 86 ++++++++++++++++ src/loc/secrets/SecretsTable.tsx | 37 +++++++ src/wallet-user/UserPaths.tsx | 7 ++ src/wallet-user/UserRouter.tsx | 5 +- .../__snapshots__/UserRouter.test.tsx.snap | 4 + yarn.lock | 10 +- 19 files changed, 352 insertions(+), 18 deletions(-) create mode 100644 src/loc/secrets/AddSecretDialog.tsx create mode 100644 src/loc/secrets/RemoveSecretDialog.tsx create mode 100644 src/loc/secrets/SecretsButton.tsx create mode 100644 src/loc/secrets/SecretsPane.tsx create mode 100644 src/loc/secrets/SecretsTable.tsx diff --git a/package.json b/package.json index faafbcae..ab4b3b45 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@creativecommons/cc-assets": "^0.1.0", - "@logion/client": "^0.45.0-2", + "@logion/client": "^0.45.0-7", "@logion/client-browser": "^0.3.5", "@logion/crossmint": "^0.1.32", "@logion/extension": "^0.8.1-1", diff --git a/src/__mocks__/@logion/client.ts b/src/__mocks__/@logion/client.ts index 4e4c5373..7ab47c6d 100644 --- a/src/__mocks__/@logion/client.ts +++ b/src/__mocks__/@logion/client.ts @@ -16,6 +16,7 @@ import { LocsState, HashOrContent, ClosedLoc, + ClosedIdentityLoc, ReadOnlyLocState, PendingRequest, } from '../LogionClientMock'; @@ -39,6 +40,7 @@ export { LocsState, HashOrContent, ClosedLoc, + ClosedIdentityLoc, ReadOnlyLocState, isTokenCompatibleWith, LegalOfficerClass, diff --git a/src/__mocks__/LogionClientMock.ts b/src/__mocks__/LogionClientMock.ts index 1fd01590..8039dad5 100644 --- a/src/__mocks__/LogionClientMock.ts +++ b/src/__mocks__/LogionClientMock.ts @@ -146,6 +146,15 @@ export class ClosedLoc extends LocRequestState { }; } +export class ClosedIdentityLoc extends LocRequestState { + + legalOfficer: { + requestVote: any, + } = { + requestVote: jest.fn(), + }; +} + export class EditableRequest extends LocRequestState { addMetadata: jest.Mock> | undefined; deleteMetadata: jest.Mock> | undefined; diff --git a/src/loc/CertificateAndDetailsButtons.tsx b/src/loc/CertificateAndDetailsButtons.tsx index 307fba05..e169fad4 100644 --- a/src/loc/CertificateAndDetailsButtons.tsx +++ b/src/loc/CertificateAndDetailsButtons.tsx @@ -27,6 +27,7 @@ import ViewQrCodeButton from './ViewQrCodeButton'; import ViewCertificateButton from './ViewCertificateButton'; import InvitedContributorsButton from "./invited-contributor/InvitedContributorsButton"; import { CollectionLimits, DEFAULT_LIMITS } from "./CollectionLimitsForm"; +import SecretsButton from "./secrets/SecretsButton"; export interface Props { loc: LocData; @@ -87,7 +88,7 @@ export default function CertificateAndDetailsButtons(props: Props) { { loc.locType === 'Identity' && props.viewer === 'LegalOfficer' && !isLogionIdentityLoc({ ...loc, requesterAddress: loc.requesterAccountId?.address }) && loc.requesterAccountId?.type === "Polkadot" && loc.status ==='CLOSED' && !props.isReadOnly && } { loc.locType === 'Identity' && !isLogionIdentityLoc({ ...loc, requesterAddress: loc.requesterAccountId?.address }) && props.viewer === 'LegalOfficer' && loc.status === "CLOSED" && hasVoteFeature && !loc.voteId && !props.isReadOnly && } - + { loc.locType === 'Identity' && props.viewer === 'User' && loc.status === "CLOSED" && } { loc.locType === 'Collection' && props.viewer === 'LegalOfficer' && } { loc.locType !== 'Collection' && props.viewer === 'LegalOfficer' && !props.isReadOnly && } diff --git a/src/loc/RequestVoteButton.test.tsx b/src/loc/RequestVoteButton.test.tsx index 9ad3bce3..b295e499 100644 --- a/src/loc/RequestVoteButton.test.tsx +++ b/src/loc/RequestVoteButton.test.tsx @@ -1,7 +1,7 @@ import { render, screen, waitFor } from "@testing-library/react"; import { clickByName } from "src/tests"; import RequestVoteButton from "./RequestVoteButton"; -import { ClosedLoc } from "src/__mocks__/@logion/client"; +import { ClosedIdentityLoc } from "src/__mocks__/@logion/client"; import { setLocState } from "./__mocks__/LocContextMock"; import { FAILED_SUBMISSION, NO_SUBMISSION, SUCCESSFUL_SUBMISSION, setExtrinsicSubmissionState } from "src/logion-chain/__mocks__/LogionChainMock"; import { expectSubmitting } from "src/test/Util"; @@ -12,7 +12,7 @@ jest.mock("../logion-chain"); describe("RequestVoteButton", () => { it("submits vote", async () => { - const locState = new ClosedLoc(); + const locState = new ClosedIdentityLoc(); setLocState(locState); locState.legalOfficer.requestVote = async (params: any) => { return VOTE_ID; @@ -25,7 +25,7 @@ describe("RequestVoteButton", () => { }); it("successfully creates a vote", async () => { - const locState = new ClosedLoc(); + const locState = new ClosedIdentityLoc(); setLocState(locState); locState.legalOfficer.requestVote = async (params: any) => { return VOTE_ID; @@ -39,7 +39,7 @@ describe("RequestVoteButton", () => { }); it("shows error on failure", async () => { - const locState = new ClosedLoc(); + const locState = new ClosedIdentityLoc(); setLocState(locState); locState.legalOfficer.requestVote = async () => {}; setExtrinsicSubmissionState(FAILED_SUBMISSION); diff --git a/src/loc/RequestVoteButton.tsx b/src/loc/RequestVoteButton.tsx index 16e40420..fbe0bd9b 100644 --- a/src/loc/RequestVoteButton.tsx +++ b/src/loc/RequestVoteButton.tsx @@ -1,4 +1,4 @@ -import { ClosedLoc } from "@logion/client"; +import { ClosedIdentityLoc } from "@logion/client"; import { useCallback, useState } from "react"; import ClientExtrinsicSubmitter, { Call, CallCallback } from "src/ClientExtrinsicSubmitter"; import Button from "src/common/Button"; @@ -18,7 +18,7 @@ export default function RequestVoteButton() { const [ submissionFailed, setSubmissionFailed ] = useState(false); const requestVoteCallback = useCallback(async (callback: CallCallback) => { - if(signer && (locState instanceof ClosedLoc)) { + if(signer && (locState instanceof ClosedIdentityLoc)) { const requestedVoteId = await locState.legalOfficer.requestVote({ callback, signer, diff --git a/src/loc/TestData.ts b/src/loc/TestData.ts index 42873768..a11088fb 100644 --- a/src/loc/TestData.ts +++ b/src/loc/TestData.ts @@ -48,6 +48,7 @@ export function buildLocRequest(locId: UUID, loc: LegalOfficerCase): LocData { legalFee: loc.legalFee, collectionItemFee: loc.collectionItemFee, tokensRecordFee: loc.tokensRecordFee, - } + }, + secrets: [], }; } diff --git a/src/loc/__snapshots__/ContextualizedLocDetails.test.tsx.snap b/src/loc/__snapshots__/ContextualizedLocDetails.test.tsx.snap index c7a25d40..e4573af7 100644 --- a/src/loc/__snapshots__/ContextualizedLocDetails.test.tsx.snap +++ b/src/loc/__snapshots__/ContextualizedLocDetails.test.tsx.snap @@ -60,6 +60,7 @@ exports[`ContextualizedLocDetails renders 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -124,6 +125,7 @@ exports[`ContextualizedLocDetails renders 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -199,6 +201,7 @@ exports[`ContextualizedLocDetails renders 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -271,6 +274,7 @@ exports[`ContextualizedLocDetails renders 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -336,6 +340,7 @@ exports[`ContextualizedLocDetails renders 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -401,6 +406,7 @@ exports[`ContextualizedLocDetails renders 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -473,6 +479,7 @@ exports[`ContextualizedLocDetails renders 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } diff --git a/src/loc/__snapshots__/UserContextualizedLocDetails.test.tsx.snap b/src/loc/__snapshots__/UserContextualizedLocDetails.test.tsx.snap index 3495aa93..a491bf81 100644 --- a/src/loc/__snapshots__/UserContextualizedLocDetails.test.tsx.snap +++ b/src/loc/__snapshots__/UserContextualizedLocDetails.test.tsx.snap @@ -61,6 +61,7 @@ exports[`UserContextualizedLocDetails renders for requester 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -160,6 +161,7 @@ exports[`UserContextualizedLocDetails renders for requester 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -229,6 +231,7 @@ exports[`UserContextualizedLocDetails renders for requester 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -295,6 +298,7 @@ exports[`UserContextualizedLocDetails renders for requester 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -361,6 +365,7 @@ exports[`UserContextualizedLocDetails renders for requester 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -426,6 +431,7 @@ exports[`UserContextualizedLocDetails renders for requester 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -501,6 +507,7 @@ exports[`UserContextualizedLocDetails renders for verified issuer 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -601,6 +608,7 @@ exports[`UserContextualizedLocDetails renders for verified issuer 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -670,6 +678,7 @@ exports[`UserContextualizedLocDetails renders for verified issuer 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -736,6 +745,7 @@ exports[`UserContextualizedLocDetails renders for verified issuer 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -802,6 +812,7 @@ exports[`UserContextualizedLocDetails renders for verified issuer 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } @@ -867,6 +878,7 @@ exports[`UserContextualizedLocDetails renders for verified issuer 1`] = ` }, }, "requesterLocId": undefined, + "secrets": Array [], "status": "OPEN", "verifiedIssuer": false, } diff --git a/src/loc/issuer/Nominate.tsx b/src/loc/issuer/Nominate.tsx index 6e8867e7..04c0fac2 100644 --- a/src/loc/issuer/Nominate.tsx +++ b/src/loc/issuer/Nominate.tsx @@ -5,7 +5,7 @@ import Icon from "../../common/Icon"; import Dialog from "../../common/Dialog"; import { useLocContext } from "../LocContext"; import { CallCallback, useLogionChain } from "../../logion-chain"; -import { ClosedLoc } from "@logion/client"; +import { ClosedIdentityLoc } from "@logion/client"; import './Nominate.css'; import ExtrinsicSubmissionStateView from "src/ExtrinsicSubmissionStateView"; @@ -26,7 +26,7 @@ export default function Nominate() { const changeIssuer = useCallback(async () => { setStatus('Confirming'); const call = async (callback: CallCallback) => mutateLocState(async current => { - if(signer && current instanceof ClosedLoc) { + if(signer && current instanceof ClosedIdentityLoc) { if(isIssuer) { return current.legalOfficer.dismissIssuer({ signer, @@ -36,7 +36,7 @@ export default function Nominate() { return current.legalOfficer.nominateIssuer({ signer, callback, - }); + }); } } else { return current; diff --git a/src/loc/secrets/AddSecretDialog.tsx b/src/loc/secrets/AddSecretDialog.tsx new file mode 100644 index 00000000..91d3e610 --- /dev/null +++ b/src/loc/secrets/AddSecretDialog.tsx @@ -0,0 +1,98 @@ +import { Secret } from "@logion/client"; +import Dialog from "../../common/Dialog"; +import { useCallback } from "react"; +import { useForm, Controller } from "react-hook-form"; +import FormGroup from "../../common/FormGroup"; +import { Form } from "react-bootstrap"; +import { useCommonContext } from "../../common/CommonContext"; + +export interface Props { + show: boolean; + onAddSecret: (secret: Secret) => void; + onCancel: () => void; +} + +export default function AddSecretDialog(props: Props) { + + const { colorTheme } = useCommonContext(); + const { handleSubmit, control, formState: { errors }, reset } = useForm(); + + const cancel = useCallback(() => { + props.onCancel(); + reset(); + }, [ props, reset ]) + + const submit = useCallback((secret: Secret) => { + props.onAddSecret(secret); + reset(); + }, [ props, reset ]) + + return ( + + ( + + ) } /> + + } + colors={ colorTheme.dialog } + /> + ( + + ) } /> + + } + colors={ colorTheme.dialog } + /> + + ) +} diff --git a/src/loc/secrets/RemoveSecretDialog.tsx b/src/loc/secrets/RemoveSecretDialog.tsx new file mode 100644 index 00000000..61ae8e09 --- /dev/null +++ b/src/loc/secrets/RemoveSecretDialog.tsx @@ -0,0 +1,49 @@ +import { Secret } from "@logion/client"; +import Dialog from "../../common/Dialog"; +import { useForm } from "react-hook-form"; +import { useCallback } from "react"; + +export interface Props { + secret: Secret | undefined; + onRemoveSecret: (secret: Secret) => void; + onCancel: () => void; +} + +export default function RemoveSecretDialog(props: Props) { + const { handleSubmit, reset } = useForm<{}>(); + + const cancel = useCallback(() => { + props.onCancel(); + reset(); + }, [ props, reset ]) + + const submit = useCallback((_: {}) => { + props.onRemoveSecret(props.secret!); + reset(); + }, [ props, reset ]) + + return ( + +

You are about to remove the secret { props.secret?.name }.

+

It will be impossible to recover it after, so make your own backup

+
+ ) +} diff --git a/src/loc/secrets/SecretsButton.tsx b/src/loc/secrets/SecretsButton.tsx new file mode 100644 index 00000000..019235d2 --- /dev/null +++ b/src/loc/secrets/SecretsButton.tsx @@ -0,0 +1,18 @@ +import Button from "../../common/Button"; +import { useNavigate } from "react-router-dom"; +import { secretsPath } from "../../wallet-user/UserPaths"; +import { useLocContext } from "../LocContext"; + +export default function SecretsButton() { + const { loc } = useLocContext(); + const navigate = useNavigate(); + if (loc === null) { + return null; + } + + return ( + + ) +} diff --git a/src/loc/secrets/SecretsPane.tsx b/src/loc/secrets/SecretsPane.tsx new file mode 100644 index 00000000..418aab4a --- /dev/null +++ b/src/loc/secrets/SecretsPane.tsx @@ -0,0 +1,86 @@ +import { useLocContext } from "../LocContext"; +import LocPane from "../LocPane"; +import Frame from "../../common/Frame"; +import { locDetailsPath as userLocDetailsPath } from "../../wallet-user/UserPaths"; +import { UserLocContextProvider } from "../UserLocContext"; +import { UUID } from "@logion/node-api"; +import { useParams } from "react-router-dom"; +import Button from "../../common/Button"; +import Icon from "../../common/Icon"; +import { useState, useCallback } from "react"; +import AddSecretDialog from "./AddSecretDialog"; +import { Secret, ClosedIdentityLoc } from "@logion/client"; +import SecretsTable from "./SecretsTable"; +import RemoveSecretDialog from "./RemoveSecretDialog"; + +function UserSecretsPane() { + const { loc, locState, backPath, mutateLocState } = useLocContext(); + const [ showAddDialog, setShowAddDialog ] = useState(false); + const [ secretToRemove, setSecretToRemove ] = useState(); + + const addSecret = useCallback(async (secret: Secret) => { + await mutateLocState(async current => { + if (current instanceof ClosedIdentityLoc) { + return await current.addSecret(secret); + } else { + return current; + } + }) + setShowAddDialog(false); + }, [ mutateLocState ]) + + const removeSecret = useCallback(async (secret: Secret) => { + await mutateLocState(async current => { + if (current instanceof ClosedIdentityLoc) { + return await current.removeSecret(secret.name); + } else { + return current; + } + }) + setSecretToRemove(undefined); + }, [ mutateLocState ]) + + return ( + + + + + setShowAddDialog(false) } + /> + setSecretToRemove(undefined) } + /> + + + ) +} + +export default function SecretsPane() { + const locId = new UUID(useParams<"locId">().locId); + + return ( + + + + ); +} diff --git a/src/loc/secrets/SecretsTable.tsx b/src/loc/secrets/SecretsTable.tsx new file mode 100644 index 00000000..7e6acd32 --- /dev/null +++ b/src/loc/secrets/SecretsTable.tsx @@ -0,0 +1,37 @@ +import Table, { EmptyTableMessage, Cell, ActionCell } from "../../common/Table"; +import { Secret } from "@logion/client"; +import Button from "react-bootstrap/Button"; + +export interface Properties { + secrets: Secret[]; + onRemoveSecret: (secret: Secret) => void; +} + +export default function SecretsTable(props: Properties) { + + return ( + <> + + }, + { + header: "Value", + render: secret => + }, + { + header: "", + render: secret => + + + + }, + ] } + renderEmpty={ () => No secret to display } + /> + + ) +} diff --git a/src/wallet-user/UserPaths.tsx b/src/wallet-user/UserPaths.tsx index 5c7f0ffc..22053248 100644 --- a/src/wallet-user/UserPaths.tsx +++ b/src/wallet-user/UserPaths.tsx @@ -117,3 +117,10 @@ export function invitedContributorsPath(locId: UUID) { .replace(":locId", locId.toString()); } +export const SECRETS_RELATIVE_PATH = LOC_DETAILS_RELATIVE_PATH + '/secrets'; +export function secretsPath(locId: UUID) { + return USER_PATH + SECRETS_RELATIVE_PATH + .replace(":locType", "identity") + .replace(":locId", locId.toString()); +} + diff --git a/src/wallet-user/UserRouter.tsx b/src/wallet-user/UserRouter.tsx index e72a96a8..9431c1e4 100644 --- a/src/wallet-user/UserRouter.tsx +++ b/src/wallet-user/UserRouter.tsx @@ -25,7 +25,8 @@ import { ISSUER_TOKENS_RECORD_RELATIVE_PATH, TOKENS_RECORD_DOCUMENT_CLAIM_HISTORY_RELATIVE_PATH, ISSUER_TOKENS_RECORD_DOCUMENT_CLAIM_HISTORY_RELATIVE_PATH, - INVITED_CONTRIBUTORS_RELATIVE_PATH + INVITED_CONTRIBUTORS_RELATIVE_PATH, + SECRETS_RELATIVE_PATH } from "./UserPaths"; import Settings from "../settings/Settings"; import Transactions from "../common/Transactions"; @@ -47,6 +48,7 @@ import { UserInvitedContributorsPane } from "../loc/invited-contributor/InvitedC import LocsDashboard from 'src/loc/dashboard/LocsDashboard'; import LocRequestButton from "../components/locrequest/LocRequestButton"; import DataLocRequest from "../loc/DataLocRequest"; +import SecretsPane from "../loc/secrets/SecretsPane"; export default function UserRouter() { const { accounts } = useLogionChain(); @@ -173,6 +175,7 @@ export default function UserRouter() { } /> } /> }/> + }/> ); } diff --git a/src/wallet-user/__snapshots__/UserRouter.test.tsx.snap b/src/wallet-user/__snapshots__/UserRouter.test.tsx.snap index b4b2a479..da1a8262 100644 --- a/src/wallet-user/__snapshots__/UserRouter.test.tsx.snap +++ b/src/wallet-user/__snapshots__/UserRouter.test.tsx.snap @@ -262,5 +262,9 @@ exports[`renders 1`] = ` } path="/loc/:locType/:locId/invited-contributors" /> + } + path="/loc/:locType/:locId/secrets" + /> `; diff --git a/yarn.lock b/yarn.lock index eb3a397a..7c047f2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3216,15 +3216,15 @@ __metadata: languageName: node linkType: hard -"@logion/client@npm:^0.45.0-2": - version: 0.45.0-2 - resolution: "@logion/client@npm:0.45.0-2" +"@logion/client@npm:^0.45.0-7": + version: 0.45.0-7 + resolution: "@logion/client@npm:0.45.0-7" dependencies: "@logion/node-api": ^0.30.0 axios: ^1.6.7 luxon: ^3.4.4 mime-db: ^1.52.0 - checksum: 6550c4a5649a58834a9743e4115b32b8de7996f31134c63a687e13b9066b27179ad65b3d80f5eb954693a0a35832aac8d77e1bfa5176f78ff85d6224f4c5007f + checksum: 37679a03af0afa468e6378c430fb31923397c268f94f8a4edaeece8d65fc38e0871dda9f19b96faf05d6be4c423ab9f2c5ce5573af1ca7e76e645eb5832c93a0 languageName: node linkType: hard @@ -12106,7 +12106,7 @@ __metadata: "@babel/preset-react": ^7.23.3 "@babel/preset-typescript": ^7.23.3 "@creativecommons/cc-assets": ^0.1.0 - "@logion/client": ^0.45.0-2 + "@logion/client": ^0.45.0-7 "@logion/client-browser": ^0.3.5 "@logion/crossmint": ^0.1.32 "@logion/extension": ^0.8.1-1 From 42ef92616e1ba6a5b9afbe1a43ffaf55a44d74fb Mon Sep 17 00:00:00 2001 From: Benoit Devos Date: Wed, 15 May 2024 17:31:44 +0200 Subject: [PATCH 2/3] feat: make secrets viewable (hidden by default); chore: improve style. logion-network/logion-internal#1253 --- src/components/toggle/Checkbox.tsx | 3 ++- src/components/toggle/Eye.css | 34 ++++++++++++++++++++++++++ src/img/eye-closed.svg | 3 +++ src/img/eye.svg | 4 +++ src/loc/secrets/AddSecretDialog.tsx | 1 + src/loc/secrets/RemoveSecretDialog.css | 3 +++ src/loc/secrets/RemoveSecretDialog.tsx | 10 ++++++-- src/loc/secrets/SecretsTable.tsx | 13 ++++++++-- src/loc/secrets/ViewableSecret.css | 5 ++++ src/loc/secrets/ViewableSecret.tsx | 18 ++++++++++++++ 10 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 src/components/toggle/Eye.css create mode 100644 src/img/eye-closed.svg create mode 100644 src/img/eye.svg create mode 100644 src/loc/secrets/RemoveSecretDialog.css create mode 100644 src/loc/secrets/ViewableSecret.css create mode 100644 src/loc/secrets/ViewableSecret.tsx diff --git a/src/components/toggle/Checkbox.tsx b/src/components/toggle/Checkbox.tsx index 9835e15b..08b6d6e3 100644 --- a/src/components/toggle/Checkbox.tsx +++ b/src/components/toggle/Checkbox.tsx @@ -1,8 +1,9 @@ import './Checkbox.css'; import './Toggle.css'; +import './Eye.css'; import { customClassName } from "../../common/types/Helpers"; -export type Skin = "Checkbox" | "Toggle white" | "Toggle black"; +export type Skin = "Checkbox" | "Toggle white" | "Toggle black" | "Eye"; export interface Props { checked: boolean; diff --git a/src/components/toggle/Eye.css b/src/components/toggle/Eye.css new file mode 100644 index 00000000..ba05d608 --- /dev/null +++ b/src/components/toggle/Eye.css @@ -0,0 +1,34 @@ +.Eye { + content: " "; + position: relative; + height: 37px; + width: 50px; +} + +.Eye.clickable { + cursor: pointer; +} + +.Eye.checked:after { + background-image: url("../../img/eye.svg"); +} +.Eye:not(.checked):after { + background-image: url("../../img/eye-closed.svg"); +} + +.Eye:after { + content: " "; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + background-repeat: no-repeat; + background-position: center; +} + +.Eye.disabled, +.Eye.clickable.disabled { + cursor: default; + opacity: 0.5; +} diff --git a/src/img/eye-closed.svg b/src/img/eye-closed.svg new file mode 100644 index 00000000..2c826d1e --- /dev/null +++ b/src/img/eye-closed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/img/eye.svg b/src/img/eye.svg new file mode 100644 index 00000000..d7de87cc --- /dev/null +++ b/src/img/eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/loc/secrets/AddSecretDialog.tsx b/src/loc/secrets/AddSecretDialog.tsx index 91d3e610..8dc82679 100644 --- a/src/loc/secrets/AddSecretDialog.tsx +++ b/src/loc/secrets/AddSecretDialog.tsx @@ -47,6 +47,7 @@ export default function AddSecretDialog(props: Props) { ] } onSubmit={ handleSubmit(submit) } > +

Add a Recoverable Secret

-

You are about to remove the secret { props.secret?.name }.

-

It will be impossible to recover it after, so make your own backup

+ +

You are about to remove the secret { props.secret?.name }:

+

It will be impossible to recover it after, so make your own backup of the value:

+ ) } diff --git a/src/loc/secrets/SecretsTable.tsx b/src/loc/secrets/SecretsTable.tsx index 7e6acd32..b8f56d65 100644 --- a/src/loc/secrets/SecretsTable.tsx +++ b/src/loc/secrets/SecretsTable.tsx @@ -1,6 +1,8 @@ import Table, { EmptyTableMessage, Cell, ActionCell } from "../../common/Table"; import { Secret } from "@logion/client"; import Button from "react-bootstrap/Button"; +import Icon from "../../common/Icon"; +import ViewableSecret from "./ViewableSecret"; export interface Properties { secrets: Secret[]; @@ -16,17 +18,24 @@ export default function SecretsTable(props: Properties) { columns={ [ { header: "Name", + width: "200px", render: secret => }, { header: "Value", - render: secret => + render: secret => }, { header: "", + width: "70px", render: secret => - + }, ] } diff --git a/src/loc/secrets/ViewableSecret.css b/src/loc/secrets/ViewableSecret.css new file mode 100644 index 00000000..512b4d51 --- /dev/null +++ b/src/loc/secrets/ViewableSecret.css @@ -0,0 +1,5 @@ +.ViewableSecret { + display: flex; + justify-content: center; + line-height: 40px; +} diff --git a/src/loc/secrets/ViewableSecret.tsx b/src/loc/secrets/ViewableSecret.tsx new file mode 100644 index 00000000..9fb59bc9 --- /dev/null +++ b/src/loc/secrets/ViewableSecret.tsx @@ -0,0 +1,18 @@ +import { useState } from "react"; +import Checkbox from "../../components/toggle/Checkbox"; +import "./ViewableSecret.css"; + +export interface Props { + value: string; +} + +export default function ViewableSecret(props: Props) { + const [ hidden, setHidden ] = useState(true); + return ( +
+ { !hidden && { props.value } } + { hidden && ****** } +
+ ) +} From bb893033e06df216df49195bf4f7dbabf084a4dd Mon Sep 17 00:00:00 2001 From: Benoit Devos Date: Thu, 16 May 2024 10:24:07 +0200 Subject: [PATCH 3/3] chore: remove react-hook-form from RemoveSecretDialog. logion-network/logion-internal#1253 --- src/loc/secrets/RemoveSecretDialog.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/loc/secrets/RemoveSecretDialog.tsx b/src/loc/secrets/RemoveSecretDialog.tsx index eb15584b..405bbb3c 100644 --- a/src/loc/secrets/RemoveSecretDialog.tsx +++ b/src/loc/secrets/RemoveSecretDialog.tsx @@ -1,7 +1,5 @@ import { Secret } from "@logion/client"; import Dialog from "../../common/Dialog"; -import { useForm } from "react-hook-form"; -import { useCallback } from "react"; import Icon from "../../common/Icon"; import ViewableSecret from "./ViewableSecret"; import "./RemoveSecretDialog.css" @@ -13,18 +11,6 @@ export interface Props { } export default function RemoveSecretDialog(props: Props) { - const { handleSubmit, reset } = useForm<{}>(); - - const cancel = useCallback(() => { - props.onCancel(); - reset(); - }, [ props, reset ]) - - const submit = useCallback((_: {}) => { - props.onRemoveSecret(props.secret!); - reset(); - }, [ props, reset ]) - return ( props.onRemoveSecret(props.secret!), } ] } - onSubmit={ handleSubmit(submit) } >

You are about to remove the secret { props.secret?.name }: