diff --git a/web/package.json b/web/package.json index 384c2d9e..0cd2c7f4 100644 --- a/web/package.json +++ b/web/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@apollo/client": "^3.9.9", - "@dolthub/react-components": "^0.2.6", + "@dolthub/react-components": "^0.2.8", "@dolthub/react-contexts": "^0.1.0", "@dolthub/react-hooks": "^0.1.7", "@dolthub/web-utils": "^0.1.8", diff --git a/web/renderer/components/ConnectionsAndDatabases/Popup.tsx b/web/renderer/components/ConnectionsAndDatabases/Popup.tsx index 0ba0327f..979e0911 100644 --- a/web/renderer/components/ConnectionsAndDatabases/Popup.tsx +++ b/web/renderer/components/ConnectionsAndDatabases/Popup.tsx @@ -4,6 +4,8 @@ import cx from "classnames"; import Link from "@components/links/Link"; import { ErrorMsg, SmallLoader } from "@dolthub/react-components"; import CreateDatabase from "@components/CreateDatabase"; +import { AiOutlinePlusCircle } from "@react-icons/all-files/ai/AiOutlinePlusCircle"; +import { connections, newConnection } from "@lib/urls"; import { FiTool } from "@react-icons/all-files/fi/FiTool"; import { StateType } from "./useSelectedConnection"; import DatabaseItem from "./DatabaseItem"; @@ -30,9 +32,14 @@ export default function Popup({
CONNECTIONS - - - +
+ + + + + + +
DATABASES diff --git a/web/renderer/components/ConnectionsAndDatabases/index.module.css b/web/renderer/components/ConnectionsAndDatabases/index.module.css index c5e7d67c..b3cd5822 100644 --- a/web/renderer/components/ConnectionsAndDatabases/index.module.css +++ b/web/renderer/components/ConnectionsAndDatabases/index.module.css @@ -54,8 +54,16 @@ @apply border-none rounded-bl; } +.icons { + @apply flex; +} + .wrench { - @apply w-4 h-4 border border-sky-400 rounded-full p-[0.1rem] -rotate-90; + @apply w-5 h-5 border border-sky-400 rounded-full p-[0.1rem] -rotate-90; +} + +.top .plus { + @apply ml-2 w-[21px] h-[21px]; } .connectionTop { diff --git a/web/renderer/components/ConnectionsAndDatabases/index.tsx b/web/renderer/components/ConnectionsAndDatabases/index.tsx index ba96f399..14b19057 100644 --- a/web/renderer/components/ConnectionsAndDatabases/index.tsx +++ b/web/renderer/components/ConnectionsAndDatabases/index.tsx @@ -9,6 +9,7 @@ import { import Link from "@components/links/Link"; import { FiDatabase } from "@react-icons/all-files/fi/FiDatabase"; import { excerpt } from "@dolthub/web-utils"; +import { connections } from "@lib/urls"; import cx from "classnames"; import useSelectedConnection from "./useSelectedConnection"; import Popup from "./Popup"; @@ -84,7 +85,7 @@ export default function ConnectionsAndDatabases(props: Props) { data.currentConnection ? ( ) : ( - + Connections ) diff --git a/web/renderer/components/CreateDatabase/index.tsx b/web/renderer/components/CreateDatabase/index.tsx index 1106b18b..d7554217 100644 --- a/web/renderer/components/CreateDatabase/index.tsx +++ b/web/renderer/components/CreateDatabase/index.tsx @@ -8,7 +8,6 @@ import { import useMutation from "@hooks/useMutation"; import { database } from "@lib/urls"; import { AiOutlinePlusCircle } from "@react-icons/all-files/ai/AiOutlinePlusCircle"; -import { AiOutlinePlus } from "@react-icons/all-files/ai/AiOutlinePlus"; import cx from "classnames"; import { useRouter } from "next/router"; import { SyntheticEvent, useState } from "react"; @@ -56,8 +55,11 @@ export default function CreateDatabase(props: Props) { onClick={() => setIsOpen(true)} className={cx(css.createDB, props.buttonClassName)} > - {props.showText ? : } - {props.showText && Create database} + {props.showText ? ( + Create Database + ) : ( + + )} - {props.showLabel && Branch} + {props.showLabel && Branch:}
} bgColor="bg-storm-600" > - Connections + Connections diff --git a/web/renderer/components/SchemaDiagramButton/index.module.css b/web/renderer/components/SchemaDiagramButton/index.module.css index 01efe7bb..69ac4a5c 100644 --- a/web/renderer/components/SchemaDiagramButton/index.module.css +++ b/web/renderer/components/SchemaDiagramButton/index.module.css @@ -1,5 +1,5 @@ .diagram { - @apply flex text-white text-base bg-[#FF8964] px-6 items-center; + @apply flex text-white text-base bg-coral-300 px-6 items-center; &:hover { @apply bg-coral-400; } diff --git a/web/renderer/components/TableList/index.module.css b/web/renderer/components/TableList/index.module.css index 71d60aec..33cab643 100644 --- a/web/renderer/components/TableList/index.module.css +++ b/web/renderer/components/TableList/index.module.css @@ -8,7 +8,7 @@ @apply flex my-8 mx-3 text-sm text-white font-normal; &:hover { - @apply text-[#FF8964]; + @apply text-coral-300; } svg { diff --git a/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/Item.tsx b/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/Item.tsx index c38282c1..f7b826c1 100644 --- a/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/Item.tsx +++ b/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/Item.tsx @@ -2,42 +2,62 @@ import { getDatabaseType } from "@components/DatabaseTypeLabel"; import { Button, ErrorMsg, Loader } from "@dolthub/react-components"; import { DatabaseConnectionFragment } from "@gen/graphql-types"; import { IoMdClose } from "@react-icons/all-files/io/IoMdClose"; +import Image from "next/legacy/image"; import cx from "classnames"; +import { DatabaseTypeLabel } from "@components/ConnectionsAndDatabases/DatabaseTypeLabel"; import useAddConnection from "./useAddConnection"; import css from "./index.module.css"; type Props = { conn: DatabaseConnectionFragment; - onDeleteClicked?: (n: string) => void; + onDeleteClicked: (n: string) => void; + borderClassName: string; + shorterLine: boolean; }; -export default function Item({ conn, onDeleteClicked }: Props) { +export default function Item({ + conn, + onDeleteClicked, + borderClassName, + shorterLine, +}: Props) { const { onAdd, err, loading } = useAddConnection(conn); - + const type = getDatabaseType(conn.type ?? undefined, !!conn.isDolt); return ( <> -
  • - {conn.name} - - - {onDeleteClicked && ( - onDeleteClicked(conn.name)}> +
  • +
    +
    +
    + DatabaseIcon +
    +
    + onDeleteClicked(conn.name)} + className={css.delete} + > - )} - +
    + + + {conn.name} + +
    +
    +
  • ); } - -type LabelProps = { - conn: DatabaseConnectionFragment; -}; - -export function DatabaseTypeLabel({ conn }: LabelProps) { - const type = getDatabaseType(conn.type ?? undefined, !!conn.isDolt); - return {type}; -} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/index.module.css b/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/index.module.css index e065b050..37221781 100644 --- a/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/index.module.css +++ b/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/index.module.css @@ -1,55 +1,145 @@ -.whiteContainer { - @apply max-w-xl w-full border rounded-lg py-10 px-16 bg-white mx-auto; +.outer { + @apply max-w-5xl mt-16 mb-10 flex items-center relative w-fit; } -.options { - @apply my-6; - - ul { - @apply mb-8; +.text { + @apply absolute top-0 left-0; + h1 { + @apply text-left; + } + p { + @apply max-w-md text-lg my-4; + } + a { + @apply text-coral-300; + } + a:hover { + @apply text-coral-400; } } +.connections { + @apply flex items-center; +} + +.outerEllipse { + background: radial-gradient( + circle, + rgba(239, 242, 243, 0.3) 0%, + rgba(25, 46, 61, 0.12) 100% + ); + @apply w-32 h-32 rounded-full z-10 flex justify-center items-center flex-shrink-0; +} + +.innerEllipse { + background: radial-gradient( + circle, + rgba(239, 242, 243, 0.6) 0%, + rgba(25, 46, 61, 0.24) 100% + ); + @apply w-24 h-24 rounded-full z-20 flex justify-center items-center; +} + +.dLogo { + @apply w-16 h-16 z-30; +} + +.dLogo::after { + content: ""; + @apply w-[46%] border-t border-stone-100 inline-block align-middle relative; +} + +.connectionContainer { + @apply flex relative; +} + +.line { + @apply w-16 h-36 absolute; +} + +.shorterLine { + @apply h-20; +} + +.roundedTop { + @apply border-l-2 border-t-2 border-white rounded-tl-[2rem] top-14; +} + +.roundedBottom { + @apply border-l-2 border-b-2 border-white rounded-bl-[2rem] bottom-14; +} + +.straightLine { + @apply border-b-2 border-white; + bottom: calc(3.75rem - 1px); +} + .connection { - @apply my-3 border rounded py-1.5 px-3 flex justify-between; + @apply my-3 border rounded flex w-80 h-24 ml-16; button { @apply text-base; } } +.left { + @apply w-24 flex justify-center items-center rounded-l; +} + +.dolt { + background: linear-gradient(#0dc1a0 0%, #29e3c1 100%); +} + +.mysql { + background: linear-gradient(#084550 0%, #00758f 100%); +} + +.doltgresql { + background: linear-gradient(#d34f26 0%, #ff8964 100%); +} + +.postgresql { + background: linear-gradient(#286288 0%, #367faf 100%); +} + .right { - @apply flex; + @apply relative w-56 bg-white rounded-r; } -.err { - @apply text-center; +.delete { + @apply absolute top-2 right-2 text-coral-400; } -.newConnection { - @apply flex items-center px-4; +.delete:hover { + @apply text-coral-300; +} - svg { - @apply mr-2; - } +.err { + @apply text-center; } -.label { - @apply text-sm w-24 rounded-full flex items-center justify-center mr-5; +.leftLine { + @apply w-36 border-t-2 border-white absolute left-24; } -.mysql { - @apply text-[#00758f] bg-[#00758f]/20; +.rightLine { + @apply w-28 border-t-2 border-white; } -.dolt { - @apply bg-mint-50 text-green-500; +.newConnection { + @apply flex items-center px-8 h-10 bg-coral-400 ml-28 min-w-40; + &:hover { + @apply bg-coral-300; + } } -.doltgresql { - @apply bg-orange-50 text-orange-500; +.typeAndName { + @apply pl-2 h-full flex flex-col justify-center items-start; } -.postgresql { - @apply bg-[#0064a5]/20 text-[#0064a5]; +.name { + @apply pl-4 pt-2 text-sky-400; +} +.label { + @apply text-sm; } diff --git a/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/index.tsx b/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/index.tsx index 700caee6..985bd11c 100644 --- a/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/index.tsx +++ b/web/renderer/components/pageComponents/ConnectionsPage/ExistingConnections/index.tsx @@ -5,14 +5,16 @@ import { StoredConnectionsDocument, useRemoveConnectionMutation, } from "@gen/graphql-types"; -import { AiOutlinePlus } from "@react-icons/all-files/ai/AiOutlinePlus"; +import { useRouter } from "next/router"; +import DoltLink from "@components/links/DoltLink"; +import DoltgresLink from "@components/links/DoltgresLink"; import { useState } from "react"; +import { newConnection } from "@lib/urls"; import Item from "./Item"; import css from "./index.module.css"; type Props = { connections: DatabaseConnectionFragment[]; - setShowForm: (s: boolean) => void; }; export default function ExistingConnections(props: Props) { @@ -24,37 +26,79 @@ export default function ExistingConnections(props: Props) { setDeleteModalOpen(true); }; + const router = useRouter(); + const onClick = () => { + const { href, as } = newConnection; + router.push(href, as).catch(console.error); + }; + + const halfHeight = Math.floor(props.connections.length / 2) * 5; + const marginTop = halfHeight < 10 ? `${12 - halfHeight}rem` : "0"; + return ( -
    -

    Connections

    -
    +
    +
    +

    Connections

    +

    + Connect the workbench to any MySQL or PostgreSQL compatible database. + Use or to unlock version control + features. +

    +
    +
    +
    +
    + Dolt Logo +
    +
    +
    + +
      - {props.connections.map(conn => ( + {props.connections.map((conn, i) => ( ))}
    - -
    +
    ); } + +function getBorderLineClassName(n: number, ind: number): string { + if (n % 2 === 0) { + return ind < n / 2 ? "roundedTop" : "roundedBottom"; + } + if (ind < Math.floor(n / 2)) { + return "roundedTop"; + } + if (ind > Math.floor(n / 2)) { + return "roundedBottom"; + } + return "straightLine"; +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/About.tsx b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/About.tsx new file mode 100644 index 00000000..c241e480 --- /dev/null +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/About.tsx @@ -0,0 +1,58 @@ +import { + Button, + FormInput, + FormSelect, + useTabsContext, +} from "@dolthub/react-components"; +import { DatabaseType } from "@gen/graphql-types"; +import { useConfigContext } from "./context/config"; +import css from "./index.module.css"; + +export default function About() { + const { state, setState } = useConfigContext(); + const { activeTabIndex, setActiveTabIndex } = useTabsContext(); + + const onNext = () => { + setActiveTabIndex(activeTabIndex + 1); + }; + + return ( +
    + setState({ name: n })} + label="Name" + labelClassName={css.label} + placeholder="my-database (required)" + light + /> + { + if (!t) return; + setState({ + type: t, + port: t === DatabaseType.Mysql ? "3306" : "5432", + username: t === DatabaseType.Mysql ? "root" : "postgres", + }); + }} + options={[ + { label: "MySQL/Dolt", value: DatabaseType.Mysql }, + { + label: "Postgres/Doltgres", + value: DatabaseType.Postgres, + }, + ]} + hideSelectedOptions + light + /> + + + ); +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/Advanced.tsx b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/Advanced.tsx new file mode 100644 index 00000000..d8d609bd --- /dev/null +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/Advanced.tsx @@ -0,0 +1,50 @@ +import { + Button, + Checkbox, + ErrorMsg, + SmallLoader, + Tooltip, +} from "@dolthub/react-components"; +import css from "./index.module.css"; +import { useConfigContext } from "./context/config"; +import { getCanSubmit } from "./context/utils"; + +export default function Advanced() { + const { state, setState, error, onSubmit } = useConfigContext(); + const { canSubmit, message } = getCanSubmit(state); + + return ( +
    + setState({ useSSL: !state.useSSL })} + name="use-ssl" + label="Use SSL" + description="If server does not allow insecure connections, client must use SSL/TLS." + className={css.checkbox} + /> + setState({ hideDoltFeatures: !state.hideDoltFeatures })} + name="hide-dolt-features" + label="Hide Dolt features" + description="Hides Dolt features like branches, logs, and commits for non-Dolt databases. Will otherwise be disabled." + className={css.checkbox} + /> + + + {state.loading && } + + + + ); +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/Connection.tsx b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/Connection.tsx new file mode 100644 index 00000000..da34066b --- /dev/null +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/Connection.tsx @@ -0,0 +1,85 @@ +import { Button, FormInput, useTabsContext } from "@dolthub/react-components"; +import { DatabaseType } from "@gen/graphql-types"; +import { SyntheticEvent } from "react"; +import css from "./index.module.css"; +import { useConfigContext } from "./context/config"; + +export default function Connection() { + const { state, setState } = useConfigContext(); + const { activeTabIndex, setActiveTabIndex } = useTabsContext(); + + const onNext = (e: SyntheticEvent) => { + e.preventDefault(); + setActiveTabIndex(activeTabIndex + 1); + }; + + return ( +
    + setState({ connectionUrl: c })} + label="URL" + placeholder={`${ + state.type === DatabaseType.Mysql ? "mysql" : "postgresql" + }://[user]:[password]@[host]/[database]`} + horizontal + light + labelClassName={css.label} + /> +
    OR
    + setState({ host: h })} + placeholder={state.hostPlaceholder} + horizontal + light + labelClassName={css.label} + /> + setState({ port: p })} + placeholder={state.type === DatabaseType.Mysql ? "3306" : "5432"} + horizontal + light + labelClassName={css.label} + /> + setState({ username: u })} + placeholder="root" + horizontal + light + labelClassName={css.label} + /> + setState({ password: p })} + placeholder="**********" + type="password" + horizontal + light + labelClassName={css.label} + /> + setState({ database: d })} + placeholder="mydb" + horizontal + light + labelClassName={css.label} + /> + + + ); +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/ConnectionTabs.tsx b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/ConnectionTabs.tsx new file mode 100644 index 00000000..9a4e9352 --- /dev/null +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/ConnectionTabs.tsx @@ -0,0 +1,101 @@ +import { + Tab, + TabList, + TabPanel, + Tabs, + useTabsContext, +} from "@dolthub/react-components"; +import cx from "classnames"; +import { FiCheck } from "@react-icons/all-files/fi/FiCheck"; +import { ReactNode } from "react"; +import About from "./About"; +import Connection from "./Connection"; +import Advanced from "./Advanced"; +import css from "./index.module.css"; +import { useConfigContext } from "./context/config"; +import { ConfigState } from "./context/state"; + +export default function ConnectionTabs() { + const { setErr } = useConfigContext(); + const clearErr = () => setErr(undefined); + + return ( + + + {["About", "Connection", "Advanced"].map((tab, i) => ( + + {tab} + + ))} + + + + + + + + + + + + ); +} + +type PanelProps = { + index: number; + children: ReactNode; +}; + +function CustomTabPanel(props: PanelProps) { + return ( + + {props.children} + + ); +} + +function CustomTab(props: { index: number; children: string }) { + const { activeTabIndex } = useTabsContext(); + const isActive = props.index === activeTabIndex; + const { state } = useConfigContext(); + const isCompleted = + props.index < activeTabIndex && getCompleted(props.children, state); + return ( + + {isCompleted && } + {props.children} + + ); +} + +function getCompleted(tabName: string, state: ConfigState): boolean { + switch (tabName) { + case "About": + return !!state.name; + case "Connection": + return !!state.connectionUrl || (!!state.host && !!state.username); + default: + return true; + } +} + +function getDisabled(tabName: string, state: ConfigState): boolean { + switch (tabName) { + case "Connection": + return !state.name; + case "Advanced": + return ( + !state.name || + (!state.connectionUrl && (!state.host || !state.username)) + ); + default: + return false; + } +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/WelcomeMessage.tsx b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/WelcomeMessage.tsx new file mode 100644 index 00000000..d2ddfb6f --- /dev/null +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/WelcomeMessage.tsx @@ -0,0 +1,15 @@ +import DoltLink from "@components/links/DoltLink"; +import DoltgresLink from "@components/links/DoltgresLink"; +import css from "./index.module.css"; + +export default function WelcomeMessage() { + return ( +
    +

    Welcome to the Dolt Workbench

    +

    + Connect the workbench to any MySQL or PostgreSQL compatible database. + Use or to unlock version control features. +

    +
    + ); +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/config.tsx b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/config.tsx new file mode 100644 index 00000000..374915fc --- /dev/null +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/config.tsx @@ -0,0 +1,94 @@ +import { createCustomContext } from "@dolthub/react-contexts"; +import { + useContextWithError, + useEffectOnMount, + useSetState, +} from "@dolthub/react-hooks"; +import { useAddDatabaseConnectionMutation } from "@gen/graphql-types"; +import useMutation from "@hooks/useMutation"; +import { useRouter } from "next/router"; +import { ReactNode, SyntheticEvent, useEffect, useMemo } from "react"; +import { maybeDatabase } from "@lib/urls"; +import { ConfigContextType, defaultState, getDefaultState } from "./state"; +import { getConnectionUrl } from "./utils"; + +export const ConfigContext = + createCustomContext("ConfigContext"); + +type Props = { + children: ReactNode; +}; + +export function ConfigProvider({ children }: Props) { + const router = useRouter(); + + const [state, setState] = useSetState(defaultState); + const { mutateFn, ...res } = useMutation({ + hook: useAddDatabaseConnectionMutation, + }); + + useEffectOnMount(() => { + const isDocker = window.location.origin === "http://localhost:3000"; + setState(getDefaultState(isDocker)); + }); + + useEffect(() => { + if (!res.err) return; + if ( + res.err.message.includes("The server does not support SSL connections") + ) { + setState({ showAdvancedSettings: true }); + } + }, [res.err]); + + const clearState = () => { + setState(defaultState); + }; + + const onSubmit = async (e: SyntheticEvent) => { + e.preventDefault(); + setState({ loading: true }); + + try { + const db = await mutateFn({ + variables: { + name: state.name, + connectionUrl: getConnectionUrl(state), + hideDoltFeatures: state.hideDoltFeatures, + useSSL: state.useSSL, + type: state.type, + }, + }); + await res.client.clearStore(); + if (!db.data) { + return; + } + const { href, as } = maybeDatabase( + db.data.addDatabaseConnection.currentDatabase, + ); + await router.push(href, as); + } catch (_) { + // Handled by res.error + } finally { + setState({ loading: false }); + } + }; + const value = useMemo(() => { + return { + state, + setState, + onSubmit, + error: res.err, + setErr: res.setErr, + clearState, + }; + }, [state, setState, onSubmit, res.err, clearState]); + + return ( + {children} + ); +} + +export function useConfigContext(): ConfigContextType { + return useContextWithError(ConfigContext); +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/state.ts b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/state.ts new file mode 100644 index 00000000..3858bf1c --- /dev/null +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/state.ts @@ -0,0 +1,42 @@ +import { DatabaseType } from "@gen/graphql-types"; +import { SetApolloErrorType } from "@lib/errors/types"; +import { Dispatch, SyntheticEvent } from "react"; + +export const defaultState = { + name: "", + host: "", + hostPlaceholder: "127.0.0.1", + port: "3306", + username: "root", + password: "", + database: "", + connectionUrl: "", + hideDoltFeatures: false, + useSSL: true, + showAbout: true, + showConnectionDetails: false, + showAdvancedSettings: false, + loading: false, + type: DatabaseType.Mysql, +}; + +export type ConfigState = typeof defaultState; +export type ConfigDispatch = Dispatch>; + +export function getDefaultState(isDocker = false): ConfigState { + const defaultHost = isDocker ? "host.docker.internal" : "127.0.0.1"; + return { + ...defaultState, + host: defaultHost, + hostPlaceholder: defaultHost, + }; +} + +export type ConfigContextType = { + onSubmit: (e: SyntheticEvent) => Promise; + state: ConfigState; + setState: ConfigDispatch; + error?: Error | undefined; + setErr: SetApolloErrorType; + clearState: () => void; +}; diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/utils.ts b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/utils.ts new file mode 100644 index 00000000..8a0bdf8c --- /dev/null +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/context/utils.ts @@ -0,0 +1,24 @@ +import { DatabaseType } from "@gen/graphql-types"; +import { ConfigState } from "./state"; + +export function getConnectionUrl(state: ConfigState): string { + if (state.connectionUrl) return state.connectionUrl; + const prefix = state.type === DatabaseType.Mysql ? "mysql" : "postgresql"; + return `${prefix}://${state.username}:${state.password}@${state.host}:${state.port}/${state.database}`; +} + +type GetCanSubmitReturnType = { + canSubmit: boolean; + message?: string; +}; + +export function getCanSubmit(state: ConfigState): GetCanSubmitReturnType { + if (!state.name) { + return { canSubmit: false, message: "Connection name is required" }; + } + if (state.connectionUrl) return { canSubmit: true }; + if (!state.host || !state.username) { + return { canSubmit: false, message: "Host or user name is required" }; + } + return { canSubmit: true }; +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/index.module.css b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/index.module.css index c8751836..75687bc5 100644 --- a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/index.module.css +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/index.module.css @@ -1,40 +1,100 @@ -.databaseForm { - @apply w-full max-w-xl; +.container { + @apply py-20 bg-stone-50 mb-16; +} + +.panel { + @apply pt-10 px-10; +} + +.tabList { + @apply px-6; +} + +.tab { + @apply w-32 h-10 text-[#9AA5AD] relative; + button { + @apply flex items-center w-full; + } + span { + @apply flex-shrink-0; + } +} +.activeTab { + @apply text-storm-600 border-storm-600; +} + +.check { + @apply mt-1.5 text-xl mr-2 flex-shrink-0 absolute left-0; +} + +.isCompleted { + @apply text-green-400; +} + +.databaseForm { + @apply w-full max-w-2xl mx-auto mt-14; + a { + @apply text-coral-300; + } + a:hover { + @apply text-coral-400; + } h3 { @apply mb-4; } } +.welcome { + @apply text-center; + h1 { + @apply text-4xl font-semibold; + } + p { + @apply text-xl my-6 leading-8; + } +} + .whiteContainer { - @apply max-w-xl w-full border rounded-lg py-10 bg-white mx-auto; + @apply max-w-2xl w-full border rounded-lg py-10 bg-white mx-auto; } -.goback { - @apply mb-2 mx-2; - svg { - @apply mb-[0.2rem] mr-1 inline-block; - } +.top { + @apply mx-10; } -.section { - @apply px-14 py-4; +.form { + @apply flex items-center flex-col; +} + +.label { + @apply text-left; } .typeSelect { - @apply ml-8 mb-4; + @apply mb-4 w-full; } .typeSelectInner { @apply w-full; } -.middle { - @apply border-y pt-8; +.or { + @apply font-semibold text-center w-full my-5 text-lg text-stone-100; } -.or { - @apply font-semibold text-center w-full my-5 text-lg; +.or::before, +.or::after { + content: ""; + @apply w-[46%] border-t border-stone-100 inline-block align-middle relative; +} + +.or::after { + @apply right-0 -ml-[52%]; +} + +.or::before { + @apply left-0 -mr-[52%]; } .advancedSettings { @@ -46,9 +106,25 @@ } .checkbox { - @apply mt-7 mb-10 text-sm; + @apply mb-3 text-sm w-full; +} + +.form .checkbox { + svg { + @apply text-coral-300; + &:hover { + @apply text-coral-400; + } + } } .instructions { @apply mb-8; } + +.button { + @apply mt-8 min-w-36 py-2 bg-coral-300; + &:hover { + @apply bg-coral-400; + } +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/index.tsx b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/index.tsx index 468ac33d..5b660916 100644 --- a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/index.tsx +++ b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/index.tsx @@ -1,182 +1,45 @@ -import { - Button, - ButtonsWithError, - Checkbox, - ExternalLink, - FormInput, - FormSelect, - Loader, -} from "@dolthub/react-components"; -import { DatabaseType } from "@gen/graphql-types"; +import { ExternalLink } from "@dolthub/react-components"; import { dockerHubRepo } from "@lib/constants"; -import { FaCaretDown } from "@react-icons/all-files/fa/FaCaretDown"; -import { FaCaretUp } from "@react-icons/all-files/fa/FaCaretUp"; -import { IoIosArrowDropleftCircle } from "@react-icons/all-files/io/IoIosArrowDropleftCircle"; -import cx from "classnames"; +import MainLayout from "@components/layouts/MainLayout"; +import WelcomeMessage from "./WelcomeMessage"; +import ConnectionTabs from "./ConnectionTabs"; import css from "./index.module.css"; -import useConfig, { getCanSubmit } from "./useConfig"; +import { ConfigProvider } from "./context/config"; type Props = { - canGoBack: boolean; - setShowForm: (s: boolean) => void; + noExistingConnection?: boolean; }; -export default function NewConnection(props: Props) { - const { onSubmit, state, setState, error, clearState } = useConfig(); - - const onCancel = props.canGoBack - ? () => { - props.setShowForm(false); - } - : clearState; +type InnerProps = { + showWelcomeMsg?: boolean; +}; +function Inner({ showWelcomeMsg }: InnerProps) { return (
    - - {props.canGoBack && ( - - back to connections - - )} + {showWelcomeMsg && }
    -
    -
    -

    Set up new connection

    -

    - View instructions for connecting to local and Docker installed - databases here. -

    - setState({ name: n })} - label="Name" - placeholder="my-database (required)" - horizontal - light - /> - { - if (!t) return; - setState({ - type: t, - port: t === DatabaseType.Mysql ? "3306" : "5432", - username: t === DatabaseType.Mysql ? "root" : "postgres", - }); - }} - options={[ - { label: "MySQL/Dolt", value: DatabaseType.Mysql }, - { - label: "Postgres/Doltgres", - value: DatabaseType.Postgres, - }, - ]} - hideSelectedOptions - horizontal - light - /> -
    -
    - setState({ connectionUrl: c })} - label="URL" - placeholder={`${ - state.type === DatabaseType.Mysql ? "mysql" : "postgresql" - }://[user]:[password]@[host]/[database]`} - horizontal - light - /> -
    OR
    - setState({ host: h })} - placeholder={state.hostPlaceholder} - horizontal - light - /> - setState({ port: p })} - placeholder={state.type === DatabaseType.Mysql ? "3306" : "5432"} - horizontal - light - /> - setState({ username: u })} - placeholder="root" - horizontal - light - /> - setState({ password: p })} - placeholder="**********" - type="password" - horizontal - light - /> - setState({ database: d })} - placeholder="mydb" - horizontal - light - /> -
    -
    - - setState({ showAdvancedSettings: !state.showAdvancedSettings }) - } - className={css.advancedSettings} - > - {state.showAdvancedSettings ? : }{" "} - Advanced settings - - {state.showAdvancedSettings && ( -
    - setState({ useSSL: !state.useSSL })} - name="use-ssl" - label="Use SSL" - description="If server does not allow insecure connections, client must use SSL/TLS." - className={css.checkbox} - /> - - setState({ hideDoltFeatures: !state.hideDoltFeatures }) - } - name="hide-dolt-features" - label="Hide Dolt features" - description="Hides Dolt features like branches, logs, and commits for non-Dolt databases. Will otherwise be disabled." - className={css.checkbox} - /> -
    - )} - - - -
    -
    +
    +

    Set up a new connection

    +

    + View instructions for connecting to local and Docker installed + databases here. +

    +
    + + +
    ); } + +export default function NewConnection({ noExistingConnection }: Props) { + return noExistingConnection ? ( + + ) : ( + + + + ); +} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/useConfig.ts b/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/useConfig.ts deleted file mode 100644 index b4e9f083..00000000 --- a/web/renderer/components/pageComponents/ConnectionsPage/NewConnection/useConfig.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { useEffectOnMount, useSetState } from "@dolthub/react-hooks"; -import { - DatabaseType, - useAddDatabaseConnectionMutation, -} from "@gen/graphql-types"; -import useMutation from "@hooks/useMutation"; -import { maybeDatabase } from "@lib/urls"; -import { useRouter } from "next/router"; -import { Dispatch, SyntheticEvent, useEffect } from "react"; - -const defaultState = { - name: "", - host: "", - hostPlaceholder: "127.0.0.1", - port: "3306", - username: "root", - password: "", - database: "", - connectionUrl: "", - hideDoltFeatures: false, - useSSL: true, - showAdvancedSettings: false, - loading: false, - type: DatabaseType.Mysql, -}; - -type ConfigState = typeof defaultState; -type ConfigDispatch = Dispatch>; - -function getDefaultState(isDocker = false): ConfigState { - const defaultHost = isDocker ? "host.docker.internal" : "127.0.0.1"; - return { - ...defaultState, - host: defaultHost, - hostPlaceholder: defaultHost, - }; -} - -type ReturnType = { - onSubmit: (e: SyntheticEvent) => Promise; - state: ConfigState; - setState: ConfigDispatch; - error?: Error | undefined; - clearState: () => void; -}; - -export default function useConfig(): ReturnType { - const router = useRouter(); - - const [state, setState] = useSetState(defaultState); - const { mutateFn, ...res } = useMutation({ - hook: useAddDatabaseConnectionMutation, - }); - - useEffectOnMount(() => { - const isDocker = window.location.origin === "http://localhost:3000"; - setState(getDefaultState(isDocker)); - }); - - useEffect(() => { - if (!res.err) return; - if ( - res.err.message.includes("The server does not support SSL connections") - ) { - setState({ showAdvancedSettings: true }); - } - }, [res.err]); - - const clearState = () => { - setState(defaultState); - }; - - const onSubmit = async (e: SyntheticEvent) => { - e.preventDefault(); - setState({ loading: true }); - - try { - const db = await mutateFn({ - variables: { - name: state.name, - connectionUrl: getConnectionUrl(state), - hideDoltFeatures: state.hideDoltFeatures, - useSSL: state.useSSL, - type: state.type, - }, - }); - await res.client.clearStore(); - if (!db.data) { - return; - } - const { href, as } = maybeDatabase( - db.data.addDatabaseConnection.currentDatabase, - ); - await router.push(href, as); - } catch (_) { - // Handled by res.error - } finally { - setState({ loading: false }); - } - }; - - return { onSubmit, state, setState, error: res.err, clearState }; -} - -function getConnectionUrl(state: ConfigState): string { - if (state.connectionUrl) return state.connectionUrl; - const prefix = state.type === DatabaseType.Mysql ? "mysql" : "postgresql"; - return `${prefix}://${state.username}:${state.password}@${state.host}:${state.port}/${state.database}`; -} - -export function getCanSubmit(state: ConfigState): boolean { - if (!state.name) return false; - if (state.connectionUrl) return true; - if (!state.host || !state.username) return false; - return true; -} diff --git a/web/renderer/components/pageComponents/ConnectionsPage/index.module.css b/web/renderer/components/pageComponents/ConnectionsPage/index.module.css index f41e333e..32cd718c 100644 --- a/web/renderer/components/pageComponents/ConnectionsPage/index.module.css +++ b/web/renderer/components/pageComponents/ConnectionsPage/index.module.css @@ -1,19 +1,3 @@ .container { - @apply max-w-6xl py-24 bg-stone-50; -} - -.inner { - @apply flex justify-between; -} - -.top { - @apply mr-24 mt-10; - - h1 { - @apply text-left; - } - - p { - @apply text-xl max-w-lg mt-7 leading-8; - } + @apply py-16 bg-stone-50 mb-16 w-fit max-w-none; } diff --git a/web/renderer/components/pageComponents/ConnectionsPage/index.tsx b/web/renderer/components/pageComponents/ConnectionsPage/index.tsx index 742bef28..1a49b8d4 100644 --- a/web/renderer/components/pageComponents/ConnectionsPage/index.tsx +++ b/web/renderer/components/pageComponents/ConnectionsPage/index.tsx @@ -1,56 +1,24 @@ import MainLayout from "@components/layouts/MainLayout"; -import DoltLink from "@components/links/DoltLink"; -import DoltgresLink from "@components/links/DoltgresLink"; import { QueryHandler } from "@dolthub/react-components"; -import { - DatabaseConnectionFragment, - useStoredConnectionsQuery, -} from "@gen/graphql-types"; -import { useEffect, useState } from "react"; +import { useStoredConnectionsQuery } from "@gen/graphql-types"; import ExistingConnections from "./ExistingConnections"; import NewConnection from "./NewConnection"; import css from "./index.module.css"; -type InnerProps = { - connections: DatabaseConnectionFragment[]; -}; - -function Inner(props: InnerProps) { - const [showForm, setShowForm] = useState(!props.connections.length); - - useEffect(() => { - setShowForm(!props.connections.length); - }, [props.connections]); - - if (showForm) { - return ( - - ); - } - return ; -} - export default function ConfigurationPage() { const res = useStoredConnectionsQuery(); return ( -
    -
    -

    Welcome to the Dolt Workbench

    -

    - Connect the workbench to any MySQL or PostgreSQL compatible - database. Use or to unlock version - control features. -

    -
    - } - /> -
    + + data.storedConnections.length ? ( + + ) : ( + + ) + } + />
    ); } diff --git a/web/renderer/components/pageComponents/DatabasesPage/index.module.css b/web/renderer/components/pageComponents/DatabasesPage/index.module.css index 1bb942b6..2a3c3503 100644 --- a/web/renderer/components/pageComponents/DatabasesPage/index.module.css +++ b/web/renderer/components/pageComponents/DatabasesPage/index.module.css @@ -1,35 +1,34 @@ +.container { + @apply flex flex-col items-center; +} + .desc { - @apply text-lg text-center mb-10; + @apply text-lg text-center mb-8; +} + +.dbList { + @apply w-fit; } .database { - @apply bg-white w-full border my-2 px-4 py-2 rounded flex justify-between; + @apply bg-white w-full h-14 border my-1 px-14 py-2 rounded flex justify-start items-center text-sky-400 min-w-80; &:hover { - @apply border-stone-300; + @apply border-stone-300 text-link-1; + } + svg { + @apply flex-shrink-0 mr-2; } } .noDbs { - @apply text-lg; + @apply text-lg italic; } .button { - @apply bg-button-1 text-white my-6 px-4 py-1.5 text-base rounded button-shadow; + @apply text-white my-6 px-4 py-1.5 text-base rounded button-shadow bg-coral-300; &:hover { - @apply text-white bg-button-2 button-shadow-hover; - } -} - -.go { - @apply hidden; -} - -.database:hover .go { - @apply flex items-center; - - svg { - @apply ml-2; + @apply text-white bg-coral-400 button-shadow-hover; } } diff --git a/web/renderer/components/pageComponents/DatabasesPage/index.tsx b/web/renderer/components/pageComponents/DatabasesPage/index.tsx index 5ba48073..e62730e8 100644 --- a/web/renderer/components/pageComponents/DatabasesPage/index.tsx +++ b/web/renderer/components/pageComponents/DatabasesPage/index.tsx @@ -1,11 +1,12 @@ import CreateDatabase from "@components/CreateDatabase"; import MainLayout from "@components/layouts/MainLayout"; import Link from "@components/links/Link"; +import { excerpt } from "@dolthub/web-utils"; import { ErrorMsg, Loader } from "@dolthub/react-components"; import { useDatabasesQuery } from "@gen/graphql-types"; import useDatabaseDetails from "@hooks/useDatabaseDetails"; import { database } from "@lib/urls"; -import { FaChevronRight } from "@react-icons/all-files/fa/FaChevronRight"; +import { FaDatabase } from "@react-icons/all-files/fa/FaDatabase"; import cx from "classnames"; import { useRouter } from "next/router"; import { useEffect } from "react"; @@ -27,36 +28,36 @@ export default function DatabasesPage() { return ( -

    Choose a database

    -

    - Choose an existing database or create a new database to get started. -

    - {res.data?.databases.length ? ( -
      - {res.data.databases.map(db => ( -
    • - -
      - {db} - - Go - -
      - -
    • - ))} -
    - ) : ( -

    - No databases found. Create a database to get started. +

    +

    Choose a database

    +

    + Choose an existing database or create a new database to get started.

    - )} - - + {res.data?.databases.length ? ( +
      + {res.data.databases.map(db => ( +
    • + +
      + + {excerpt(db, 32)} +
      + +
    • + ))} +
    + ) : ( +

    + No databases found. Create a database to get started. +

    + )} + + +
    ); diff --git a/web/renderer/lib/urls.ts b/web/renderer/lib/urls.ts index b91a22cd..b2aebbd7 100644 --- a/web/renderer/lib/urls.ts +++ b/web/renderer/lib/urls.ts @@ -7,6 +7,10 @@ export type RefUrl = (p: ps.RefOptionalSchemaParams) => Route; export const databases = new Route("/database"); +export const connections = new Route("/connections"); + +export const newConnection = connections.addStatic("new"); + export const database = (p: ps.DatabaseOptionalSchemaParams): Route => databases .addDynamic("databaseName", p.databaseName) diff --git a/web/renderer/pages/connections.tsx b/web/renderer/pages/connections/index.tsx similarity index 87% rename from web/renderer/pages/connections.tsx rename to web/renderer/pages/connections/index.tsx index e7f6a25a..c5b2a692 100644 --- a/web/renderer/pages/connections.tsx +++ b/web/renderer/pages/connections/index.tsx @@ -3,7 +3,7 @@ import Page from "@components/util/Page"; import { NextPage } from "next"; const Connections: NextPage = () => ( - + ); diff --git a/web/renderer/pages/connections/new.tsx b/web/renderer/pages/connections/new.tsx new file mode 100644 index 00000000..b55a0e5d --- /dev/null +++ b/web/renderer/pages/connections/new.tsx @@ -0,0 +1,11 @@ +import NewConnection from "@components/pageComponents/ConnectionsPage/NewConnection"; +import Page from "@components/util/Page"; +import { NextPage } from "next"; + +const AddConnection: NextPage = () => ( + + + +); + +export default AddConnection; diff --git a/web/renderer/public/images/d-large-logo.png b/web/renderer/public/images/d-large-logo.png new file mode 100644 index 00000000..0ddf53c1 Binary files /dev/null and b/web/renderer/public/images/d-large-logo.png differ diff --git a/web/renderer/public/images/database-icon.png b/web/renderer/public/images/database-icon.png new file mode 100644 index 00000000..6c0ec4e9 Binary files /dev/null and b/web/renderer/public/images/database-icon.png differ diff --git a/web/renderer/tailwind.config.ts b/web/renderer/tailwind.config.ts index f4eb0c8e..9992e30b 100644 --- a/web/renderer/tailwind.config.ts +++ b/web/renderer/tailwind.config.ts @@ -19,6 +19,11 @@ const config = mergeConfig({ "./renderer/styles/**/*.css", ], theme: { + colors: { + coral: { + 300: "#FF8964", + }, + }, extend: {}, }, plugins: [], diff --git a/web/yarn.lock b/web/yarn.lock index 1aa5386a..bd0f96c2 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2564,7 +2564,7 @@ __metadata: languageName: node linkType: hard -"@dolthub/react-components@npm:^0.2.6, @dolthub/react-components@npm:^0.2.7": +"@dolthub/react-components@npm:^0.2.7": version: 0.2.7 resolution: "@dolthub/react-components@npm:0.2.7" dependencies: @@ -2591,6 +2591,33 @@ __metadata: languageName: node linkType: hard +"@dolthub/react-components@npm:^0.2.8": + version: 0.2.8 + resolution: "@dolthub/react-components@npm:0.2.8" + dependencies: + "@dolthub/react-contexts": "npm:^0.1.0" + "@dolthub/react-hooks": "npm:^0.1.7" + "@dolthub/web-utils": "npm:^0.1.5" + "@react-icons/all-files": "npm:^4.1.0" + classnames: "npm:^2.5.1" + deepmerge: "npm:^4.3.1" + github-markdown-css: "npm:^5.8.1" + numeral: "npm:^2.0.6" + react-copy-to-clipboard: "npm:^5.1.0" + react-loader: "npm:^2.4.7" + react-markdown: "npm:^9.0.1" + react-select: "npm:^5.8.3" + react-tooltip: "npm:^5.28.0" + reactjs-popup: "npm:^2.0.6" + remark-gfm: "npm:^4.0.0" + tailwindcss: "npm:^3.4.1" + peerDependencies: + react: ^18 + react-dom: ^18 + checksum: 10c0/7708505e0e4b1fc610b52813ca703c9b8fa5d1204e7f1ea141361cde24b209d59b4c1c3def419066e3d2765489815721285015ae5a53905dd3ffeb5e3e17e1ac + languageName: node + linkType: hard + "@dolthub/react-contexts@npm:^0.1.0": version: 0.1.0 resolution: "@dolthub/react-contexts@npm:0.1.0" @@ -8897,7 +8924,7 @@ __metadata: resolution: "dolt-workbench@workspace:." dependencies: "@apollo/client": "npm:^3.9.9" - "@dolthub/react-components": "npm:^0.2.6" + "@dolthub/react-components": "npm:^0.2.8" "@dolthub/react-contexts": "npm:^0.1.0" "@dolthub/react-hooks": "npm:^0.1.7" "@dolthub/web-utils": "npm:^0.1.8" @@ -10780,6 +10807,13 @@ __metadata: languageName: node linkType: hard +"github-markdown-css@npm:^5.8.1": + version: 5.8.1 + resolution: "github-markdown-css@npm:5.8.1" + checksum: 10c0/67d3540731f9f1fc3b903362ef270e6db0ffe252fd4998a1a8777fc63b288b1904e5e57703e74a2ee04f1d613a4732b1949cb472b66a3c2e9e2b7a1750d1f0a6 + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -16501,6 +16535,26 @@ __metadata: languageName: node linkType: hard +"react-select@npm:^5.8.3": + version: 5.10.0 + resolution: "react-select@npm:5.10.0" + dependencies: + "@babel/runtime": "npm:^7.12.0" + "@emotion/cache": "npm:^11.4.0" + "@emotion/react": "npm:^11.8.1" + "@floating-ui/dom": "npm:^1.0.1" + "@types/react-transition-group": "npm:^4.4.0" + memoize-one: "npm:^6.0.0" + prop-types: "npm:^15.6.0" + react-transition-group: "npm:^4.3.0" + use-isomorphic-layout-effect: "npm:^1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/64cc73ef43556d0a199420d7d19f9f72e3c5e3a7f6828aef5421ec16cc0e4bc337061a8fa3c03afc5b929a087a4ca866f497e0ef865b03fe014c5cacde5e71dd + languageName: node + linkType: hard + "react-timeago@npm:^7.2.0": version: 7.2.0 resolution: "react-timeago@npm:7.2.0" @@ -16510,7 +16564,7 @@ __metadata: languageName: node linkType: hard -"react-tooltip@npm:^5.26.3, react-tooltip@npm:^5.27.1": +"react-tooltip@npm:^5.26.3, react-tooltip@npm:^5.27.1, react-tooltip@npm:^5.28.0": version: 5.28.0 resolution: "react-tooltip@npm:5.28.0" dependencies: @@ -19111,6 +19165,18 @@ __metadata: languageName: node linkType: hard +"use-isomorphic-layout-effect@npm:^1.2.0": + version: 1.2.0 + resolution: "use-isomorphic-layout-effect@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/2e4bdee68d65893b37e716ebdcc111550775189c80e662eda87d6f5b54dc431d3383a18914ea01a893ee5478902a878012713eaebcacbb6611ab88c463accb83 + languageName: node + linkType: hard + "use-sync-external-store@npm:1.2.2, use-sync-external-store@npm:^1.2.0": version: 1.2.2 resolution: "use-sync-external-store@npm:1.2.2"