diff --git a/firebase-vscode/package-lock.json b/firebase-vscode/package-lock.json index 524a2780568..b66b9bbdbdf 100644 --- a/firebase-vscode/package-lock.json +++ b/firebase-vscode/package-lock.json @@ -8,6 +8,7 @@ "name": "firebase-vscode", "version": "0.0.24", "dependencies": { + "@preact/signals-react": "^1.3.6", "@vscode/codicons": "0.0.30", "@vscode/webview-ui-toolkit": "^1.2.1", "classnames": "^2.3.2", @@ -347,6 +348,31 @@ "node": ">= 8" } }, + "node_modules/@preact/signals-core": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.4.0.tgz", + "integrity": "sha512-5iYoZBhELLIhUQceZI7sDTQWPb+xcVSn2qk8T/aNl/VMh+A4AiPX9YRSh4XO7fZ6pncrVxl1Iln82poVqYVbbw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@preact/signals-react": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@preact/signals-react/-/signals-react-1.3.6.tgz", + "integrity": "sha512-jr/4lhcRo5W3hfieCJGDPbxq3YjfZDvpmTcisJ+lRhjWvnoYrgMKBoTiLsFPACO8VIEATaIBZXYiGAV08YvnfQ==", + "dependencies": { + "@preact/signals-core": "^1.4.0", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x" + } + }, "node_modules/@teamsupercell/typings-for-css-modules-loader": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/@teamsupercell/typings-for-css-modules-loader/-/typings-for-css-modules-loader-2.5.2.tgz", @@ -5270,6 +5296,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5899,6 +5933,20 @@ "fastq": "^1.6.0" } }, + "@preact/signals-core": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.4.0.tgz", + "integrity": "sha512-5iYoZBhELLIhUQceZI7sDTQWPb+xcVSn2qk8T/aNl/VMh+A4AiPX9YRSh4XO7fZ6pncrVxl1Iln82poVqYVbbw==" + }, + "@preact/signals-react": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@preact/signals-react/-/signals-react-1.3.6.tgz", + "integrity": "sha512-jr/4lhcRo5W3hfieCJGDPbxq3YjfZDvpmTcisJ+lRhjWvnoYrgMKBoTiLsFPACO8VIEATaIBZXYiGAV08YvnfQ==", + "requires": { + "@preact/signals-core": "^1.4.0", + "use-sync-external-store": "^1.2.0" + } + }, "@teamsupercell/typings-for-css-modules-loader": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/@teamsupercell/typings-for-css-modules-loader/-/typings-for-css-modules-loader-2.5.2.tgz", @@ -9463,6 +9511,12 @@ "punycode": "^2.1.0" } }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/firebase-vscode/package.json b/firebase-vscode/package.json index a0fc485c9a3..c4fa2a71091 100644 --- a/firebase-vscode/package.json +++ b/firebase-vscode/package.json @@ -99,6 +99,7 @@ "test": "node ./dist/test/runTest.js" }, "dependencies": { + "@preact/signals-react": "^1.3.6", "@vscode/codicons": "0.0.30", "@vscode/webview-ui-toolkit": "^1.2.1", "classnames": "^2.3.2", diff --git a/firebase-vscode/webviews/SidebarApp.tsx b/firebase-vscode/webviews/SidebarApp.tsx index 9e7c0f71828..0d04d7633e3 100644 --- a/firebase-vscode/webviews/SidebarApp.tsx +++ b/firebase-vscode/webviews/SidebarApp.tsx @@ -22,6 +22,7 @@ export function SidebarApp() { const [channels, setChannels] = useState(null); const [user, setUser] = useState(null); const [framework, setFramework] = useState(null); + /** * null - has not finished checking yet * empty array - finished checking, no users logged in @@ -57,7 +58,7 @@ export function SidebarApp() { } if (firebaseJson?.hosting) { webLogger.debug("Detected firebase.json"); - setHostingInitState('success'); + setHostingInitState("success"); broker.send("showMessage", { msg: "Auto-detected hosting setup in this folder", }); @@ -88,21 +89,24 @@ export function SidebarApp() { setUser(user); }); - broker.on("notifyHostingInitDone", ({ success, projectId, folderPath, framework }) => { - if (success) { - webLogger.debug(`notifyHostingInitDone: ${projectId}, ${folderPath}`); - setHostingInitState('success'); - if (framework) { - setFramework(framework); + broker.on( + "notifyHostingInitDone", + ({ success, projectId, folderPath, framework }) => { + if (success) { + webLogger.debug(`notifyHostingInitDone: ${projectId}, ${folderPath}`); + setHostingInitState("success"); + if (framework) { + setFramework(framework); + } + } else { + setHostingInitState(null); } - } else { - setHostingInitState(null); } - }); + ); broker.on("notifyHostingDeploy", ({ success }) => { webLogger.debug(`notifyHostingDeploy: ${success}`); - setDeployState(success ? 'success' : 'failure'); + setDeployState(success ? "success" : "failure"); }); }, []); @@ -137,7 +141,7 @@ export function SidebarApp() { {!!user && ( )} - {hostingInitState === 'success' && !!user && !!projectId && ( + {hostingInitState === "success" && !!user && !!projectId && ( )} - {hostingInitState !== 'success' && !!user && !!projectId && ( + {hostingInitState !== "success" && !!user && !!projectId && ( { setupHosting(); diff --git a/firebase-vscode/webviews/components/AccountSection.tsx b/firebase-vscode/webviews/components/AccountSection.tsx index 1fb1aef1adc..79726e1061a 100644 --- a/firebase-vscode/webviews/components/AccountSection.tsx +++ b/firebase-vscode/webviews/components/AccountSection.tsx @@ -143,19 +143,21 @@ function UserSelectionMenu({ {!isMonospace && } {allUsersSorted.map((user: UserWithType | ServiceAccountUser) => ( <> - {!isMonospace && ( { - broker.send("requestChangeUser", { user }); - onClose(); - }} - key={user.email} - > - {user?.type === "service_account" - ? isMonospace - ? TEXT.MONOSPACE_LOGIN_SELECTION_ITEM - : TEXT.VSCE_SERVICE_ACCOUNT_SELECTION_ITEM - : user.email} - )} + {!isMonospace && ( + { + broker.send("requestChangeUser", { user }); + onClose(); + }} + key={user.email} + > + {user?.type === "service_account" + ? isMonospace + ? TEXT.MONOSPACE_LOGIN_SELECTION_ITEM + : TEXT.VSCE_SERVICE_ACCOUNT_SELECTION_ITEM + : user.email} + + )} {user?.type === "service_account" && ( { @@ -169,7 +171,9 @@ function UserSelectionMenu({ }} key="service-account-email" > - + )} diff --git a/firebase-vscode/webviews/globals/app.tsx b/firebase-vscode/webviews/globals/app.tsx new file mode 100644 index 00000000000..a9dc46b953a --- /dev/null +++ b/firebase-vscode/webviews/globals/app.tsx @@ -0,0 +1,11 @@ +import React, { ReactNode, StrictMode } from "react"; +import { ExtensionStateProvider } from "./extension-state"; + +/** Generic wrapper that all webviews should be wrapped with */ +export function App({ children }: { children: ReactNode }): JSX.Element { + return ( + + {children} + + ); +} diff --git a/firebase-vscode/webviews/globals/extension-state.tsx b/firebase-vscode/webviews/globals/extension-state.tsx new file mode 100644 index 00000000000..6819f66bf91 --- /dev/null +++ b/firebase-vscode/webviews/globals/extension-state.tsx @@ -0,0 +1,65 @@ +import React, { createContext, ReactNode, useContext, useEffect } from "react"; +import { broker } from "./html-broker"; +import { signal, computed } from "@preact/signals-react"; +import { User } from "../types/auth"; + +export enum Environment { + UNSPECIFIED, + VSC, + IDX, +} + +function createExtensionState() { + const environment = signal(Environment.UNSPECIFIED); + const users = signal([]); + const selectedUserEmail = signal(""); + const projectId = signal(""); + + const selectedUser = computed(() => + users.value.find((user) => user.email === selectedUserEmail.value) + ); + + return { environment, users, projectId, selectedUserEmail, selectedUser }; +} + +const ExtensionState = + createContext>(null); + +/** Global extension state, this should live high in the react-tree to minimize superfluous renders */ +export function ExtensionStateProvider({ + children, +}: { + children: ReactNode; +}): JSX.Element { + const state = createExtensionState(); + + useEffect(() => { + broker.on("notifyEnv", ({ env }) => { + state.environment.value = env.isMonospace + ? Environment.IDX + : Environment.VSC; + }); + + broker.on("notifyUsers", ({ users }) => { + state.users.value = users; + }); + + broker.on("notifyUserChanged", ({ user }) => { + state.selectedUserEmail.value = user.email; + }); + + broker.on("notifyProjectChanged", ({ projectId }) => { + state.projectId.value = projectId; + }); + + broker.send("getInitialData"); + }, [state]); + + return ( + {children} + ); +} + +export function useExtensionState() { + return useContext(ExtensionState); +} diff --git a/firebase-vscode/webviews/sidebar.entry.tsx b/firebase-vscode/webviews/sidebar.entry.tsx index 40d5c965ff0..cd4b91ee6d2 100644 --- a/firebase-vscode/webviews/sidebar.entry.tsx +++ b/firebase-vscode/webviews/sidebar.entry.tsx @@ -1,6 +1,11 @@ import React from "react"; import { createRoot } from "react-dom/client"; import { SidebarApp } from "./SidebarApp"; +import { App } from "./globals/app"; const root = createRoot(document.getElementById("root")!); -root.render(); +root.render( + + + +);