From fa1297bf087062200403bd1dd2f5e54a906a431c Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Fri, 5 Jan 2024 12:58:38 -0500 Subject: [PATCH] feat: share config --- .changeset/many-tigers-guess.md | 6 ++ .../components/Config/AnnounceButton.tsx | 6 +- .../hostd/components/Config/ConfigActions.tsx | 37 ++++++++++++ .../components/Config/ConfigContextMenu.tsx | 49 ++++++++++++++++ apps/hostd/components/Config/index.tsx | 56 ++++--------------- apps/hostd/contexts/config/index.tsx | 18 +++++- .../contexts/metrics/useNowAtInterval.tsx | 1 - .../components/Config/ConfigActions.tsx | 2 + .../components/Config/ConfigContextMenu.tsx | 49 ++++++++++++++++ apps/renterd/components/Config/index.tsx | 7 ++- apps/renterd/contexts/config/index.tsx | 18 +++++- libs/design-system/package.json | 1 + libs/design-system/src/index.ts | 1 + libs/design-system/src/lib/clipboard.ts | 14 ++++- libs/design-system/src/lib/nodeToImage.tsx | 33 +++++++++++ package-lock.json | 31 +++++----- package.json | 1 + 17 files changed, 257 insertions(+), 73 deletions(-) create mode 100644 .changeset/many-tigers-guess.md create mode 100644 apps/hostd/components/Config/ConfigActions.tsx create mode 100644 apps/hostd/components/Config/ConfigContextMenu.tsx create mode 100644 apps/renterd/components/Config/ConfigContextMenu.tsx create mode 100644 libs/design-system/src/lib/nodeToImage.tsx diff --git a/.changeset/many-tigers-guess.md b/.changeset/many-tigers-guess.md new file mode 100644 index 000000000..27f171ffb --- /dev/null +++ b/.changeset/many-tigers-guess.md @@ -0,0 +1,6 @@ +--- +'hostd': minor +'renterd': minor +--- + +The configuration page now has menu options to download or copy an image of the current configuration for easier sharing. diff --git a/apps/hostd/components/Config/AnnounceButton.tsx b/apps/hostd/components/Config/AnnounceButton.tsx index 582d7d984..7eacd4052 100644 --- a/apps/hostd/components/Config/AnnounceButton.tsx +++ b/apps/hostd/components/Config/AnnounceButton.tsx @@ -54,11 +54,7 @@ export function AnnounceButton() { ) return ( - diff --git a/apps/hostd/components/Config/ConfigActions.tsx b/apps/hostd/components/Config/ConfigActions.tsx new file mode 100644 index 000000000..b929fb339 --- /dev/null +++ b/apps/hostd/components/Config/ConfigActions.tsx @@ -0,0 +1,37 @@ +import { Text, Button } from '@siafoundation/design-system' +import { Reset16, Save16 } from '@siafoundation/react-icons' +import { AnnounceButton } from './AnnounceButton' +import { useConfig } from '../../contexts/config' +import { ConfigContextMenu } from './ConfigContextMenu' + +export function ConfigActions() { + const { changeCount, revalidateAndResetForm, form, onSubmit } = useConfig() + return ( +
+ {!!changeCount && ( + + {changeCount === 1 ? '1 change' : `${changeCount} changes`} + + )} + + + + +
+ ) +} diff --git a/apps/hostd/components/Config/ConfigContextMenu.tsx b/apps/hostd/components/Config/ConfigContextMenu.tsx new file mode 100644 index 000000000..efdd7e37e --- /dev/null +++ b/apps/hostd/components/Config/ConfigContextMenu.tsx @@ -0,0 +1,49 @@ +import { + DropdownMenu, + DropdownMenuItem, + Button, + DropdownMenuLeftSlot, + DropdownMenuLabel, +} from '@siafoundation/design-system' +import { + Copy16, + Download16, + OverflowMenuHorizontal16, +} from '@siafoundation/react-icons' +import { useConfig } from '../../contexts/config' + +export function ConfigContextMenu() { + const { takeScreenshot } = useConfig() + return ( + + + + } + contentProps={{ align: 'end' }} + > + Actions + { + takeScreenshot({ name: 'config image', copy: true }) + }} + > + + + + Copy image of configuration + + { + takeScreenshot({ name: 'config', download: true }) + }} + > + + + + Download image of configuration + + + ) +} diff --git a/apps/hostd/components/Config/index.tsx b/apps/hostd/components/Config/index.tsx index a33b3133d..1eb49aee3 100644 --- a/apps/hostd/components/Config/index.tsx +++ b/apps/hostd/components/Config/index.tsx @@ -1,31 +1,18 @@ -import { Text, Button, ConfigurationPanel } from '@siafoundation/design-system' -import { - Reset16, - Save16, - Warning16, - CheckmarkFilled16, -} from '@siafoundation/react-icons' +import { Text, ConfigurationPanel } from '@siafoundation/design-system' +import { Warning16, CheckmarkFilled16 } from '@siafoundation/react-icons' import { HostdSidenav } from '../HostdSidenav' import { routes } from '../../config/routes' import { useDialog } from '../../contexts/dialog' import { HostdAuthedLayout } from '../../components/HostdAuthedLayout' -import { AnnounceButton } from './AnnounceButton' import { useConfig } from '../../contexts/config' import { ConfigNav } from './ConfigNav' import { StateConnError } from './StateConnError' +import { ConfigActions } from './ConfigActions' export function Config() { const { openDialog } = useDialog() - const { - fields, - settings, - dynDNSCheck, - changeCount, - revalidateAndResetForm, - form, - onSubmit, - remoteError, - } = useConfig() + const { fields, settings, dynDNSCheck, form, remoteError, configRef } = + useConfig() return ( - {!!changeCount && ( - - {changeCount === 1 ? '1 change' : `${changeCount} changes`} - - )} - - - - - } + actions={} openSettings={() => openDialog('settings')} > {remoteError ? ( ) : ( -
+
{ + nodeToImage(configRef.current, props) + }, + [] + ) + return { fields, settings, @@ -96,6 +110,8 @@ export function useConfigMain() { showAdvanced, setShowAdvanced, remoteError, + takeScreenshot, + configRef, } } diff --git a/apps/hostd/contexts/metrics/useNowAtInterval.tsx b/apps/hostd/contexts/metrics/useNowAtInterval.tsx index 2dcc4223c..1ff91aa93 100644 --- a/apps/hostd/contexts/metrics/useNowAtInterval.tsx +++ b/apps/hostd/contexts/metrics/useNowAtInterval.tsx @@ -9,7 +9,6 @@ export function useNowAtInterval(dataInterval: DataInterval) { setNow(new Date().getTime()) const i = setInterval(() => { setNow(new Date().getTime()) - console.log('reset time range') }, getDataIntervalInMs(dataInterval)) return () => clearInterval(i) }, [dataInterval]) diff --git a/apps/renterd/components/Config/ConfigActions.tsx b/apps/renterd/components/Config/ConfigActions.tsx index 59e6c1dbb..43d3c66d6 100644 --- a/apps/renterd/components/Config/ConfigActions.tsx +++ b/apps/renterd/components/Config/ConfigActions.tsx @@ -9,6 +9,7 @@ import { } from '@siafoundation/design-system' import { Reset16, Save16, Settings16 } from '@siafoundation/react-icons' import { useConfig } from '../../contexts/config' +import { ConfigContextMenu } from './ConfigContextMenu' export function ConfigActions() { const { @@ -72,6 +73,7 @@ export function ConfigActions() {
+
) } diff --git a/apps/renterd/components/Config/ConfigContextMenu.tsx b/apps/renterd/components/Config/ConfigContextMenu.tsx new file mode 100644 index 000000000..efdd7e37e --- /dev/null +++ b/apps/renterd/components/Config/ConfigContextMenu.tsx @@ -0,0 +1,49 @@ +import { + DropdownMenu, + DropdownMenuItem, + Button, + DropdownMenuLeftSlot, + DropdownMenuLabel, +} from '@siafoundation/design-system' +import { + Copy16, + Download16, + OverflowMenuHorizontal16, +} from '@siafoundation/react-icons' +import { useConfig } from '../../contexts/config' + +export function ConfigContextMenu() { + const { takeScreenshot } = useConfig() + return ( + + + + } + contentProps={{ align: 'end' }} + > + Actions + { + takeScreenshot({ name: 'config image', copy: true }) + }} + > + + + + Copy image of configuration + + { + takeScreenshot({ name: 'config', download: true }) + }} + > + + + + Download image of configuration + + + ) +} diff --git a/apps/renterd/components/Config/index.tsx b/apps/renterd/components/Config/index.tsx index 9f796a0f0..7f04c4030 100644 --- a/apps/renterd/components/Config/index.tsx +++ b/apps/renterd/components/Config/index.tsx @@ -11,7 +11,7 @@ import { StateConnError } from './StateConnError' export function Config() { const { openDialog } = useDialog() - const { form, fields, remoteError } = useConfig() + const { form, fields, remoteError, configRef } = useConfig() return ( ) : ( -
+
{ + nodeToImage(configRef.current, props) + }, + [] + ) + return { onSubmit, revalidateAndResetForm, @@ -215,6 +229,8 @@ export function useConfigMain() { showAdvanced, setShowAdvanced, remoteError, + configRef, + takeScreenshot, } } diff --git a/libs/design-system/package.json b/libs/design-system/package.json index b354dd92d..5521b60e1 100644 --- a/libs/design-system/package.json +++ b/libs/design-system/package.json @@ -50,6 +50,7 @@ "clipboard-polyfill": "^4.0.1", "@visx/xychart": "^2.18.0", "react-dropzone": "^14.2.3", + "html-to-image": "^1.11.11", "react-number-format": "^5.3.1", "@radix-ui/react-radio-group": "^1.0.0", "@radix-ui/react-accordion": "^1.0.0", diff --git a/libs/design-system/src/index.ts b/libs/design-system/src/index.ts index 7bf296ac6..c58d506c0 100644 --- a/libs/design-system/src/index.ts +++ b/libs/design-system/src/index.ts @@ -177,5 +177,6 @@ export * from './lib/chartStats' export * from './lib/ipRegex' export * from './lib/getOs' export * from './lib/countryEmoji' +export * from './lib/nodeToImage' export { colors } from './config/colors' diff --git a/libs/design-system/src/lib/clipboard.ts b/libs/design-system/src/lib/clipboard.ts index d244cd440..e088403cc 100644 --- a/libs/design-system/src/lib/clipboard.ts +++ b/libs/design-system/src/lib/clipboard.ts @@ -1,5 +1,5 @@ import React from 'react' -import { writeText } from 'clipboard-polyfill' +import { writeText, write } from 'clipboard-polyfill' import { ToastOptions, triggerToast, triggerToastNode } from './toast' export const copyToClipboard = (text: string, entity?: string) => { @@ -10,6 +10,18 @@ export const copyToClipboard = (text: string, entity?: string) => { writeText(text) } +export const copyImageToClipboard = ( + image: Blob, + type: string, + entity?: string +) => { + const message = entity + ? `Copied ${entity} to clipboard` + : 'Copied to clipboard' + triggerToast(message) + write([new ClipboardItem({ [type]: image })]) +} + export const copyToClipboardCustom = ( text: string, message: React.ReactNode, diff --git a/libs/design-system/src/lib/nodeToImage.tsx b/libs/design-system/src/lib/nodeToImage.tsx new file mode 100644 index 000000000..ce39bd751 --- /dev/null +++ b/libs/design-system/src/lib/nodeToImage.tsx @@ -0,0 +1,33 @@ +import * as htmlToImage from 'html-to-image' +import { copyImageToClipboard } from './clipboard' + +export async function nodeToImage( + node: HTMLElement, + { + name, + quality, + copy, + download, + }: { + name: string + quality?: number + copy?: boolean + download?: boolean + } +) { + if (!node) { + throw Error('HTML node required') + } + const dataUrl = await htmlToImage.toPng(node, { quality: quality || 0.5 }) + if (download) { + const link = document.createElement('a') + link.download = `${name}.png` + link.href = dataUrl + link.click() + } + if (copy) { + const fetchResponse = await fetch(dataUrl) + const blob = await fetchResponse.blob() + copyImageToClipboard(blob, 'image/png', name) + } +} diff --git a/package-lock.json b/package-lock.json index 0f2ec5dec..e65421cb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "formik": "^2.2.9", "framer-motion": "^7.6.5", "gray-matter": "^4.0.3", + "html-to-image": "^1.11.11", "identicon.js": "^2.3.3", "jest-environment-jsdom": "29.4.3", "lowdb": "^3.0.0", @@ -302,11 +303,12 @@ "date-fns": "^2.28.0", "formik": "^2.2.9", "framer-motion": "^7.6.5", + "html-to-image": "^1.11.11", "next-themes": "^0.2.1", - "react-currency-input-field": "^3.6.5", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.2.0", "react-idle-timer": "^5.7.2", + "react-number-format": "^5.3.1", "react-qr-code": "^2.0.7", "yup": "^0.32.11" }, @@ -15824,6 +15826,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-to-image": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", + "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==" + }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -22334,14 +22341,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-currency-input-field": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/react-currency-input-field/-/react-currency-input-field-3.6.5.tgz", - "integrity": "sha512-tP62WFAhkVv0RGOQVGEPE3MfgciQ4gkiASlmLBDu6tdfMC3jFhIkLU1uqUy3glUvRHyT4avnX/YVVbbXKHLtnA==", - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -33295,11 +33294,12 @@ "date-fns": "^2.28.0", "formik": "^2.2.9", "framer-motion": "^7.6.5", + "html-to-image": "^1.11.11", "next-themes": "^0.2.1", - "react-currency-input-field": "^3.6.5", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.2.0", "react-idle-timer": "^5.7.2", + "react-number-format": "^5.3.1", "react-qr-code": "^2.0.7", "yup": "^0.32.11" } @@ -39596,6 +39596,11 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-to-image": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", + "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==" + }, "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -44138,12 +44143,6 @@ "loose-envify": "^1.1.0" } }, - "react-currency-input-field": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/react-currency-input-field/-/react-currency-input-field-3.6.5.tgz", - "integrity": "sha512-tP62WFAhkVv0RGOQVGEPE3MfgciQ4gkiASlmLBDu6tdfMC3jFhIkLU1uqUy3glUvRHyT4avnX/YVVbbXKHLtnA==", - "requires": {} - }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/package.json b/package.json index 6e14938ac..5c0f1c5d0 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "formik": "^2.2.9", "framer-motion": "^7.6.5", "gray-matter": "^4.0.3", + "html-to-image": "^1.11.11", "identicon.js": "^2.3.3", "jest-environment-jsdom": "29.4.3", "lowdb": "^3.0.0",