diff --git a/web/README.desktop-app.md b/web/README.desktop-app.md index 9ded2aa9..ad8b44c2 100644 --- a/web/README.desktop-app.md +++ b/web/README.desktop-app.md @@ -1,6 +1,16 @@ ## Run the desktop app in dev mode: +1. Rebuild `graphql-server` (if changes were made): + ```bash +# in graphql-server +yarn build +``` + +2.Start the desktop app: + +```bash +# in web yarn dev:app ``` diff --git a/web/main/background.ts b/web/main/background.ts index 2c348bf2..1ea708b6 100644 --- a/web/main/background.ts +++ b/web/main/background.ts @@ -125,10 +125,9 @@ app.on("ready", async () => { mainWindow = createWindow("main", { width: 1400, height: 900, - minHeight: 600, - minWidth: 600, titleBarStyle: process.platform === "darwin" ? "hiddenInset" : undefined, - titleBarOverlay: process.platform === "darwin", + titleBarOverlay: + process.platform === "darwin" ? { height: HEADER_HEIGHT } : undefined, trafficLightPosition: { x: 20, y: HEADER_HEIGHT / 2 - MACOS_TRAFFIC_LIGHTS_HEIGHT / 2, @@ -142,9 +141,7 @@ app.on("ready", async () => { Menu.setApplicationMenu(initMenu(mainWindow, isProd)); setupTitleBarClickMac(); createGraphqlSeverProcess(); - mainWindow?.webContents.executeJavaScript( - ` console.error('for mac nav:', ${process.env.NEXT_PUBLIC_FOR_MAC_NAV})`, - ); + await waitForGraphQLServer("http://localhost:9002/graphql"); if (isProd) { @@ -163,6 +160,7 @@ app.on("ready", async () => { return { action: "deny" }; }); + mainWindow.setMinimumSize(1080, 780); // hit when clicking with no target // optionally redirect to browser diff --git a/web/main/helpers/create-window.ts b/web/main/helpers/create-window.ts index 29c8c885..2746f414 100644 --- a/web/main/helpers/create-window.ts +++ b/web/main/helpers/create-window.ts @@ -71,7 +71,6 @@ export const createWindow = ( }; state = ensureVisibleOnSomeDisplay(restore()); - const win = new BrowserWindow({ ...state, ...options, diff --git a/web/package.json b/web/package.json index 3770c608..38f17332 100644 --- a/web/package.json +++ b/web/package.json @@ -93,7 +93,7 @@ "autoprefixer": "^10.4.19", "babel-jest": "^29.7.0", "cssnano": "^7.0.6", - "electron": "^31.0.1", + "electron": "33.2.1", "electron-builder": "^25.1.8", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", diff --git a/web/renderer/components/ConnectionsAndDatabases/ConnectionItem.tsx b/web/renderer/components/ConnectionsAndDatabases/ConnectionItem.tsx new file mode 100644 index 00000000..643abb1a --- /dev/null +++ b/web/renderer/components/ConnectionsAndDatabases/ConnectionItem.tsx @@ -0,0 +1,53 @@ +import { + DatabaseConnection, + DatabaseConnectionFragment, +} from "@gen/graphql-types"; +import { MdRemoveRedEye } from "@react-icons/all-files/md/MdRemoveRedEye"; +import { FaChevronRight } from "@react-icons/all-files/fa/FaChevronRight"; +import { Button } from "@dolthub/react-components"; +import { excerpt } from "@dolthub/web-utils"; +import cx from "classnames"; +import { DatabaseTypeLabel } from "./DatabaseTypeLabel"; +import css from "./index.module.css"; + +type Props = { + conn: DatabaseConnectionFragment; + selectedConnection: DatabaseConnectionFragment; + onSelected: (conn: DatabaseConnection) => void; + currentConnection: DatabaseConnection; +}; +export default function ConnectionItem({ + conn, + selectedConnection, + onSelected, + currentConnection, +}: Props) { + return ( + onSelected(conn)} + > +
+
+ {excerpt(conn.name, 16)} + + {conn.name === currentConnection.name && ( + + )} +
+ +
+
+ {excerpt(getHostAndPort(conn.connectionUrl), 42)} +
+
+ ); +} + +function getHostAndPort(connectionString: string) { + const url = new URL(connectionString); + return `${url.hostname}:${url.port}`; +} diff --git a/web/renderer/components/ConnectionsAndDatabases/DatabaseItem.tsx b/web/renderer/components/ConnectionsAndDatabases/DatabaseItem.tsx new file mode 100644 index 00000000..b875907c --- /dev/null +++ b/web/renderer/components/ConnectionsAndDatabases/DatabaseItem.tsx @@ -0,0 +1,77 @@ +import { Button, ErrorMsg, Loader } from "@dolthub/react-components"; +import { MdRemoveRedEye } from "@react-icons/all-files/md/MdRemoveRedEye"; +import { + DatabaseConnectionFragment, + DatabaseType, + useAddDatabaseConnectionMutation, + useResetDatabaseMutation, +} from "@gen/graphql-types"; +import useMutation from "@hooks/useMutation"; +import { useRouter } from "next/router"; +import { database } from "@lib/urls"; +import { excerpt } from "@dolthub/web-utils"; +import cx from "classnames"; +import css from "./index.module.css"; + +type Props = { + db: string; + conn: DatabaseConnectionFragment; + currentConnection: DatabaseConnectionFragment; + currentDatabase: string; +}; + +export default function DatabaseItem({ + db, + conn, + currentConnection, + currentDatabase, +}: Props) { + const { + mutateFn: resetDB, + loading, + err, + } = useMutation({ + hook: useResetDatabaseMutation, + }); + const router = useRouter(); + const [addDb, res] = useAddDatabaseConnectionMutation(); + + const onClick = async (databaseName: string) => { + const addedDb = await addDb({ variables: conn }); + if (!addedDb.data) { + return; + } + await res.client.clearStore(); + + if (conn.type === DatabaseType.Postgres) { + await resetDB({ variables: { newDatabase: databaseName } }); + } + const { href, as } = database({ databaseName }); + router.push(href, as).catch(console.error); + }; + + if (loading) { + return ; + } + + if (err) { + return ; + } + const dbName = excerpt(db, 32); + if (db === currentDatabase && conn.name === currentConnection.name) { + return ( + + {dbName} + + + ); + } + return ( + onClick(db)} + > + {dbName} + + ); +} diff --git a/web/renderer/components/ConnectionsAndDatabases/DatabaseTypeLabel.tsx b/web/renderer/components/ConnectionsAndDatabases/DatabaseTypeLabel.tsx new file mode 100644 index 00000000..97e65124 --- /dev/null +++ b/web/renderer/components/ConnectionsAndDatabases/DatabaseTypeLabel.tsx @@ -0,0 +1,39 @@ +import { getDatabaseType } from "@components/DatabaseTypeLabel"; +import { DatabaseConnectionFragment } from "@gen/graphql-types"; +import css from "./index.module.css"; + +type Props = { + conn: DatabaseConnectionFragment; +}; + +export function DatabaseTypeLabel({ conn }: Props) { + const type = getDatabaseType(conn.type ?? undefined, !!conn.isDolt); + switch (type) { + case "Dolt": + return ( + + Dolt + + ); + case "MySQL": + return ( + + MySQL + + ); + case "PostgreSQL": + return ( + + PostgreSQL + + ); + case "DoltgreSQL": + return ( + + DoltgreSQL + + ); + default: + return {type}; + } +} diff --git a/web/renderer/components/ConnectionsAndDatabases/Popup.tsx b/web/renderer/components/ConnectionsAndDatabases/Popup.tsx new file mode 100644 index 00000000..0ba0327f --- /dev/null +++ b/web/renderer/components/ConnectionsAndDatabases/Popup.tsx @@ -0,0 +1,72 @@ +import { DatabaseConnection, DatabaseType } from "@gen/graphql-types"; +import { DatabaseParams } from "@lib/params"; +import cx from "classnames"; +import Link from "@components/links/Link"; +import { ErrorMsg, SmallLoader } from "@dolthub/react-components"; +import CreateDatabase from "@components/CreateDatabase"; +import { FiTool } from "@react-icons/all-files/fi/FiTool"; +import { StateType } from "./useSelectedConnection"; +import DatabaseItem from "./DatabaseItem"; +import css from "./index.module.css"; +import ConnectionItem from "./ConnectionItem"; + +type Props = { + params: DatabaseParams; + currentConnection: DatabaseConnection; + onSelected: (conn: DatabaseConnection) => void; + storedConnections: DatabaseConnection[]; + state: StateType; +}; + +export default function Popup({ + params, + currentConnection, + onSelected, + storedConnections, + state, +}: Props) { + return ( +
+
+
+ CONNECTIONS + + + +
+
+ DATABASES + +
+
+
+
+ {storedConnections.map(conn => ( + + ))} +
+
+ {state.loading && } + {state.databases.map(db => ( + + ))} + +
+
+
+ ); +} diff --git a/web/renderer/components/ConnectionsAndDatabases/index.module.css b/web/renderer/components/ConnectionsAndDatabases/index.module.css new file mode 100644 index 00000000..c5e7d67c --- /dev/null +++ b/web/renderer/components/ConnectionsAndDatabases/index.module.css @@ -0,0 +1,124 @@ +.iconAndSelector { + -webkit-app-region: no-drag; + @apply hidden; + @screen lg { + @apply flex items-center w-64; + } +} + +.dbIcon { + @apply mr-3 w-4 h-4; +} + +.selector { + @apply rounded px-4 py-1 bg-[#3C4D5A] min-w-56 flex items-center justify-between font-normal; +} + +.container { + @apply flex max-w-[42rem] flex-col; +} + +.top, +.middle { + @apply flex; +} + +.top svg { + @apply text-sky-400 w-5 h-5; +} + +.middle { + @apply min-h-32; +} + +.left { + @apply w-1/2 border-r border-stone-100; +} + +.right { + @apply w-1/2; +} + +.header { + @apply flex items-center border-b border-stone-100 p-4 justify-between; + span { + @apply font-semibold text-sm text-storm-200; + } +} + +.connection { + @apply h-16 flex-col items-start w-full px-4 border-b border-storm-50 border-opacity-40; +} + +.connection:last-child { + @apply border-none rounded-bl; +} + +.wrench { + @apply w-4 h-4 border border-sky-400 rounded-full p-[0.1rem] -rotate-90; +} + +.connectionTop { + @apply flex items-center justify-between; +} + +.nameAndLabel { + @apply flex items-center; +} + +.connectionName { + @apply text-left; +} + +.label { + @apply text-sm px-3 h-5 bg-[#EFF2F3] rounded-full flex items-center justify-center ml-4; + img { + @apply h-[0.65rem]; + } +} + +.connectionUrl { + @apply text-storm-200 text-xs font-normal text-left py-1; +} + +.dbItem { + @apply text-storm-500 font-semibold text-sm py-2 px-4 flex w-full items-center justify-between; + svg { + @apply font-normal h-3; + } +} + +.selected, +.connection:hover, +.dbItem:hover { + @apply bg-storm-50 bg-opacity-50; +} + +.selected .connectionName, +.connection:hover .connectionName, +.dbItem:hover { + @apply text-storm-600; +} + +.viewing { + @apply w-4 text-link-1 ml-2; +} + +.arrow { + @apply text-sky-400 w-3 h-3; +} + +.err { + @apply ml-4; +} + +.link { + @apply text-sky-500 justify-between; +} + +.connectionError { + @apply flex items-center text-white font-normal; + &:hover { + @apply text-sky-400; + } +} diff --git a/web/renderer/components/ConnectionsAndDatabases/index.tsx b/web/renderer/components/ConnectionsAndDatabases/index.tsx new file mode 100644 index 00000000..ba96f399 --- /dev/null +++ b/web/renderer/components/ConnectionsAndDatabases/index.tsx @@ -0,0 +1,94 @@ +import { QueryHandler, ButtonWithPopup } from "@dolthub/react-components"; +import { useRef, useState } from "react"; +import { useOnClickOutside } from "@dolthub/react-hooks"; +import { DatabaseParams } from "@lib/params"; +import { + DatabaseConnectionFragment, + useCurrentConnectionQuery, +} from "@gen/graphql-types"; +import Link from "@components/links/Link"; +import { FiDatabase } from "@react-icons/all-files/fi/FiDatabase"; +import { excerpt } from "@dolthub/web-utils"; +import cx from "classnames"; +import useSelectedConnection from "./useSelectedConnection"; +import Popup from "./Popup"; +import css from "./index.module.css"; + +type Props = { + params: DatabaseParams; + setNoDrag?: (isOpen: boolean) => void; + className?: string; +}; + +type InnerProps = Props & { + connection: DatabaseConnectionFragment; +}; + +function Inner({ connection, params, setNoDrag, className }: InnerProps) { + const { onSelected, state, storedConnections } = + useSelectedConnection(connection); + const [isOpen, setIsOpen] = useState(false); + const connectionsRef = useRef(null); + + useOnClickOutside(connectionsRef, () => { + setIsOpen(false); + }); + const triggerText = `${excerpt(connection.name, 24)} / ${excerpt(params.databaseName, 24)}`; + + return ( +
+ + { + if (setNoDrag) { + setNoDrag(true); + } + await onSelected(connection); + }} + onClose={() => { + if (setNoDrag) { + setNoDrag(false); + } + }} + triggerText={triggerText} + buttonClassName={css.selector} + > +
+ +
+
+
+ ); +} + +export default function ConnectionsAndDatabases(props: Props) { + const res = useCurrentConnectionQuery(); + + return ( + + data.currentConnection ? ( + + ) : ( + + Connections + + ) + } + /> + ); +} diff --git a/web/renderer/components/ConnectionsAndDatabases/useSelectedConnection.ts b/web/renderer/components/ConnectionsAndDatabases/useSelectedConnection.ts new file mode 100644 index 00000000..473ad9bc --- /dev/null +++ b/web/renderer/components/ConnectionsAndDatabases/useSelectedConnection.ts @@ -0,0 +1,58 @@ +import { useSetState } from "@dolthub/react-hooks"; +import { + DatabaseConnectionFragment, + useDatabasesByConnectionLazyQuery, + useStoredConnectionsQuery, +} from "@gen/graphql-types"; +import { handleCaughtApolloError } from "@lib/errors/helpers"; +import { ApolloErrorType } from "@lib/errors/types"; + +export type StateType = { + connection: DatabaseConnectionFragment; + databases: string[]; + err: ApolloErrorType | undefined; + loading: boolean; +}; + +type ReturnType = { + onSelected: (connection: DatabaseConnectionFragment) => Promise; + storedConnections: DatabaseConnectionFragment[]; + state: StateType; + setState: (s: StateType) => void; +}; + +export default function useSelectedConnection( + conn: DatabaseConnectionFragment, +): ReturnType { + const connectionsRes = useStoredConnectionsQuery(); + const storedConnections = connectionsRes.data?.storedConnections || []; + const [state, setState] = useSetState({ + databases: [] as string[], + connection: conn, + err: undefined as ApolloErrorType | undefined, + loading: false, + }); + const [getDbs] = useDatabasesByConnectionLazyQuery(); + + const onSelected = async (connection: DatabaseConnectionFragment) => { + setState({ + loading: true, + err: undefined, + }); + try { + const dbs = await getDbs({ + variables: connection, + }); + setState({ + connection, + databases: dbs.data?.databasesByConnection || [], + }); + } catch (e) { + handleCaughtApolloError(e, er => setState({ err: er })); + } finally { + setState({ loading: false }); + } + }; + + return { onSelected, state, setState, storedConnections }; +} diff --git a/web/renderer/components/CreateDatabase/index.module.css b/web/renderer/components/CreateDatabase/index.module.css index 8342886a..cca3702c 100644 --- a/web/renderer/components/CreateDatabase/index.module.css +++ b/web/renderer/components/CreateDatabase/index.module.css @@ -2,6 +2,10 @@ @apply text-sm flex items-center; svg { - @apply mr-1; + @apply h-4 w-4; } } + +.text { + @apply ml-2; +} diff --git a/web/renderer/components/CreateDatabase/index.tsx b/web/renderer/components/CreateDatabase/index.tsx index 5fec510a..1106b18b 100644 --- a/web/renderer/components/CreateDatabase/index.tsx +++ b/web/renderer/components/CreateDatabase/index.tsx @@ -7,6 +7,7 @@ import { } from "@gen/graphql-types"; 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"; @@ -16,6 +17,7 @@ import css from "./index.module.css"; type Props = { buttonClassName?: string; isPostgres: boolean; + showText?: boolean; }; export default function CreateDatabase(props: Props) { @@ -54,8 +56,8 @@ export default function CreateDatabase(props: Props) { onClick={() => setIsOpen(true)} className={cx(css.createDB, props.buttonClassName)} > - - Create database + {props.showText ? : } + {props.showText && Create database} + diff --git a/web/renderer/components/DatabaseHeaderAndNav/AddItemDropdown/index.module.css b/web/renderer/components/DatabaseHeaderAndNav/AddItemDropdown/index.module.css index 2ea2d584..0385233e 100644 --- a/web/renderer/components/DatabaseHeaderAndNav/AddItemDropdown/index.module.css +++ b/web/renderer/components/DatabaseHeaderAndNav/AddItemDropdown/index.module.css @@ -23,7 +23,7 @@ } .button { - @apply flex justify-center items-center pl-2 pr-1 h-8 text-white text-sm font-normal border-opaque-rounded bg-white/10 w-20 mr-3; + @apply flex justify-center items-center pl-2 pr-1 h-7 text-white text-sm font-normal border-opaque-rounded bg-white/10 w-20 mr-3; &:hover { @apply button-shadow; diff --git a/web/renderer/components/DatabaseHeaderAndNav/Full.tsx b/web/renderer/components/DatabaseHeaderAndNav/Full.tsx index 1df472cf..44a590eb 100644 --- a/web/renderer/components/DatabaseHeaderAndNav/Full.tsx +++ b/web/renderer/components/DatabaseHeaderAndNav/Full.tsx @@ -1,7 +1,5 @@ import DatabaseNav from "@components/DatabaseNav"; import MobileDatabaseNav from "@components/DatabaseNav/ForMobile"; -import DatabaseTypeLabel from "@components/DatabaseTypeLabel"; -import DatabaseBreadcrumbs from "@components/breadcrumbs/DatabaseBreadcrumbs"; import { OptionalRefParams } from "@lib/params"; import cx from "classnames"; import RightHeaderButtons from "./RightHeaderButtons"; @@ -11,7 +9,6 @@ type Props = { params: OptionalRefParams & { schemaName?: string }; title?: string; initialTabIndex: number; - setShowSmall: (s: boolean) => void; showSmall: boolean; }; @@ -22,18 +19,8 @@ export default function Full(props: Props) { className={cx(css.header, { [css.hideFullHeader]: props.showSmall })} >
-
- - -
-
- props.setShowSmall(true)} - /> +
+
diff --git a/web/renderer/components/DatabaseHeaderAndNav/RefreshConnectionButton.tsx b/web/renderer/components/DatabaseHeaderAndNav/RefreshConnectionButton.tsx index d43febf2..cfa1f477 100644 --- a/web/renderer/components/DatabaseHeaderAndNav/RefreshConnectionButton.tsx +++ b/web/renderer/components/DatabaseHeaderAndNav/RefreshConnectionButton.tsx @@ -42,7 +42,7 @@ export default function ResetConnectionButton() { - + void; }; export default function RightHeaderButtons(props: Props) { @@ -15,11 +12,6 @@ export default function RightHeaderButtons(props: Props) {
-
- - - -
); } diff --git a/web/renderer/components/DatabaseHeaderAndNav/Small.tsx b/web/renderer/components/DatabaseHeaderAndNav/Small.tsx index dac0bffb..27ca22fd 100644 --- a/web/renderer/components/DatabaseHeaderAndNav/Small.tsx +++ b/web/renderer/components/DatabaseHeaderAndNav/Small.tsx @@ -8,14 +8,12 @@ import css from "./index.module.css"; type Props = { params: OptionalRefParams & { schemaName?: string }; breadcrumbs?: ReactNode; - setShowSmall: (s: boolean) => void; showSmall: boolean; }; export default function SmallDBHeader(props: Props) { return (
@@ -28,10 +26,7 @@ export default function SmallDBHeader(props: Props) {
- props.setShowSmall(false)} - /> +
); diff --git a/web/renderer/components/DatabaseHeaderAndNav/index.module.css b/web/renderer/components/DatabaseHeaderAndNav/index.module.css index 30044515..5e158d2d 100644 --- a/web/renderer/components/DatabaseHeaderAndNav/index.module.css +++ b/web/renderer/components/DatabaseHeaderAndNav/index.module.css @@ -1,5 +1,5 @@ .header { - @apply flex flex-col text-white; + @apply flex flex-col text-white absolute w-full top-12; } .hideFullHeader { @@ -11,10 +11,10 @@ } .headerDetails { - @apply inline-block h-fit py-6 px-8 justify-between bg-storm-500 relative; - + @apply inline-block py-2 px-8 bg-storm-500 relative h-28 justify-end; + background-image: linear-gradient(to bottom, #354c5c, #192e3d); @screen lg { - @apply flex pt-6 h-28; + @apply flex h-12; } } @@ -33,6 +33,10 @@ } } +.zIndex { + @apply z-10; +} + .topRight { @apply flex mt-4 justify-start items-center; > * { @@ -83,14 +87,6 @@ } } -.menu { - @apply hidden; - - @screen lg { - @apply mt-1 ml-3 block; - } -} - .resetButton { @apply mr-5 text-xl text-white hover:text-stone-50 hidden lg:block; } @@ -98,3 +94,15 @@ .tooltip { @apply z-10; } + +.forAppNav { + @screen lg { + @apply h-16 pt-4 justify-end; + } +} + +.hideForApp { + @screen lg { + @apply hidden; + } +} diff --git a/web/renderer/components/DatabaseTableNav/NavLinks.tsx b/web/renderer/components/DatabaseTableNav/NavLinks.tsx index 1e7dd71d..bf7654fa 100644 --- a/web/renderer/components/DatabaseTableNav/NavLinks.tsx +++ b/web/renderer/components/DatabaseTableNav/NavLinks.tsx @@ -1,10 +1,17 @@ import DefinitionList from "@components/DefinitionList"; import TableList from "@components/TableList"; import Views from "@components/Views"; -import { Tab, TabList, TabPanel, Tabs } from "@dolthub/react-components"; +import { + Tab, + TabList, + TabPanel, + Tabs, + useTabsContext, +} from "@dolthub/react-components"; import { DatabasePageParams, OptionalRefParams } from "@lib/params"; import { useRouter } from "next/router"; import { ReactNode } from "react"; +import cx from "classnames"; import css from "./index.module.css"; type Props = { @@ -23,9 +30,7 @@ export default function NavLinks({ className, params }: Props) { {tabs.map((tab, i) => ( - - {tab} - + ))} + {tab} + + ); +} diff --git a/web/renderer/components/DatabaseTableNav/NewBranchLink.tsx b/web/renderer/components/DatabaseTableNav/NewBranchLink.tsx index 78aaf591..70478ef5 100644 --- a/web/renderer/components/DatabaseTableNav/NewBranchLink.tsx +++ b/web/renderer/components/DatabaseTableNav/NewBranchLink.tsx @@ -25,7 +25,7 @@ export default function NewBranchLink(props: Props) { data-tooltip-content={ props.doltDisabled ? "Use Dolt to create branch" : "Create new branch" } - data-tooltip-place="top" + data-tooltip-place="top-end" >
-
- - - -
- - - - - + {open && ( + <> +
+ + + +
+ + + + + + + )}
- {isPostgres && params.refName && ( -
- -
- )} +
+ {isPostgres && params.refName && ( +
+ +
+ )} +
diff --git a/web/renderer/components/DefinitionList/Item.tsx b/web/renderer/components/DefinitionList/Item.tsx index 1c5dc0aa..6ecfeebe 100644 --- a/web/renderer/components/DefinitionList/Item.tsx +++ b/web/renderer/components/DefinitionList/Item.tsx @@ -30,7 +30,7 @@ export default function Item({ name, params, isActive, query }: Props) { {excerpt(name, 45)} - {isActive ? "Viewing" : } + {isActive ? "viewing" : } diff --git a/web/renderer/components/DefinitionList/index.module.css b/web/renderer/components/DefinitionList/index.module.css index cc245450..a4b1781e 100644 --- a/web/renderer/components/DefinitionList/index.module.css +++ b/web/renderer/components/DefinitionList/index.module.css @@ -2,16 +2,16 @@ @apply mb-10; h4 { - @apply mx-3 mb-1.5 mt-3 text-base; + @apply mx-3 mb-1.5 mt-3 text-base text-storm-50; } } .text { - @apply mb-5 mt-2 mx-5; + @apply mb-5 mt-2 mx-5 text-white; } .icon { - @apply text-xl mr-2 text-primary rounded-full opacity-25; + @apply text-xl mr-2 text-white rounded-full opacity-20; } .item { @@ -22,11 +22,10 @@ } } -.item:hover .icon { - @apply opacity-100; - - &:hover { - @apply text-sky-900; +.item:hover { + .icon, + .name { + @apply text-storm-600; } } @@ -40,15 +39,15 @@ .selected, .item:hover { - @apply bg-white text-link-2; + @apply bg-storm-200 text-link-2; } .name { - @apply text-link-1 text-sm font-semibold; + @apply text-white text-sm font-normal; } .viewing { - @apply py-0.5 mr-4 text-coral-400 font-semibold text-sm; + @apply mr-3 text-coral-400 bg-white rounded-full px-2 font-normal text-sm; } .smallLoader { diff --git a/web/renderer/components/FormSelectForRefs/BranchAndTagSelector.tsx b/web/renderer/components/FormSelectForRefs/BranchAndTagSelector.tsx index b71c9012..b714347d 100644 --- a/web/renderer/components/FormSelectForRefs/BranchAndTagSelector.tsx +++ b/web/renderer/components/FormSelectForRefs/BranchAndTagSelector.tsx @@ -3,7 +3,6 @@ import { Maybe } from "@dolthub/web-utils"; import { useTableNamesForBranchLazyQuery } from "@gen/graphql-types"; import { DatabasePageParams } from "@lib/params"; import { RefUrl, branches, ref, releases } from "@lib/urls"; -import cx from "classnames"; import { useRouter } from "next/router"; import getGroupOption from "./getGroupOption"; import css from "./index.module.css"; @@ -15,7 +14,6 @@ type Props = { selectedValue?: string; routeRefChangeTo: RefUrl; className?: string; - isPostgres?: boolean; }; export default function BranchAndTagSelector(props: Props) { @@ -84,15 +82,10 @@ export default function BranchAndTagSelector(props: Props) { onChange={async e => handleChangeRef(e?.value)} options={options} placeholder="select a branch or tag..." - outerClassName={cx( - { [css.outerForPostgres]: !!props.isPostgres }, - props.className, - )} + outerClassName={props.className} labelClassName={css.branchLabel} - className={cx(css.branchAndTagSelect, { - [css.selectForPostgres]: !!props.isPostgres, - })} - label={props.isPostgres ? "Branch" : undefined} + className={css.branchAndTagSelect} + label="Branch:" horizontal selectedOptionFirst light diff --git a/web/renderer/components/FormSelectForRefs/DoltDisabledSelector.tsx b/web/renderer/components/FormSelectForRefs/DoltDisabledSelector.tsx index 28dca529..f0e1efed 100644 --- a/web/renderer/components/FormSelectForRefs/DoltDisabledSelector.tsx +++ b/web/renderer/components/FormSelectForRefs/DoltDisabledSelector.tsx @@ -13,9 +13,7 @@ export default function DoltDisabledSelector(props: Props) {
{props.showLabel && Branch}
{ - window.ipc.macTitlebarClicked(); -}; - -export default function DesktopAppNavbar() { - return ( -
- } - leftLinks={} - rightLinks={
} - bgColor="bg-storm-600" - /> -
- ); -} - -function LeftLinks() { - return ( -
- Connections -
- ); -} - -function Logo() { - return ( - - Dolt Workbench - - ); -} diff --git a/web/renderer/components/Navbar/index.module.css b/web/renderer/components/Navbar/index.module.css index f2e9c02d..c2084f8f 100644 --- a/web/renderer/components/Navbar/index.module.css +++ b/web/renderer/components/Navbar/index.module.css @@ -1,8 +1,66 @@ -.leftLinks { - @apply ml-20 flex items-center; +.titleBar { + @apply bg-storm-600 text-white w-full z-100; + @screen lg { + @apply h-12 fixed flex justify-end items-center; + } + a, + button { + @apply cursor-pointer; + -webkit-app-region: no-drag; + } + -webkit-app-region: no-drag; +} + +.center { + @apply justify-center; } -.titlebar { +.outer { + @apply hidden justify-between items-center pr-8; + @screen md { + @apply flex; + width: calc(50% + 8rem); + } +} + +.desktopOuter { + @apply hidden; + @screen md { + @apply flex justify-between items-center w-full; + } +} + +.left, +.middle, +.right { + @apply w-1/3; +} + +.left { + @apply pl-8; +} + +.middle { + @apply justify-center; +} + +.right { + @apply flex justify-end pr-8; + + a, + button { + @apply text-sm font-normal tracking-wide text-white/90 hover:text-sky-100; + } + + a { + @apply mr-2 ml-10 flex items-center; + > svg { + @apply mr-3; + } + } +} + +.drag { -webkit-app-region: drag; a, button { @@ -10,3 +68,13 @@ -webkit-app-region: no-drag; } } + +.logo { + @apply max-h-5; +} + +.forMobile { + @screen md { + @apply hidden; + } +} diff --git a/web/renderer/components/Navbar/index.tsx b/web/renderer/components/Navbar/index.tsx index 10563225..836ac5c8 100644 --- a/web/renderer/components/Navbar/index.tsx +++ b/web/renderer/components/Navbar/index.tsx @@ -1,39 +1,98 @@ -import DocsLink from "@components/links/DocsLink"; +import { DatabaseParams } from "@lib/params"; +import ConnectionsAndDatabases from "@components/ConnectionsAndDatabases"; import Link from "@components/links/Link"; -import { ExternalLink, Navbar } from "@dolthub/react-components"; +import { useState } from "react"; +import cx from "classnames"; import { dockerHubRepo, workbenchGithubRepo } from "@lib/constants"; -import { FaDocker } from "@react-icons/all-files/fa/FaDocker"; +import { ExternalLink, MobileNavbar } from "@dolthub/react-components"; import { FaGithub } from "@react-icons/all-files/fa/FaGithub"; -import DesktopAppNavbar from "./DesktopAppNavbar"; +import { FaDocker } from "@react-icons/all-files/fa/FaDocker"; +import DocsLink from "@components/links/DocsLink"; +import css from "./index.module.css"; // TODO: Support desktop app nav bar on windows const forMacNav = process.env.NEXT_PUBLIC_FOR_MAC_NAV === "true"; -export default function Nav() { - return forMacNav ? ( - - ) : ( - } - leftLinks={} - rightLinks={} - bgColor="bg-storm-600" - /> +const handleDoubleClick = (e: React.MouseEvent) => { + if (e.currentTarget === e.target) { + window.ipc.macTitlebarClicked(); + } +}; + +type Props = { + params?: DatabaseParams; +}; + +export default function DesktopAppNavbar({ params }: Props) { + const [noDrag, setNoDrag] = useState(false); + + return ( +
+ {params ? ( + + ) : ( + + )} +
); } -function LeftLinks() { +type InnerProps = { + params: DatabaseParams; + setNoDrag: (noDrag: boolean) => void; +}; + +function Inner({ params, setNoDrag }: InnerProps) { + if (forMacNav) { + return ( +
+ + +
+ ); + } return ( <> - Documentation - Connections +
+ + +
+ +
+
+ } + bgColor="bg-storm-600" + > + Connections + + ); } +type LogoProps = { + imgSrc: string; + className?: string; +}; + +export function Logo({ imgSrc, className }: LogoProps) { + return ( + + Dolt Workbench + + ); +} function RightLinks() { return ( <> + Documentation GitHub @@ -43,11 +102,3 @@ function RightLinks() { ); } - -function Logo() { - return ( - - Dolt Workbench - - ); -} diff --git a/web/renderer/components/Navbar/useSelectedConnection.ts b/web/renderer/components/Navbar/useSelectedConnection.ts deleted file mode 100644 index 7c103233..00000000 --- a/web/renderer/components/Navbar/useSelectedConnection.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - DatabaseType, - useDatabasesByConnectionLazyQuery, - useStoredConnectionsQuery, -} from "@gen/graphql-types"; -import useApolloError from "@hooks/useApolloError"; -import { handleCaughtApolloError } from "@lib/errors/helpers"; -import { ApolloErrorType } from "@lib/errors/types"; -import { useState } from "react"; - -type ReturnType = { - onSelected: (connectionName: string) => Promise; - databases: string[]; - loading: boolean; - err: ApolloErrorType | undefined; -}; - -export default function useSelectedConnection(): ReturnType { - const connectionsRes = useStoredConnectionsQuery(); - const [databases, setDatabases] = useState([]); - const [getDbs] = useDatabasesByConnectionLazyQuery(); - const [loading, setLoading] = useState(connectionsRes.loading); - const [err, setErr] = useApolloError(connectionsRes.error); - - const onSelected = async (connectionName: string) => { - setLoading(true); - setErr(undefined); - try { - const selected = connectionsRes.data?.storedConnections.find( - c => c.name === connectionName, - ); - if (!selected) { - setErr(new Error("Connection not found")); - return; - } - const dbs = await getDbs({ - variables: { - connectionUrl: selected.connectionUrl || "", - type: selected.type || DatabaseType.Mysql, - name: selected.name || "", - useSSL: selected.useSSL || true, - }, - }); - setDatabases(dbs.data?.databasesByConnection || []); - } catch (e) { - handleCaughtApolloError(e, setErr); - } finally { - setLoading(false); - } - }; - - return { onSelected, databases, loading, err }; -} diff --git a/web/renderer/components/SchemaDiagramButton/index.module.css b/web/renderer/components/SchemaDiagramButton/index.module.css index 14df9c2c..01efe7bb 100644 --- a/web/renderer/components/SchemaDiagramButton/index.module.css +++ b/web/renderer/components/SchemaDiagramButton/index.module.css @@ -1,5 +1,8 @@ .diagram { - @apply flex text-white ml-3 mt-2 text-base; + @apply flex text-white text-base bg-[#FF8964] px-6 items-center; + &:hover { + @apply bg-coral-400; + } svg { @apply mt-1 mr-2 text-sm; diff --git a/web/renderer/components/SchemasSelector/CreateSchema.tsx b/web/renderer/components/SchemasSelector/CreateSchema.tsx index 1374e414..042930bf 100644 --- a/web/renderer/components/SchemasSelector/CreateSchema.tsx +++ b/web/renderer/components/SchemasSelector/CreateSchema.tsx @@ -53,7 +53,7 @@ export default function CreateSchema(props: Props) { data-tooltip-content="Create new schema" data-tooltip-place="top" > - + { return { @@ -41,7 +45,7 @@ export function Selector(props: SelectorProps) { }; })} hideSelectedOptions - label="Schema" + label="Schema:" horizontal={props.horizontal} light /> @@ -82,12 +86,12 @@ export default function SchemasSelector(props: Props) { }; return ( - + diff --git a/web/renderer/components/TableList/Item/Right.tsx b/web/renderer/components/TableList/Item/Right.tsx index 16f346da..040b53cd 100644 --- a/web/renderer/components/TableList/Item/Right.tsx +++ b/web/renderer/components/TableList/Item/Right.tsx @@ -39,7 +39,7 @@ export default function Right(props: { params: TableParams; active: boolean }) { {props.active && ( - {editing ? "Editing" : "Viewing"} + {editing ? "editing" : "viewing"} )} diff --git a/web/renderer/components/TableList/Item/index.module.css b/web/renderer/components/TableList/Item/index.module.css index 055dedd5..b2b9c740 100644 --- a/web/renderer/components/TableList/Item/index.module.css +++ b/web/renderer/components/TableList/Item/index.module.css @@ -3,7 +3,7 @@ } .buttonIcon { - @apply text-primary opacity-25 text-2xl mr-4 h-6 mt-1 rounded-full; + @apply hidden text-sky-900 text-2xl mr-4 h-6 rounded-full font-normal; &:focus { @apply widget-shadow-lightblue outline-none; @@ -12,43 +12,48 @@ .active, .item:hover { - @apply bg-white; - - .buttonIcon { - @apply font-semibold; - } + @apply bg-storm-50 text-storm-600; } .item:hover .buttonIcon { - @apply opacity-100; + @apply block; +} - &:hover { - @apply text-sky-900; +.isExpanded { + @apply bg-[#fafbfc]; + .buttonIcon { + @apply block; } } -.isExpanded { - background-color: #fafbfc; +.isExpanded .tableName { + @apply text-storm-600 font-normal; +} + +.isExpanded .table { + @apply text-storm-600 font-normal bg-storm-50; } .table { - @apply flex justify-between text-sm; + @apply flex justify-between items-center text-sm; + &:hover { + @apply bg-storm-50; + .tableName { + @apply text-storm-600; + } + } } .tableName { @apply hidden; @screen lg { - @apply flex p-3 text-link-1 font-semibold w-full text-sm; + @apply flex p-3 text-white w-full text-sm; svg { @apply inline-block text-sm mt-1 mr-2; } - &:hover { - @apply text-link-2; - } - &:focus { @apply outline-none; @@ -60,11 +65,11 @@ } .mobileTableLink { - @apply flex justify-between mx-2 p-3 text-link-1 font-semibold w-full text-sm lg:hidden; + @apply flex justify-between mx-2 p-3 text-link-1 font-normal w-full text-sm lg:hidden; } .right { - @apply flex items-center mt-1; + @apply flex items-center; a { @apply hidden lg:block; @@ -72,5 +77,5 @@ } .tableStatus { - @apply text-coral-400 pr-4 mr-1 font-semibold; + @apply text-coral-400 bg-white rounded-full px-2 mr-3 font-normal; } diff --git a/web/renderer/components/TableList/index.module.css b/web/renderer/components/TableList/index.module.css index 0ca36080..71d60aec 100644 --- a/web/renderer/components/TableList/index.module.css +++ b/web/renderer/components/TableList/index.module.css @@ -1,10 +1,14 @@ +.tableList { + @apply pb-20; +} + .addTable { @apply hidden; @screen lg { - @apply flex my-8 mx-3 text-sm text-sky-900; + @apply flex my-8 mx-3 text-sm text-white font-normal; &:hover { - @apply text-link-2; + @apply text-[#FF8964]; } svg { @@ -14,5 +18,9 @@ } .empty { - @apply my-4 mx-5; + @apply my-4 mx-5 text-white; +} + +.bottom { + @apply bg-storm-500 flex items-center justify-between px-6 border-t border-white border-opacity-10 fixed bottom-0 min-w-96 w-96; } diff --git a/web/renderer/components/TableList/index.tsx b/web/renderer/components/TableList/index.tsx index 3f13a71e..186b752a 100644 --- a/web/renderer/components/TableList/index.tsx +++ b/web/renderer/components/TableList/index.tsx @@ -7,7 +7,7 @@ import { Maybe } from "@dolthub/web-utils"; import useTableNames from "@hooks/useTableNames"; import { RefOptionalSchemaParams } from "@lib/params"; import { createTable } from "@lib/urls"; -import { AiOutlinePlus } from "@react-icons/all-files/ai/AiOutlinePlus"; +import { AiOutlinePlusCircle } from "@react-icons/all-files/ai/AiOutlinePlusCircle"; import { useEffect } from "react"; import Item from "./Item"; import css from "./index.module.css"; @@ -33,25 +33,25 @@ function Inner(props: InnerProps) {
{props.tables.length ? ( - <> -
    - {props.tables.map(t => ( - - ))} -
- - +
    + {props.tables.map(t => ( + + ))} +
) : (

No tables found for {props.params.refName}

)} - - - - Add new table - - +
+ {props.tables.length > 0 && } + + + + Add new table + + +
); } diff --git a/web/renderer/components/Views/ViewItem.tsx b/web/renderer/components/Views/ViewItem.tsx index 2f48bce0..606d54df 100644 --- a/web/renderer/components/Views/ViewItem.tsx +++ b/web/renderer/components/Views/ViewItem.tsx @@ -36,7 +36,7 @@ export default function ViewItem(props: Props) { className={viewingQuery ? css.viewing : css.icon} data-cy={`db-views-view-button-${name}`} > - {viewingQuery ? "Viewing" : } + {viewingQuery ? "viewing" : }
diff --git a/web/renderer/components/Views/index.module.css b/web/renderer/components/Views/index.module.css index 543ce9c5..8b3e4621 100644 --- a/web/renderer/components/Views/index.module.css +++ b/web/renderer/components/Views/index.module.css @@ -3,11 +3,17 @@ } .text { - @apply my-4 mx-5; + @apply my-4 mx-5 text-storm-50; + a { + @apply text-white; + &:hover { + @apply text-sky-300; + } + } } .icon { - @apply text-2xl mr-2 text-primary rounded-full opacity-25; + @apply text-2xl mr-2 text-white rounded-full; } .item { @@ -18,14 +24,6 @@ } } -.item:hover .icon { - @apply opacity-100; - - &:hover { - @apply text-sky-900; - } -} - .button { @apply flex justify-between items-center w-full; @@ -34,15 +32,18 @@ } } -.selected, .item:hover { - @apply bg-white text-link-2; + @apply bg-storm-50; + .name, + .icon { + @apply text-storm-600; + } } .name { - @apply text-link-1 text-sm font-semibold; + @apply text-white text-sm font-semibold; } .viewing { - @apply py-0.5 mr-4 text-coral-400 font-semibold text-sm; + @apply py-0.5 mr-3 text-coral-400 bg-white px-2 rounded-full font-semibold text-sm; } diff --git a/web/renderer/components/layouts/DatabaseLayout/Wrapper.tsx b/web/renderer/components/layouts/DatabaseLayout/Wrapper.tsx index 4aaf57f9..814b78e1 100644 --- a/web/renderer/components/layouts/DatabaseLayout/Wrapper.tsx +++ b/web/renderer/components/layouts/DatabaseLayout/Wrapper.tsx @@ -7,14 +7,20 @@ import { gqlDatabaseNotFoundErr } from "@lib/errors/graphql"; import { errorMatches } from "@lib/errors/helpers"; import { useRouter } from "next/router"; import { ReactNode } from "react"; +import { DatabaseParams } from "@lib/params"; import css from "./index.module.css"; type Props = { children: ReactNode; + params: DatabaseParams; +}; + +type InnerProps = { + children: ReactNode; }; // eslint-disable-next-line @typescript-eslint/promise-function-async -function Inner(props: Props) { +function Inner(props: InnerProps) { const router = useRouter(); const res = useCurrentDatabaseQuery(); if (res.loading) { @@ -35,7 +41,7 @@ export default function DatabaseLayoutWrapper(props: Props) { const { toggleSqlEditor } = useSqlEditorContext(); const { keyMap, handlers } = useHotKeysForToggle(toggleSqlEditor); return ( - + {props.children} @@ -45,9 +51,9 @@ export default function DatabaseLayoutWrapper(props: Props) { export function DatabaseLayoutWrapperOuter(props: Props) { return (
- +
- {props.children} + {props.children}
); diff --git a/web/renderer/components/layouts/DatabaseLayout/index.module.css b/web/renderer/components/layouts/DatabaseLayout/index.module.css index 4e89830f..d3a6ed78 100644 --- a/web/renderer/components/layouts/DatabaseLayout/index.module.css +++ b/web/renderer/components/layouts/DatabaseLayout/index.module.css @@ -12,10 +12,10 @@ } .content { - @apply flex flex-col relative bottom-0 left-0 right-0 top-0 bg-stone-50; + @apply flex flex-col relative bottom-0 left-0 right-0 top-32 bg-stone-50; @screen lg { - @apply flex-row absolute top-40; + @apply flex-row absolute top-24; } } diff --git a/web/renderer/components/layouts/DatabaseLayout/index.tsx b/web/renderer/components/layouts/DatabaseLayout/index.tsx index 08c1f13d..b8ed395c 100644 --- a/web/renderer/components/layouts/DatabaseLayout/index.tsx +++ b/web/renderer/components/layouts/DatabaseLayout/index.tsx @@ -37,7 +37,7 @@ export default function DatabaseLayout(props: Props) { const { isMobile } = useReactiveWidth(1024); const [showTableNav, setShowTableNav] = useState(false); return ( - + void; + onDeleteClicked?: (n: string) => void; }; export default function Item({ conn, onDeleteClicked }: Props) { @@ -20,9 +20,11 @@ export default function Item({ conn, onDeleteClicked }: Props) { {conn.name} - onDeleteClicked(conn.name)}> - - + {onDeleteClicked && ( + onDeleteClicked(conn.name)}> + + + )} @@ -35,7 +37,7 @@ type LabelProps = { conn: DatabaseConnectionFragment; }; -function DatabaseTypeLabel({ conn }: LabelProps) { +export function DatabaseTypeLabel({ conn }: LabelProps) { const type = getDatabaseType(conn.type ?? undefined, !!conn.isDolt); return {type}; } diff --git a/web/renderer/components/pageComponents/DatabasesPage/index.tsx b/web/renderer/components/pageComponents/DatabasesPage/index.tsx index 7329df66..5ba48073 100644 --- a/web/renderer/components/pageComponents/DatabasesPage/index.tsx +++ b/web/renderer/components/pageComponents/DatabasesPage/index.tsx @@ -51,7 +51,11 @@ export default function DatabasesPage() { No databases found. Create a database to get started.

)} - +
diff --git a/web/renderer/components/pageComponents/FileUploadPage/Layout/index.module.css b/web/renderer/components/pageComponents/FileUploadPage/Layout/index.module.css index 05c83099..a0e5a56c 100644 --- a/web/renderer/components/pageComponents/FileUploadPage/Layout/index.module.css +++ b/web/renderer/components/pageComponents/FileUploadPage/Layout/index.module.css @@ -43,7 +43,7 @@ } .outer { - @apply absolute bottom-0 left-0 right-0 top-28 hidden lg:flex; + @apply absolute bottom-0 left-0 right-0 top-32 hidden lg:flex; } .main { diff --git a/web/renderer/components/pageComponents/FileUploadPage/Layout/index.tsx b/web/renderer/components/pageComponents/FileUploadPage/Layout/index.tsx index 48a8b32d..2f230037 100644 --- a/web/renderer/components/pageComponents/FileUploadPage/Layout/index.tsx +++ b/web/renderer/components/pageComponents/FileUploadPage/Layout/index.tsx @@ -14,7 +14,7 @@ type Props = { export default function Layout(props: Props) { return ( - + {/* */}
diff --git a/web/renderer/public/images/dolt-logo.png b/web/renderer/public/images/dolt-logo.png new file mode 100644 index 00000000..3146f897 Binary files /dev/null and b/web/renderer/public/images/dolt-logo.png differ diff --git a/web/renderer/public/images/dolt-workbench-grey.png b/web/renderer/public/images/dolt-workbench-grey.png new file mode 100644 index 00000000..f35ee40d Binary files /dev/null and b/web/renderer/public/images/dolt-workbench-grey.png differ diff --git a/web/renderer/public/images/doltgres-logo.png b/web/renderer/public/images/doltgres-logo.png new file mode 100644 index 00000000..a4c4d8c9 Binary files /dev/null and b/web/renderer/public/images/doltgres-logo.png differ diff --git a/web/renderer/public/images/mysql-logo.png b/web/renderer/public/images/mysql-logo.png new file mode 100644 index 00000000..70c3b0b0 Binary files /dev/null and b/web/renderer/public/images/mysql-logo.png differ diff --git a/web/renderer/public/images/postgres-logo.png b/web/renderer/public/images/postgres-logo.png new file mode 100644 index 00000000..91e16400 Binary files /dev/null and b/web/renderer/public/images/postgres-logo.png differ diff --git a/web/yarn.lock b/web/yarn.lock index 89a09d28..4f2bddfc 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -8933,7 +8933,7 @@ __metadata: commit-graph: "npm:^2.3.8" cssnano: "npm:^7.0.6" diff: "npm:^5.1.0" - electron: "npm:^31.0.1" + electron: "npm:33.2.1" electron-builder: "npm:^25.1.8" electron-serve: "npm:^1.3.0" electron-store: "npm:^8.2.0" @@ -9199,16 +9199,16 @@ __metadata: languageName: node linkType: hard -"electron@npm:^31.0.1": - version: 31.7.1 - resolution: "electron@npm:31.7.1" +"electron@npm:33.2.1": + version: 33.2.1 + resolution: "electron@npm:33.2.1" dependencies: "@electron/get": "npm:^2.0.0" "@types/node": "npm:^20.9.0" extract-zip: "npm:^2.0.1" bin: electron: cli.js - checksum: 10c0/8e134b1754b65ebebbb3f5832a068823d0558bc71f831ed007848432910fe9354ef8df0a37ad3ca8b9cf5ebe1c3ae6d47c1cf2186b306cd4ab61a65e2fcd15d1 + checksum: 10c0/25111ab9b48bf95a24b916292c8d4f4d05309b12823d5e942636f377a8033c9c47d466f686895bfe4619b80d222470a439d9b0227516740c8e19fa14991b880b languageName: node linkType: hard