Skip to content

Commit

Permalink
Allow the selection of a custom IPFS gateway (#557)
Browse files Browse the repository at this point in the history
  • Loading branch information
amonsosanz authored Nov 23, 2023
1 parent 3d0cc3a commit fcbb8b9
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 85 deletions.
10 changes: 8 additions & 2 deletions ui/src/adapters/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { Response, buildErrorResponse, buildSuccessResponse } from "src/adapters
import { ID, IDParser, buildAuthorizationHeader } from "src/adapters/api";
import { datetimeParser, getListParser, getStrictParser } from "src/adapters/parsers";
import { ApiSchema, Env, JsonLdType } from "src/domain";
import { API_VERSION, QUERY_SEARCH_PARAM } from "src/utils/constants";
import { getStorageByKey } from "src/utils/browser";
import { API_VERSION, IPFS_CUSTOM_GATEWAY_KEY, QUERY_SEARCH_PARAM } from "src/utils/constants";
import { List } from "src/utils/types";

type ApiSchemaInput = Omit<ApiSchema, "createdAt"> & {
Expand Down Expand Up @@ -126,10 +127,15 @@ export async function getApiSchemas({
}

export const getIPFSGatewayUrl = (env: Env, ipfsUrl: string): Response<string> => {
const ipfsGatewayUrl = getStorageByKey({
defaultValue: env.ipfsGatewayUrl,
key: IPFS_CUSTOM_GATEWAY_KEY,
parser: z.string().url(),
});
const cid = ipfsUrl.split("ipfs://")[1];

return cid !== undefined
? buildSuccessResponse(`${env.ipfsGatewayUrl}/ipfs/${cid}`)
? buildSuccessResponse(`${ipfsGatewayUrl}/ipfs/${cid}`)
: buildErrorResponse("Invalid IPFS URL");
};

Expand Down
4 changes: 4 additions & 0 deletions ui/src/assets/icons/settings-01.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 120 additions & 0 deletions ui/src/components/shared/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Form, Input, Modal, Typography } from "antd";
import { useState } from "react";
import { z } from "zod";

import IconClose from "src/assets/icons/x.svg?react";
import { useEnvContext } from "src/contexts/Env";
import { getStorageByKey, setStorageByKey } from "src/utils/browser";
import {
CLOSE,
IPFS_CUSTOM_GATEWAY_KEY,
IPFS_PUBLIC_GATEWAY_CHECKER_URL,
SAVE,
URL_FIELD_ERROR_MESSAGE,
} from "src/utils/constants";

export function SettingsModal({
afterOpenChange,
isOpen,
onClose,
onSave,
}: {
afterOpenChange: (isOpen: boolean) => void;
isOpen: boolean;
onClose: () => void;
onSave: () => void;
}) {
const env = useEnvContext();
const [form] = Form.useForm();

const defaultIpfsGatewayUrl = getStorageByKey({
defaultValue: env.ipfsGatewayUrl,
key: IPFS_CUSTOM_GATEWAY_KEY,
parser: z.string().url(),
});

const [areSettingsValid, setAreSettingsValid] = useState<boolean>(false);

const onResetToDefault = () => {
form.setFieldValue("ipfsGatewayUrl", env.ipfsGatewayUrl);
void form.validateFields(["ipfsGatewayUrl"]);
};

const onSaveClick = () => {
const parsedValue = z.string().url().safeParse(form.getFieldValue("ipfsGatewayUrl"));
if (parsedValue.success) {
const value = parsedValue.data.trim();
const sanitizedValue = value.endsWith("/") ? value.slice(0, -1) : value;
setStorageByKey({
key: IPFS_CUSTOM_GATEWAY_KEY,
value: sanitizedValue,
});
onSave();
}
};

const onFormChange = () => {
setAreSettingsValid(!form.getFieldsError().some(({ errors }) => errors.length));
};

return (
<Modal
afterOpenChange={afterOpenChange}
cancelText={CLOSE}
centered
closable
closeIcon={<IconClose />}
maskClosable
okButtonProps={{ disabled: !areSettingsValid }}
okText={SAVE}
onCancel={onClose}
onOk={onSaveClick}
open={isOpen}
style={{ maxWidth: 400 }}
title="Change IPFS gateway"
>
<Form
form={form}
initialValues={{
ipfsGatewayUrl: defaultIpfsGatewayUrl,
}}
layout="vertical"
onFieldsChange={onFormChange}
>
<Typography.Paragraph style={{ whiteSpace: "pre-line" }} type="secondary">
The IPFS gateway makes it possible to access files hosted on IPFS. You can customize the
IPFS gateway or continue using the default.{"\n"}
{"\n"}You can use sites like the{" "}
<Typography.Link href={IPFS_PUBLIC_GATEWAY_CHECKER_URL} target="_blank">
IPFS Public Gateway Checker
</Typography.Link>{" "}
to choose a custom gateway.
</Typography.Paragraph>
<Form.Item
extra={
<Typography.Link
onClick={onResetToDefault}
style={{ display: "block", textAlign: "right" }}
>
Reset to default
</Typography.Link>
}
label="IPFS gateway URL"
name="ipfsGatewayUrl"
required
rules={[
{
required: true,
},
{
message: URL_FIELD_ERROR_MESSAGE,
validator: (_, value) => z.string().url().parseAsync(value),
},
]}
>
<Input placeholder={env.ipfsGatewayUrl} />
</Form.Item>
</Form>
</Modal>
);
}
205 changes: 122 additions & 83 deletions ui/src/components/shared/SiderMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Col, Divider, Menu, Row, Space, Tag, Typography } from "antd";
import { Col, Divider, Menu, Row, Space, Tag, Typography, message } from "antd";
import { useState } from "react";
import { generatePath, matchRoutes, useLocation, useNavigate } from "react-router-dom";

import IconCredentials from "src/assets/icons/credit-card-refresh.svg?react";
import IconFile from "src/assets/icons/file-05.svg?react";
import IconSchema from "src/assets/icons/file-search-02.svg?react";
import IconLink from "src/assets/icons/link-external-01.svg?react";
import IconSettings from "src/assets/icons/settings-01.svg?react";
import IconIssuerState from "src/assets/icons/switch-horizontal.svg?react";
import IconConnections from "src/assets/icons/users-01.svg?react";
import { LogoLink } from "src/components/shared/LogoLink";
import { SettingsModal } from "src/components/shared/SettingsModal";
import { UserDisplay } from "src/components/shared/UserDisplay";
import { useEnvContext } from "src/contexts/Env";
import { useIssuerStateContext } from "src/contexts/IssuerState";
Expand All @@ -33,6 +36,9 @@ export function SiderMenu({
const { status } = useIssuerStateContext();
const { pathname } = useLocation();
const navigate = useNavigate();
const [messageAPI, messageContext] = message.useMessage();
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);
const [isSettingsModalMounted, setIsSettingsModalMounted] = useState<boolean>(false);

const connectionsPath = ROUTES.connections.path;
const credentialsPath = ROUTES.credentials.path;
Expand Down Expand Up @@ -80,94 +86,127 @@ export function SiderMenu({
};

return (
<Row
className="menu-sider-layout"
justify="space-between"
style={{
padding: isBreakpoint ? "32px 24px" : "96px 24px 32px",
}}
>
<Col>
<UserDisplay />
<>
{messageContext}

<Divider />
<Row
className="menu-sider-layout"
justify="space-between"
style={{
padding: isBreakpoint ? "32px 24px" : "96px 24px 32px",
}}
>
<Col>
<UserDisplay />

<Menu
items={[
{
icon: <IconSchema />,
key: schemasPath,
label: SCHEMAS,
onClick: () => onMenuClick(schemasPath),
title: "",
},
{
icon: <IconCredentials />,
key: credentialsPath,
label: CREDENTIALS,
onClick: () =>
onMenuClick(
generatePath(credentialsPath, {
tabID: CREDENTIALS_TABS[0].tabID,
})
<Divider />

<Menu
items={[
{
icon: <IconSchema />,
key: schemasPath,
label: SCHEMAS,
onClick: () => onMenuClick(schemasPath),
title: "",
},
{
icon: <IconCredentials />,
key: credentialsPath,
label: CREDENTIALS,
onClick: () =>
onMenuClick(
generatePath(credentialsPath, {
tabID: CREDENTIALS_TABS[0].tabID,
})
),
title: "",
},
{
icon: <IconConnections />,
key: connectionsPath,
label: CONNECTIONS,
onClick: () => onMenuClick(connectionsPath),
title: "",
},
{
icon: <IconIssuerState />,
key: issuerStatePath,
label:
isAsyncTaskDataAvailable(status) && status.data ? (
<Space>
{ISSUER_STATE}
<Tag color="purple" style={{ fontSize: 12 }}>
Pending actions
</Tag>
</Space>
) : (
ISSUER_STATE
),
onClick: () => onMenuClick(issuerStatePath),
title: "",
},
]}
selectedKeys={getSelectedKey()}
/>
</Col>

<Space direction="vertical" size={40}>
<Menu
items={[
{
icon: <IconSettings />,
key: "settings",
label: (
<Typography.Link
onClick={() => {
setIsSettingsModalOpen(true);
setIsSettingsModalMounted(true);
}}
>
<Row justify="space-between">Settings</Row>
</Typography.Link>
),
title: "",
},
{
icon: <IconConnections />,
key: connectionsPath,
label: CONNECTIONS,
onClick: () => onMenuClick(connectionsPath),
title: "",
},
{
icon: <IconIssuerState />,
key: issuerStatePath,
label:
isAsyncTaskDataAvailable(status) && status.data ? (
<Space>
{ISSUER_STATE}
<Tag color="purple" style={{ fontSize: 12 }}>
Pending actions
</Tag>
</Space>
) : (
ISSUER_STATE
},
{
icon: <IconFile />,
key: "documentation",
label: (
<Typography.Link href={DOCS_URL} target="_blank">
<Row justify="space-between">
<span>Documentation</span>

<IconLink className="icon-secondary" height={16} />
</Row>
</Typography.Link>
),
onClick: () => onMenuClick(issuerStatePath),
title: "",
},
]}
selectedKeys={getSelectedKey()}
/>
</Col>
},
]}
selectable={false}
/>
{isBreakpoint && (
<Space>
<LogoLink />

<Space direction="vertical" size={40}>
<Menu
items={[
{
icon: <IconFile />,
key: "documentation",
label: (
<Typography.Link href={DOCS_URL} target="_blank">
<Row justify="space-between">
<span>Documentation</span>
{buildTag && <Tag>{buildTag}</Tag>}
</Space>
)}
</Space>
</Row>

<IconLink className="icon-secondary" height={16} />
</Row>
</Typography.Link>
),
},
]}
{isSettingsModalMounted && (
<SettingsModal
afterOpenChange={setIsSettingsModalMounted}
isOpen={isSettingsModalOpen}
onClose={() => {
setIsSettingsModalOpen(false);
}}
onSave={() => {
setIsSettingsModalOpen(false);
void messageAPI.success("IPFS gateway successfully changed");
}}
/>
{isBreakpoint && (
<Space>
<LogoLink />

{buildTag && <Tag>{buildTag}</Tag>}
</Space>
)}
</Space>
</Row>
)}
</>
);
}
Loading

0 comments on commit fcbb8b9

Please sign in to comment.