mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
move ProjectSettings to own page, add MigrateToV2
This commit is contained in:
41
src/App.tsx
41
src/App.tsx
@@ -10,6 +10,9 @@ import PrivateRoute from "utils/PrivateRoute";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import EmptyState from "components/EmptyState";
|
||||
import Loading from "components/Loading";
|
||||
import Navigation from "components/Navigation";
|
||||
import Logo from "assets/Logo";
|
||||
import MigrateToV2 from "components/Settings/MigrateToV2";
|
||||
|
||||
import { SnackProvider } from "contexts/SnackContext";
|
||||
import ConfirmationProvider from "components/ConfirmationDialog/Provider";
|
||||
@@ -41,6 +44,8 @@ const TablePage = lazy(() => import("./pages/Table" /* webpackChunkName: "TableP
|
||||
const ProjectSettingsPage = lazy(() => import("./pages/Settings/ProjectSettings" /* webpackChunkName: "ProjectSettingsPage" */));
|
||||
// prettier-ignore
|
||||
const UserSettingsPage = lazy(() => import("./pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
|
||||
// prettier-ignore
|
||||
const UserManagementPage = lazy(() => import("./pages/Settings/UserManagement" /* webpackChunkName: "UserManagementPage" */));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -92,6 +97,7 @@ export default function App() {
|
||||
routes.settings,
|
||||
routes.projectSettings,
|
||||
routes.userSettings,
|
||||
routes.userManagement,
|
||||
]}
|
||||
render={() => (
|
||||
<RowyContextProvider>
|
||||
@@ -99,7 +105,17 @@ export default function App() {
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.home}
|
||||
render={() => <HomePage />}
|
||||
render={() => (
|
||||
<Navigation
|
||||
title={
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Logo />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<HomePage />
|
||||
</Navigation>
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path={routes.tableWithId}
|
||||
@@ -120,14 +136,33 @@ export default function App() {
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.projectSettings}
|
||||
render={() => <ProjectSettingsPage />}
|
||||
render={() => (
|
||||
<Navigation title="Project Settings">
|
||||
<ProjectSettingsPage />
|
||||
</Navigation>
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.userSettings}
|
||||
render={() => <UserSettingsPage />}
|
||||
render={() => (
|
||||
<Navigation title="Settings">
|
||||
<UserSettingsPage />
|
||||
</Navigation>
|
||||
)}
|
||||
/>
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={routes.userManagement}
|
||||
render={() => (
|
||||
<Navigation title="User Management">
|
||||
<UserManagementPage />
|
||||
</Navigation>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
|
||||
<MigrateToV2 />
|
||||
</RowyContextProvider>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon";
|
||||
import { mdiWrenchOutline } from "@mdi/js";
|
||||
|
||||
export default function ProjectSettings(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d={mdiWrenchOutline} />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -9,8 +9,9 @@ import { Typography } from "@material-ui/core";
|
||||
import { alpha } from "@material-ui/core/styles";
|
||||
import Skeleton from "@material-ui/core/Skeleton";
|
||||
|
||||
import { auth, db } from "../../firebase";
|
||||
import { defaultUiConfig, getSignInOptions } from "../../firebase/firebaseui";
|
||||
import { auth, db } from "@src/firebase";
|
||||
import { defaultUiConfig, getSignInOptions } from "@src/firebase/firebaseui";
|
||||
import { PUBLIC_SETTINGS } from "config/dbPaths";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
@@ -47,9 +48,10 @@ const useStyles = makeStyles((theme) =>
|
||||
|
||||
"& .firebaseui-card-content, & .firebaseui-card-footer": { padding: 0 },
|
||||
"& .firebaseui-idp-list, & .firebaseui-tenant-list": { margin: 0 },
|
||||
"& .firebaseui-idp-list>.firebaseui-list-item, & .firebaseui-tenant-list>.firebaseui-list-item": {
|
||||
margin: 0,
|
||||
},
|
||||
"& .firebaseui-idp-list>.firebaseui-list-item, & .firebaseui-tenant-list>.firebaseui-list-item":
|
||||
{
|
||||
margin: 0,
|
||||
},
|
||||
"& .firebaseui-list-item + .firebaseui-list-item": {
|
||||
paddingTop: theme.spacing(2),
|
||||
},
|
||||
@@ -135,9 +137,10 @@ const useStyles = makeStyles((theme) =>
|
||||
...theme.typography.subtitle2,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
"& .mdl-textfield--floating-label.is-dirty .mdl-textfield__label, .mdl-textfield--floating-label.is-focused .mdl-textfield__label": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
"& .mdl-textfield--floating-label.is-dirty .mdl-textfield__label, .mdl-textfield--floating-label.is-focused .mdl-textfield__label":
|
||||
{
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
"& .firebaseui-textfield.mdl-textfield .firebaseui-label:after": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
@@ -186,7 +189,7 @@ export default function FirebaseUi(props: Partial<FirebaseUiProps>) {
|
||||
Parameters<typeof getSignInOptions>[0] | undefined
|
||||
>();
|
||||
useEffect(() => {
|
||||
db.doc("/_rowy_/publicSettings")
|
||||
db.doc(PUBLIC_SETTINGS)
|
||||
.get()
|
||||
.then((doc) => {
|
||||
const options = doc?.get("signInOptions");
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import useDoc from "hooks/useDoc";
|
||||
import Modal from "components/Modal";
|
||||
import { Box, CircularProgress, Typography } from "@material-ui/core";
|
||||
import { IFormDialogProps } from "./Table/ColumnMenu/NewColumn";
|
||||
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
|
||||
|
||||
export interface IProjectSettings
|
||||
extends Pick<IFormDialogProps, "handleClose"> {}
|
||||
|
||||
export default function BuilderInstaller({ handleClose }: IProjectSettings) {
|
||||
const [settingsState] = useDoc({
|
||||
path: "_rowy_/settings",
|
||||
});
|
||||
|
||||
if (settingsState.loading) return null;
|
||||
|
||||
const complete =
|
||||
settingsState.doc.buildStatus === "COMPLETE" ||
|
||||
!!settingsState.doc.buildUrl;
|
||||
const building = settingsState.doc.buildStatus === "BUILDING";
|
||||
const waiting = !settingsState.doc.buildStatus && !settingsState.doc.buildUrl;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
title="One Click Builder Installer"
|
||||
maxWidth="sm"
|
||||
children={
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Typography variant="body2">
|
||||
You will be redirected to Google Cloud Shell to deploy Function
|
||||
Builder to Cloud Run.
|
||||
</Typography>
|
||||
<br />
|
||||
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
flexDirection="column"
|
||||
>
|
||||
{complete && (
|
||||
<>
|
||||
<CheckCircleIcon />
|
||||
<Typography variant="overline">Deploy Complete</Typography>
|
||||
</>
|
||||
)}
|
||||
{building && (
|
||||
<>
|
||||
<CircularProgress size={25} />
|
||||
<Typography variant="overline">Deploying...</Typography>
|
||||
</>
|
||||
)}
|
||||
{waiting && (
|
||||
<>
|
||||
<CircularProgress size={25} />
|
||||
<Typography variant="overline">
|
||||
Waiting for deploy...
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -13,8 +13,8 @@ const ConfirmationProvider: React.FC<IConfirmationProviderProps> = ({
|
||||
const [state, setState] = useState<confirmationProps>();
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleClose = () => {
|
||||
setState(undefined);
|
||||
setOpen(false);
|
||||
setTimeout(() => setState(undefined), 300);
|
||||
};
|
||||
const requestConfirmation = (props: confirmationProps) => {
|
||||
setState(props);
|
||||
|
||||
@@ -2,7 +2,13 @@ import { Zoom, Stack, Typography } from "@material-ui/core";
|
||||
|
||||
export default function HomeWelcomePrompt() {
|
||||
return (
|
||||
<Zoom in style={{ transformOrigin: `${320 - 52}px ${320 - 52}px` }}>
|
||||
<Zoom
|
||||
in
|
||||
style={{
|
||||
transformOrigin: `${320 - 52}px ${320 - 52}px`,
|
||||
transitionDelay: "3s",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useLocation, Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Drawer,
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
} from "@material-ui/core";
|
||||
import HomeIcon from "@material-ui/icons/HomeOutlined";
|
||||
import SettingsIcon from "@material-ui/icons/SettingsOutlined";
|
||||
import ProjectSettingsIcon from "assets/icons/ProjectSettings";
|
||||
import ProjectSettingsIcon from "@material-ui/icons/BuildCircleOutlined";
|
||||
import UserManagementIcon from "@material-ui/icons/AccountCircleOutlined";
|
||||
import CloseIcon from "assets/icons/Backburger";
|
||||
|
||||
import { APP_BAR_HEIGHT } from ".";
|
||||
@@ -37,6 +38,7 @@ export default function NavDrawer({
|
||||
...props
|
||||
}: INavDrawerProps) {
|
||||
const { userClaims, sections } = useRowyContext();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const closeDrawer = (e: {}) => props.onClose(e, "escapeKeyDown");
|
||||
|
||||
@@ -65,7 +67,12 @@ export default function NavDrawer({
|
||||
<nav>
|
||||
<List disablePadding>
|
||||
<li>
|
||||
<MenuItem component={Link} to={routes.home} onClick={closeDrawer}>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={routes.home}
|
||||
selected={pathname === routes.home}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
@@ -75,7 +82,8 @@ export default function NavDrawer({
|
||||
<li>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={routes.settings}
|
||||
to={routes.userSettings}
|
||||
selected={pathname === routes.userSettings}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<ListItemIcon>
|
||||
@@ -89,6 +97,7 @@ export default function NavDrawer({
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={routes.projectSettings}
|
||||
selected={pathname === routes.projectSettings}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<ListItemIcon>
|
||||
@@ -98,6 +107,21 @@ export default function NavDrawer({
|
||||
</MenuItem>
|
||||
</li>
|
||||
)}
|
||||
{userClaims?.roles?.includes("ADMIN") && (
|
||||
<li>
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={routes.userManagement}
|
||||
selected={pathname === routes.userManagement}
|
||||
onClick={closeDrawer}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<UserManagementIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="User Management" />
|
||||
</MenuItem>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<Divider variant="middle" sx={{ mt: 1, mb: 1 }} />
|
||||
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
import { differenceInDays } from "date-fns";
|
||||
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
import {
|
||||
MenuItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
} from "@material-ui/core";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
|
||||
import meta from "../../../package.json";
|
||||
import WIKI_LINKS from "constants/wikiLinks";
|
||||
|
||||
const useLastCheckedUpdateState = createPersistedState(
|
||||
"__ROWY__LAST_CHECKED_UPDATE"
|
||||
);
|
||||
export const useLatestUpdateState = createPersistedState(
|
||||
"__ROWY__LATEST_UPDATE"
|
||||
);
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
secondaryAction: { pointerEvents: "none" },
|
||||
secondaryIcon: {
|
||||
display: "block",
|
||||
color: theme.palette.action.active,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function UpdateChecker() {
|
||||
const classes = useStyles();
|
||||
|
||||
const [lastCheckedUpdate, setLastCheckedUpdate] = useLastCheckedUpdateState<
|
||||
string
|
||||
>();
|
||||
const [latestUpdate, setLatestUpdate] = useLatestUpdateState<null | Record<
|
||||
string,
|
||||
any
|
||||
>>(null);
|
||||
|
||||
const [checkState, setCheckState] = useState<null | "LOADING" | "NO_UPDATE">(
|
||||
null
|
||||
);
|
||||
|
||||
const checkForUpdate = async () => {
|
||||
setCheckState("LOADING");
|
||||
|
||||
// https://docs.github.com/en/rest/reference/repos#get-the-latest-release
|
||||
const endpoint = meta.repository.url
|
||||
.replace("github.com", "api.github.com/repos")
|
||||
.replace(/.git$/, "/releases/latest");
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.tag_name > "v" + meta.version) {
|
||||
setLatestUpdate(json);
|
||||
setCheckState(null);
|
||||
} else {
|
||||
setCheckState("NO_UPDATE");
|
||||
}
|
||||
|
||||
setLastCheckedUpdate(new Date().toISOString());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setLatestUpdate(null);
|
||||
setCheckState("NO_UPDATE");
|
||||
}
|
||||
};
|
||||
|
||||
// Check for new updates on page load, if last check was more than 7 days ago
|
||||
useEffect(() => {
|
||||
if (!lastCheckedUpdate) checkForUpdate();
|
||||
else if (differenceInDays(new Date(), new Date(lastCheckedUpdate)) > 7)
|
||||
checkForUpdate();
|
||||
}, [lastCheckedUpdate]);
|
||||
|
||||
// Verify latest update is not installed yet
|
||||
useEffect(() => {
|
||||
if (latestUpdate?.tag_name <= "v" + meta.version) setLatestUpdate(null);
|
||||
}, [latestUpdate, setLatestUpdate]);
|
||||
|
||||
if (checkState === "LOADING")
|
||||
return <MenuItem disabled>Checking for updates…</MenuItem>;
|
||||
if (checkState === "NO_UPDATE")
|
||||
return <MenuItem disabled>No updates available</MenuItem>;
|
||||
if (latestUpdate === null)
|
||||
return <MenuItem onClick={checkForUpdate}>Check for updates</MenuItem>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={latestUpdate?.html_url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<ListItemText
|
||||
primary="Update available"
|
||||
secondary={latestUpdate?.tag_name}
|
||||
/>
|
||||
<ListItemSecondaryAction className={classes.secondaryAction}>
|
||||
<OpenInNewIcon className={classes.secondaryIcon} />
|
||||
</ListItemSecondaryAction>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={WIKI_LINKS.updating}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<ListItemText secondary="How to update" />
|
||||
<ListItemSecondaryAction className={classes.secondaryAction}>
|
||||
<OpenInNewIcon
|
||||
color="secondary"
|
||||
fontSize="small"
|
||||
className={classes.secondaryIcon}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
Link as MuiLink,
|
||||
Divider,
|
||||
} from "@material-ui/core";
|
||||
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
|
||||
@@ -19,8 +18,6 @@ import ArrowRightIcon from "@material-ui/icons/ArrowRight";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import routes from "constants/routes";
|
||||
import { projectId } from "@src/firebase";
|
||||
import meta from "@root/package.json";
|
||||
|
||||
export default function UserMenu(props: IconButtonProps) {
|
||||
const anchorEl = useRef<HTMLButtonElement>(null);
|
||||
@@ -96,9 +93,27 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
onClose={() => setOpen(false)}
|
||||
sx={{ "& .MuiPaper-root": { minWidth: 160 } }}
|
||||
>
|
||||
<ListItem style={{ cursor: "default" }}>
|
||||
<ListItemAvatar>{avatar}</ListItemAvatar>
|
||||
<ListItemText primary={displayName} secondary={email} />
|
||||
<ListItem
|
||||
sx={{
|
||||
cursor: "default",
|
||||
flexDirection: "column",
|
||||
textAlign: "center",
|
||||
pt: 1.5,
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar
|
||||
sx={{
|
||||
minWidth: 48,
|
||||
"& > *": { width: 48, height: 48, fontSize: 48 },
|
||||
}}
|
||||
>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={displayName}
|
||||
secondary={email}
|
||||
primaryTypographyProps={{ variant: "subtitle1" }}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Divider variant="middle" sx={{ mt: 0.5, mb: 0.5 }} />
|
||||
@@ -150,47 +165,6 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
<MenuItem component={Link} to={routes.signOut}>
|
||||
Sign out
|
||||
</MenuItem>
|
||||
|
||||
<Divider variant="middle" />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={
|
||||
<MuiLink
|
||||
component="a"
|
||||
href={meta.repository.url.replace(".git", "") + "/releases"}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
>
|
||||
{meta.name} v{meta.version}
|
||||
</MuiLink>
|
||||
}
|
||||
secondary={
|
||||
<>
|
||||
Project:{" "}
|
||||
<MuiLink
|
||||
component="a"
|
||||
href={`https://console.firebase.google.com/project/${projectId}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
>
|
||||
{projectId}
|
||||
</MuiLink>
|
||||
</>
|
||||
}
|
||||
primaryTypographyProps={{ variant: "caption", color: "inherit" }}
|
||||
secondaryTypographyProps={{ variant: "caption", color: "inherit" }}
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
color: "text.disabled",
|
||||
margin: 0,
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,22 +7,36 @@ import {
|
||||
IconButton,
|
||||
Box,
|
||||
Typography,
|
||||
Fade,
|
||||
} from "@material-ui/core";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
|
||||
import NavDrawer from "./NavDrawer";
|
||||
import UserMenu from "./UserMenu";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { projectId } from "@src/firebase";
|
||||
|
||||
export const APP_BAR_HEIGHT = 56;
|
||||
|
||||
export interface INavigationProps {
|
||||
children: ReactNode;
|
||||
title?: ReactNode;
|
||||
currentSection?: string;
|
||||
currentTable?: string;
|
||||
}
|
||||
|
||||
export default function Navigation({ children, title }: INavigationProps) {
|
||||
export default function Navigation({
|
||||
children,
|
||||
title,
|
||||
currentSection,
|
||||
currentTable,
|
||||
}: INavigationProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (typeof title === "string")
|
||||
document.title = `${title} | ${projectId} | ${name}`;
|
||||
|
||||
const trigger = useScrollTrigger({ disableHysteresis: true, threshold: 0 });
|
||||
|
||||
return (
|
||||
@@ -30,8 +44,7 @@ export default function Navigation({ children, title }: INavigationProps) {
|
||||
<AppBar
|
||||
position="sticky"
|
||||
color="inherit"
|
||||
elevation={0}
|
||||
className={trigger ? "scrolled" : ""}
|
||||
elevation={trigger ? 1 : 0}
|
||||
sx={{
|
||||
height: APP_BAR_HEIGHT, // Elevation 8
|
||||
backgroundImage:
|
||||
@@ -47,13 +60,9 @@ export default function Navigation({ children, title }: INavigationProps) {
|
||||
left: 0,
|
||||
|
||||
bgcolor: "background.default",
|
||||
opacity: trigger ? 0 : 1,
|
||||
transition: (theme) => theme.transitions.create("opacity"),
|
||||
},
|
||||
|
||||
"&:hover, &.scrolled": {
|
||||
boxShadow: 1,
|
||||
"&::before": { opacity: 0 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
@@ -78,9 +87,9 @@ export default function Navigation({ children, title }: INavigationProps) {
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Box sx={{ flex: 1, ml: 20 / 8, mr: 20 / 8, userSelect: "none" }}>
|
||||
<Box sx={{ flex: 1, userSelect: "none" }}>
|
||||
{typeof title === "string" ? (
|
||||
<Typography variant="h6" component="h1">
|
||||
<Typography variant="h6" component="h1" textAlign="center">
|
||||
{title}
|
||||
</Typography>
|
||||
) : (
|
||||
@@ -93,7 +102,12 @@ export default function Navigation({ children, title }: INavigationProps) {
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<NavDrawer open={open} onClose={() => setOpen(false)} />
|
||||
<NavDrawer
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
currentSection={currentSection}
|
||||
currentTable={currentTable}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</>
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { FieldType } from "@antlerengineering/form-builder";
|
||||
import _startCase from "lodash/startCase";
|
||||
import { authOptions } from "firebase/firebaseui";
|
||||
|
||||
import { Link } from "@material-ui/core";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
import WIKI_LINKS from "constants/wikiLinks";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
export const projectSettingsForm = [
|
||||
{
|
||||
type: FieldType.contentHeader,
|
||||
name: "_contentHeading_signInOptions",
|
||||
label: "Authentication",
|
||||
},
|
||||
{
|
||||
type: FieldType.multiSelect,
|
||||
name: "signInOptions",
|
||||
label: "Sign-In Options",
|
||||
options: Object.keys(authOptions).map((option) => ({
|
||||
value: option,
|
||||
label: _startCase(option).replace("Github", "GitHub"),
|
||||
})),
|
||||
defaultValue: ["google"],
|
||||
required: true,
|
||||
assistiveText: (
|
||||
<>
|
||||
Before enabling a new sign-in option, make sure it’s configured in your
|
||||
Firebase project.
|
||||
<br />
|
||||
<Link
|
||||
href={`https://github.com/firebase/firebaseui-web#configuring-sign-in-providers`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
How to configure sign-in options
|
||||
<OpenInNewIcon
|
||||
aria-label="Open in new tab"
|
||||
fontSize="small"
|
||||
style={{ verticalAlign: "bottom", marginLeft: 4 }}
|
||||
/>
|
||||
</Link>
|
||||
</>
|
||||
) as any,
|
||||
},
|
||||
{
|
||||
type: FieldType.contentHeader,
|
||||
name: "_contentHeading_cloudRun",
|
||||
label: "Functions Builder",
|
||||
},
|
||||
{
|
||||
type: FieldType.shortText,
|
||||
name: "buildUrl",
|
||||
label: "Cloud Run Trigger URL",
|
||||
format: "url",
|
||||
assistiveText: (
|
||||
<>
|
||||
A Cloud Run instance is required to build and deploy {name} Cloud
|
||||
Functions.
|
||||
<Link href={WIKI_LINKS.functions} target="_blank" rel="noopener">
|
||||
Learn more
|
||||
<OpenInNewIcon
|
||||
aria-label="Open in new tab"
|
||||
fontSize="small"
|
||||
style={{ verticalAlign: "bottom", marginLeft: 4 }}
|
||||
/>
|
||||
</Link>
|
||||
<br />
|
||||
To deploy the Cloud Run instance, click the button bellow and follow the
|
||||
Cloud Shell prompts.
|
||||
</>
|
||||
) as any,
|
||||
},
|
||||
];
|
||||
@@ -1,62 +0,0 @@
|
||||
import { FormDialog } from "@antlerengineering/form-builder";
|
||||
import { projectSettingsForm } from "./form";
|
||||
|
||||
import useDoc, { DocActions } from "hooks/useDoc";
|
||||
import { IFormDialogProps } from "components/Table/ColumnMenu/NewColumn";
|
||||
import { Button } from "@material-ui/core";
|
||||
|
||||
export interface IProjectSettings
|
||||
extends Pick<IFormDialogProps, "handleClose"> {
|
||||
handleOpenBuilderInstaller: () => void;
|
||||
}
|
||||
|
||||
export default function ProjectSettings({
|
||||
handleClose,
|
||||
handleOpenBuilderInstaller,
|
||||
}: IProjectSettings) {
|
||||
const [settingsState, settingsDispatch] = useDoc({
|
||||
path: "_rowy_/settings",
|
||||
});
|
||||
const [publicSettingsState, publicSettingsDispatch] = useDoc({
|
||||
path: "_rowy_/publicSettings",
|
||||
});
|
||||
|
||||
if (settingsState.loading || publicSettingsState.loading) return null;
|
||||
|
||||
const handleSubmit = (v) => {
|
||||
const { signInOptions, ...values } = v;
|
||||
|
||||
settingsDispatch({ action: DocActions.update, data: values });
|
||||
publicSettingsDispatch({
|
||||
action: DocActions.update,
|
||||
data: { signInOptions },
|
||||
});
|
||||
};
|
||||
|
||||
const onOpenBuilderInstaller = () => {
|
||||
handleClose();
|
||||
window.open(
|
||||
"https://deploy.cloud.run/?git_repo=https://github.com/rowyio/FunctionsBuilder.git",
|
||||
"_blank"
|
||||
);
|
||||
handleOpenBuilderInstaller();
|
||||
};
|
||||
|
||||
const hasCloudRunConfig = !!settingsState.doc.buildUrl;
|
||||
|
||||
return (
|
||||
<FormDialog
|
||||
onClose={handleClose}
|
||||
title="Project Settings"
|
||||
fields={projectSettingsForm}
|
||||
values={{ ...settingsState.doc, ...publicSettingsState.doc }}
|
||||
onSubmit={handleSubmit}
|
||||
SubmitButtonProps={{ children: "Save" }}
|
||||
formFooter={
|
||||
hasCloudRunConfig ? null : (
|
||||
<Button onClick={onOpenBuilderInstaller}>One click deploy</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
205
src/components/Settings/MigrateToV2.tsx
Normal file
205
src/components/Settings/MigrateToV2.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
|
||||
import { Typography, Link, Button } from "@material-ui/core";
|
||||
import LoadingButton from "@material-ui/lab/LoadingButton";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
import Modal from "components/Modal";
|
||||
|
||||
import { useRowyContext } from "contexts/RowyContext";
|
||||
import { useConfirmation } from "components/ConfirmationDialog";
|
||||
import { db, projectId } from "@src/firebase";
|
||||
import { name } from "@root/package.json";
|
||||
import { SETTINGS, TABLE_SCHEMAS } from "config/dbPaths";
|
||||
import { WIKI_LINKS } from "constants/wikiLinks";
|
||||
|
||||
const useMigrateToV2State = createPersistedState("__ROWY__MIGRATE_TO_V2");
|
||||
|
||||
const checkIfMigrationRequired = async () => {
|
||||
const oldSettingsExists = (await db.doc("_FIRETABLE_/settings").get()).exists;
|
||||
if (!oldSettingsExists) return false;
|
||||
|
||||
const migrated =
|
||||
(await db.doc(SETTINGS).get()).data()?.migratedToV2 !== undefined;
|
||||
if (migrated) return false;
|
||||
|
||||
const tableSchemas = (
|
||||
await db.collection("_FIRETABLE_/settings/schema").get()
|
||||
).size;
|
||||
if (tableSchemas === 0) return false;
|
||||
|
||||
return tableSchemas;
|
||||
};
|
||||
|
||||
const migrate = async () => {
|
||||
const oldSettings = (await db.doc("_FIRETABLE_/settings").get()).data() ?? {};
|
||||
await db.doc(SETTINGS).set(oldSettings, { merge: true });
|
||||
|
||||
const tables = await db.collection("_FIRETABLE_/settings/schema").get();
|
||||
const promises = tables.docs.map((table) =>
|
||||
db
|
||||
.collection(TABLE_SCHEMAS)
|
||||
.doc(table.id)
|
||||
.set(table.data(), { merge: true })
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
await db.doc(SETTINGS).update({ migratedToV2: true });
|
||||
};
|
||||
|
||||
export default function MigrateToV2() {
|
||||
const { userClaims } = useRowyContext();
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
|
||||
const [requiresMigration, setRequiresMigration] = useMigrateToV2State<
|
||||
null | false | number
|
||||
>(null);
|
||||
|
||||
const [migrationStatus, setMigrationStatus] = useState<
|
||||
"IDLE" | "MIGRATING" | "COMPLETE"
|
||||
>("IDLE");
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
Array.isArray(userClaims?.roles) &&
|
||||
userClaims?.roles.includes("ADMIN") &&
|
||||
requiresMigration !== false
|
||||
) {
|
||||
checkIfMigrationRequired().then(setRequiresMigration);
|
||||
}
|
||||
}, [userClaims, requiresMigration, setRequiresMigration]);
|
||||
|
||||
if (migrationStatus === "COMPLETE")
|
||||
return (
|
||||
<Modal
|
||||
title={`Welcome to ${name}!`}
|
||||
onClose={() => {
|
||||
setMigrationStatus("IDLE");
|
||||
setRequiresMigration(false);
|
||||
}}
|
||||
hideCloseButton
|
||||
maxWidth="xs"
|
||||
body="Your project settings and tables have been successfully migrated."
|
||||
actions={{
|
||||
primary: {
|
||||
children: "Continue",
|
||||
onClick: () => {
|
||||
setMigrationStatus("IDLE");
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!requiresMigration) return null;
|
||||
|
||||
const freshStart = async () => {
|
||||
await db.doc(SETTINGS).update({ migratedToV2: false });
|
||||
setRequiresMigration(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`Migrate to ${name}`}
|
||||
onClose={() => {}}
|
||||
disableBackdropClick
|
||||
disableEscapeKeyDown
|
||||
hideCloseButton
|
||||
maxWidth="xs"
|
||||
body={
|
||||
<>
|
||||
<div>
|
||||
<Typography gutterBottom>
|
||||
It looks like your Firestore database is configured for Firetable.
|
||||
You can migrate your configuration, including your{" "}
|
||||
<strong>{requiresMigration} tables</strong>, to {name}.
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
Alternatively, you can{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
requestConfirmation({
|
||||
title: "Continue without migrating?",
|
||||
body: `You will start a new ${name} project without your existing config or tables.`,
|
||||
handleConfirm: freshStart,
|
||||
});
|
||||
}}
|
||||
component="button"
|
||||
color="inherit"
|
||||
variant="body2"
|
||||
display="inline"
|
||||
style={{ verticalAlign: "baseline" }}
|
||||
>
|
||||
skip migration
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
1. Update your Firestore Security Rules
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
Add the required rules to your Firestore Security Rules.
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
href={WIKI_LINKS.securityRules + "#required-rules"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
endIcon={<OpenInNewIcon aria-label="Open in new tab" />}
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Copy Required Rules
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
href={`https://console.firebase.google.com/project/${projectId}/firestore/rules`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
endIcon={<OpenInNewIcon aria-label="Open in new tab" />}
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Set Rules in Firebase Console
|
||||
</Button>
|
||||
|
||||
<div>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
2. Migrate your config and tables
|
||||
</Typography>
|
||||
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
loading={migrationStatus === "MIGRATING"}
|
||||
onClick={() => {
|
||||
setMigrationStatus("MIGRATING");
|
||||
migrate()
|
||||
.then(() => {
|
||||
setMigrationStatus("COMPLETE");
|
||||
setRequiresMigration(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
setMigrationStatus("IDLE");
|
||||
alert(
|
||||
"Failed to migrate. Please check you have set the Firestore Security Rules correctly.\n\n" +
|
||||
e.message
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Migrate
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
208
src/components/Settings/ProjectSettings/About.tsx
Normal file
208
src/components/Settings/ProjectSettings/About.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
import { differenceInDays } from "date-fns";
|
||||
|
||||
import { Grid, Typography, Button, Link, Divider } from "@material-ui/core";
|
||||
import LoadingButton from "@material-ui/lab/LoadingButton";
|
||||
import Logo from "assets/Logo";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
|
||||
import { name, version, repository } from "@root/package.json";
|
||||
import { projectId } from "@src/firebase";
|
||||
import WIKI_LINKS from "constants/wikiLinks";
|
||||
|
||||
const useLastCheckedUpdateState = createPersistedState(
|
||||
"__ROWY__LAST_CHECKED_UPDATE"
|
||||
);
|
||||
export const useLatestUpdateState = createPersistedState(
|
||||
"__ROWY__LATEST_UPDATE"
|
||||
);
|
||||
|
||||
export default function About() {
|
||||
const [lastCheckedUpdate, setLastCheckedUpdate] =
|
||||
useLastCheckedUpdateState<string>();
|
||||
const [latestUpdate, setLatestUpdate] = useLatestUpdateState<null | Record<
|
||||
string,
|
||||
any
|
||||
>>(null);
|
||||
|
||||
const [checkState, setCheckState] = useState<null | "LOADING" | "NO_UPDATE">(
|
||||
null
|
||||
);
|
||||
|
||||
const checkForUpdate = useCallback(async () => {
|
||||
setCheckState("LOADING");
|
||||
|
||||
// https://docs.github.com/en/rest/reference/repos#get-the-latest-release
|
||||
const endpoint = repository.url
|
||||
.replace("github.com", "api.github.com/repos")
|
||||
.replace(/.git$/, "/releases/latest");
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.tag_name > "v" + version) {
|
||||
setLatestUpdate(json);
|
||||
setCheckState(null);
|
||||
} else {
|
||||
setCheckState("NO_UPDATE");
|
||||
}
|
||||
|
||||
setLastCheckedUpdate(new Date().toISOString());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setLatestUpdate(null);
|
||||
setCheckState("NO_UPDATE");
|
||||
}
|
||||
}, [setLastCheckedUpdate, setLatestUpdate]);
|
||||
|
||||
// Check for new updates on page load, if last check was more than 7 days ago
|
||||
useEffect(() => {
|
||||
if (!lastCheckedUpdate) checkForUpdate();
|
||||
else if (differenceInDays(new Date(), new Date(lastCheckedUpdate)) > 7)
|
||||
checkForUpdate();
|
||||
}, [lastCheckedUpdate, checkForUpdate]);
|
||||
|
||||
// Verify latest update is not installed yet
|
||||
useEffect(() => {
|
||||
if (latestUpdate?.tag_name <= "v" + version) setLatestUpdate(null);
|
||||
}, [latestUpdate, setLatestUpdate]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid item>
|
||||
<Logo />
|
||||
</Grid>
|
||||
<Grid item style={{ textAlign: "right" }}>
|
||||
<Link
|
||||
component="a"
|
||||
href={repository.url.replace(".git", "")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="body2"
|
||||
display="block"
|
||||
>
|
||||
GitHub
|
||||
<OpenInNewIcon
|
||||
aria-label="Open in new tab"
|
||||
fontSize="small"
|
||||
sx={{ verticalAlign: "bottom", ml: 0.5 }}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
component="a"
|
||||
href={repository.url.replace(".git", "") + "/releases"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="body2"
|
||||
display="block"
|
||||
>
|
||||
Release notes
|
||||
<OpenInNewIcon
|
||||
aria-label="Open in new tab"
|
||||
fontSize="small"
|
||||
sx={{ verticalAlign: "bottom", ml: 0.5 }}
|
||||
/>
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<div>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid item>
|
||||
<Typography display="block">
|
||||
{name} v{version}
|
||||
</Typography>
|
||||
{latestUpdate === null ? (
|
||||
<Typography color="textSecondary" display="block">
|
||||
Up to date
|
||||
</Typography>
|
||||
) : (
|
||||
<Link
|
||||
href={latestUpdate.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="body2"
|
||||
display="block"
|
||||
>
|
||||
Update available: {latestUpdate.tag_name}
|
||||
<OpenInNewIcon
|
||||
aria-label="Open in new tab"
|
||||
fontSize="small"
|
||||
sx={{ verticalAlign: "bottom", ml: 0.5 }}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
{latestUpdate === null ? (
|
||||
<LoadingButton
|
||||
onClick={checkForUpdate}
|
||||
loading={checkState === "LOADING"}
|
||||
>
|
||||
Check for Updates
|
||||
</LoadingButton>
|
||||
) : (
|
||||
<Button
|
||||
href={WIKI_LINKS.updating}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
endIcon={<OpenInNewIcon aria-label="Open in new tab" />}
|
||||
>
|
||||
How to Update
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid item>
|
||||
<Typography>Firebase Project: {projectId}</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Link
|
||||
href={`https://console.firebase.google.com/project/${projectId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="body2"
|
||||
>
|
||||
Firebase Console
|
||||
<OpenInNewIcon
|
||||
aria-label="Open in new tab"
|
||||
fontSize="small"
|
||||
sx={{ verticalAlign: "bottom", ml: 0.5 }}
|
||||
/>
|
||||
</Link>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
54
src/components/Settings/ProjectSettings/Authentication.tsx
Normal file
54
src/components/Settings/ProjectSettings/Authentication.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useState } from "react";
|
||||
import { authOptions } from "firebase/firebaseui";
|
||||
import _startCase from "lodash/startCase";
|
||||
|
||||
import MultiSelect from "@antlerengineering/multiselect";
|
||||
import { Typography, Link } from "@material-ui/core";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
|
||||
import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings";
|
||||
|
||||
export default function Authentication({
|
||||
publicSettings,
|
||||
updatePublicSettings,
|
||||
}: IProjectSettingsChildProps) {
|
||||
const [signInOptions, setSignInOptions] = useState(
|
||||
Array.isArray(publicSettings?.signInOptions)
|
||||
? publicSettings.signInOptions
|
||||
: ["google"]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MultiSelect
|
||||
label="Sign-In Options"
|
||||
value={signInOptions}
|
||||
options={Object.keys(authOptions).map((option) => ({
|
||||
value: option,
|
||||
label: _startCase(option).replace("Github", "GitHub"),
|
||||
}))}
|
||||
onChange={setSignInOptions}
|
||||
onClose={() => updatePublicSettings({ signInOptions })}
|
||||
multiple
|
||||
TextFieldProps={{ id: "signInOptions" }}
|
||||
/>
|
||||
|
||||
<Typography>
|
||||
Before enabling a new sign-in option, make sure it’s configured in your
|
||||
Firebase project.{" "}
|
||||
<Link
|
||||
href={`https://github.com/firebase/firebaseui-web#configuring-sign-in-providers`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
How to configure sign-in options
|
||||
<OpenInNewIcon
|
||||
aria-label="Open in new tab"
|
||||
fontSize="small"
|
||||
sx={{ verticalAlign: "bottom", ml: 0.5 }}
|
||||
/>
|
||||
</Link>
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
81
src/components/Settings/ProjectSettings/FunctionsBuilder.tsx
Normal file
81
src/components/Settings/ProjectSettings/FunctionsBuilder.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Typography, Link, Grid, TextField } from "@material-ui/core";
|
||||
import LoadingButton from "@material-ui/lab/LoadingButton";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
|
||||
import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings";
|
||||
import WIKI_LINKS from "constants/wikiLinks";
|
||||
import { repository } from "@root/package.json";
|
||||
|
||||
export default function FunctionsBuilder({
|
||||
settings,
|
||||
updateSettings,
|
||||
}: IProjectSettingsChildProps) {
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
Functions Builder is a Cloud Run instance that deploys this project’s
|
||||
Cloud Functions.{" "}
|
||||
<Link
|
||||
href={WIKI_LINKS.functions}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more
|
||||
<OpenInNewIcon
|
||||
aria-label="Open in new tab"
|
||||
fontSize="small"
|
||||
sx={{ verticalAlign: "bottom", ml: 0.5 }}
|
||||
/>
|
||||
</Link>
|
||||
</Typography>
|
||||
|
||||
<div>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Grid item xs={12} sm>
|
||||
<Typography>
|
||||
If you have not yet deployed Functions Builder, click this button
|
||||
and follow the prompts on Cloud Shell.
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
href={`https://deploy.cloud.run/?git_repo=${repository.url
|
||||
.split("/")
|
||||
.slice(0, -1)
|
||||
.join("/")}/FunctionsBuilder.git`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
endIcon={<OpenInNewIcon aria-label="Open in new tab" />}
|
||||
loading={
|
||||
settings.buildStatus === "BUILDING" ||
|
||||
settings.buildStatus === "COMPLETE"
|
||||
}
|
||||
loadingIndicator={
|
||||
settings.buildStatus === "COMPLETE" ? "Deployed" : undefined
|
||||
}
|
||||
>
|
||||
Deploy to Cloud Run
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
label="Cloud Run Instance URL"
|
||||
id="buildUrl"
|
||||
defaultValue={settings.buildUrl}
|
||||
onChange={(e) => updateSettings({ buildUrl: e.target.value })}
|
||||
fullWidth
|
||||
placeholder="https://<id>.run.app"
|
||||
type="url"
|
||||
autoComplete="url"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
src/components/Settings/SettingsSection.tsx
Normal file
36
src/components/Settings/SettingsSection.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Typography, Paper } from "@material-ui/core";
|
||||
|
||||
export interface ISettingsSectionProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function SettingsSection({
|
||||
children,
|
||||
title,
|
||||
}: ISettingsSectionProps) {
|
||||
return (
|
||||
<section style={{ cursor: "default" }}>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="h2"
|
||||
sx={{ mx: 1, mb: 0.5 }}
|
||||
id={title}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Paper
|
||||
sx={{
|
||||
p: { xs: 2, sm: 3 },
|
||||
|
||||
"& > :not(style) + :not(style)": {
|
||||
m: 0,
|
||||
mt: { xs: 2, sm: 3 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Paper>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
59
src/components/Settings/SettingsSkeleton.tsx
Normal file
59
src/components/Settings/SettingsSkeleton.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Typography, Paper, Skeleton, Stack, Divider } from "@material-ui/core";
|
||||
|
||||
export default function SettingsSkeleton() {
|
||||
return (
|
||||
<section style={{ cursor: "default" }}>
|
||||
<Typography variant="subtitle1" component="h2" sx={{ mx: 1, mb: 0.5 }}>
|
||||
<Skeleton width={120} />
|
||||
</Typography>
|
||||
<Paper
|
||||
sx={{
|
||||
p: { xs: 2, sm: 3 },
|
||||
|
||||
"& > :not(style) + :not(style)": {
|
||||
m: 0,
|
||||
mt: { xs: 2, sm: 3 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
spacing={2}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<div>
|
||||
<Skeleton width={120} />
|
||||
<Skeleton width={80} />
|
||||
</div>
|
||||
|
||||
<Skeleton
|
||||
width={100}
|
||||
variant="rectangular"
|
||||
sx={{ borderRadius: 1 }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack
|
||||
spacing={2}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<div>
|
||||
<Skeleton width={120} />
|
||||
<Skeleton width={80} />
|
||||
</div>
|
||||
|
||||
<Skeleton
|
||||
width={100}
|
||||
variant="rectangular"
|
||||
sx={{ borderRadius: 1 }}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -19,19 +19,13 @@ import { FieldType } from "constants/fields";
|
||||
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Divider from "@material-ui/core/Divider";
|
||||
import Subheading from "components/Table/ColumnMenu/Subheading";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import routes from "constants/routes";
|
||||
import { SETTINGS } from "config/dbPaths";
|
||||
|
||||
export default function FieldSettings(props: IMenuModalProps) {
|
||||
const {
|
||||
name,
|
||||
fieldName,
|
||||
type,
|
||||
open,
|
||||
config,
|
||||
handleClose,
|
||||
handleSave,
|
||||
} = props;
|
||||
const { name, fieldName, type, open, config, handleClose, handleSave } =
|
||||
props;
|
||||
|
||||
const [showRebuildPrompt, setShowRebuildPrompt] = useState(false);
|
||||
const [newConfig, setNewConfig] = useState(config ?? {});
|
||||
@@ -125,12 +119,11 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
if (showRebuildPrompt) {
|
||||
requestConfirmation({
|
||||
title: "Deploy Changes",
|
||||
body:
|
||||
"You have made changes that affect the behavior of the cloud function of this table, Would you like to redeploy it now?",
|
||||
body: "You have made changes that affect the behavior of the cloud function of this table, Would you like to redeploy it now?",
|
||||
confirm: "Deploy",
|
||||
cancel: "Later",
|
||||
handleConfirm: async () => {
|
||||
const settingsDoc = await db.doc("/_rowy_/settings").get();
|
||||
const settingsDoc = await db.doc(SETTINGS).get();
|
||||
const buildUrl = settingsDoc.get("buildUrl");
|
||||
if (!buildUrl) {
|
||||
snack.open({
|
||||
@@ -149,7 +142,8 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
),
|
||||
});
|
||||
}
|
||||
const userTokenInfo = await appContext?.currentUser?.getIdTokenResult();
|
||||
const userTokenInfo =
|
||||
await appContext?.currentUser?.getIdTokenResult();
|
||||
const userToken = userTokenInfo?.token;
|
||||
try {
|
||||
snackLog.requestSnackLog();
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
IExtension,
|
||||
IExtensionType,
|
||||
} from "./utils";
|
||||
import { SETTINGS } from "config/dbPaths";
|
||||
import WIKI_LINKS from "constants/wikiLinks";
|
||||
|
||||
export default function ExtensionsEditor() {
|
||||
@@ -92,7 +93,7 @@ export default function ExtensionsEditor() {
|
||||
const serialisedExtension = serialiseExtension(localExtensionsObjects);
|
||||
tableActions?.table.updateConfig("extensions", serialisedExtension);
|
||||
|
||||
const settingsDoc = await db.doc("/_rowy_/settings").get();
|
||||
const settingsDoc = await db.doc(SETTINGS).get();
|
||||
const buildUrl = settingsDoc.get("buildUrl");
|
||||
if (!buildUrl) {
|
||||
snack.open({
|
||||
|
||||
@@ -38,6 +38,7 @@ import EmptyState from "components/EmptyState";
|
||||
import PropTypes from "prop-types";
|
||||
import routes from "constants/routes";
|
||||
import { DATE_TIME_FORMAT } from "constants/dates";
|
||||
import { SETTINGS, TABLE_SCHEMAS, TABLE_GROUP_SCHEMAS } from "config/dbPaths";
|
||||
|
||||
function a11yProps(index) {
|
||||
return {
|
||||
@@ -183,9 +184,8 @@ function LogPanel(props) {
|
||||
|
||||
// useStateRef is necessary to resolve the state syncing issue
|
||||
// https://stackoverflow.com/a/63039797/12208834
|
||||
const [liveStreaming, setLiveStreaming, liveStreamingStateRef] = useStateRef(
|
||||
true
|
||||
);
|
||||
const [liveStreaming, setLiveStreaming, liveStreamingStateRef] =
|
||||
useStateRef(true);
|
||||
const liveStreamingRef = useRef<any>();
|
||||
const isActive = value === index;
|
||||
|
||||
@@ -264,9 +264,8 @@ function SnackLog({ log, onClose, onOpenPanel }) {
|
||||
const status = log?.status;
|
||||
const classes = useStyles();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [liveStreaming, setLiveStreaming, liveStreamingStateRef] = useStateRef(
|
||||
true
|
||||
);
|
||||
const [liveStreaming, setLiveStreaming, liveStreamingStateRef] =
|
||||
useStateRef(true);
|
||||
const liveStreamingRef = useRef<any>();
|
||||
|
||||
const handleScroll = _throttle(() => {
|
||||
@@ -403,7 +402,7 @@ export default function TableLogs() {
|
||||
}, []);
|
||||
|
||||
const checkBuildURL = async () => {
|
||||
const settingsDoc = await db.doc("/_rowy_/settings").get();
|
||||
const settingsDoc = await db.doc(SETTINGS).get();
|
||||
const buildUrl = settingsDoc.get("buildUrl");
|
||||
if (!buildUrl) {
|
||||
setBuildURLConfigured(false);
|
||||
@@ -412,8 +411,8 @@ export default function TableLogs() {
|
||||
|
||||
const tableCollection = decodeURIComponent(router.match.params.id);
|
||||
const buildStreamID =
|
||||
"_rowy_/settings/" +
|
||||
`${isCollectionGroup() ? "groupSchema/" : "schema/"}` +
|
||||
(isCollectionGroup() ? TABLE_GROUP_SCHEMAS : TABLE_SCHEMAS) +
|
||||
"/" +
|
||||
tableCollection
|
||||
.split("/")
|
||||
.filter(function (_, i) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useRowyContext } from "contexts/RowyContext";
|
||||
import useRouter from "../../hooks/useRouter";
|
||||
import { db } from "../../firebase";
|
||||
import { name } from "@root/package.json";
|
||||
import { SETTINGS, TABLE_SCHEMAS } from "config/dbPaths";
|
||||
|
||||
export enum TableSettingsDialogModes {
|
||||
create,
|
||||
@@ -123,13 +124,13 @@ export default function TableSettingsDialog({
|
||||
};
|
||||
|
||||
const handleResetStructure = async () => {
|
||||
const schemaDocRef = db.doc(`_rowy_/settings/table/${data!.collection}`);
|
||||
const schemaDocRef = db.doc(`${TABLE_SCHEMAS}/${data!.collection}`);
|
||||
await schemaDocRef.update({ columns: {} });
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const tablesDocRef = db.doc(`_rowy_/settings`);
|
||||
const tablesDocRef = db.doc(SETTINGS);
|
||||
const tableData = (await tablesDocRef.get()).data();
|
||||
const updatedTables = tableData?.tables.filter(
|
||||
(table) =>
|
||||
|
||||
@@ -9,6 +9,7 @@ import MultiSelect from "@antlerengineering/multiselect";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { db } from "../../../firebase";
|
||||
import { useRowyContext } from "contexts/RowyContext";
|
||||
import { TABLE_SCHEMAS } from "config/dbPaths";
|
||||
|
||||
export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
const { tables } = useRowyContext();
|
||||
@@ -24,7 +25,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
{ value: string; label: string; type: FieldType }[]
|
||||
>([]);
|
||||
const getColumns = async (table) => {
|
||||
const tableConfigDoc = await db.doc(`_rowy_/settings/table/${table}`).get();
|
||||
const tableConfigDoc = await db.doc(`${TABLE_SCHEMAS}/${table}`).get();
|
||||
const tableConfig = tableConfigDoc.data();
|
||||
if (tableConfig && tableConfig.columns)
|
||||
setColumns(
|
||||
|
||||
7
src/config/dbPaths.ts
Normal file
7
src/config/dbPaths.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const SETTINGS = "_rowy_/settings";
|
||||
export const PUBLIC_SETTINGS = "_rowy_/publicSettings";
|
||||
|
||||
export const TABLE_SCHEMAS = SETTINGS + "/schema";
|
||||
export const TABLE_GROUP_SCHEMAS = SETTINGS + "/groupSchema";
|
||||
|
||||
export const USERS = SETTINGS + "/users";
|
||||
@@ -18,6 +18,7 @@ export enum routes {
|
||||
settings = "/settings",
|
||||
userSettings = "/settings/user",
|
||||
projectSettings = "/settings/project",
|
||||
userManagement = "/settings/userManagement",
|
||||
}
|
||||
|
||||
export default routes;
|
||||
|
||||
@@ -14,6 +14,7 @@ import Themes from "Themes";
|
||||
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import { name } from "@root/package.json";
|
||||
import { USERS } from "config/dbPaths";
|
||||
|
||||
const useThemeState = createPersistedState("__ROWY__THEME");
|
||||
const useThemeOverriddenState = createPersistedState(
|
||||
@@ -62,7 +63,7 @@ export const AppProvider: React.FC = ({ children }) => {
|
||||
if (currentUser) {
|
||||
analytics.setUserId(currentUser.uid);
|
||||
analytics.setUserProperties({ instance: window.location.hostname });
|
||||
dispatchUserDoc({ path: `_rowy_/settings/users/${currentUser.uid}` });
|
||||
dispatchUserDoc({ path: `${USERS}/${currentUser.uid}` });
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ const documentInitialState = {
|
||||
error: null,
|
||||
};
|
||||
|
||||
const useDoc = (initialOverrides: any) => {
|
||||
const useDoc = (
|
||||
initialOverrides: any,
|
||||
options: { createIfMissing?: boolean } = {}
|
||||
) => {
|
||||
const documentReducer = (prevState: any, newProps: any) => {
|
||||
switch (newProps.action) {
|
||||
case DocActions.clear:
|
||||
@@ -43,7 +46,7 @@ const useDoc = (initialOverrides: any) => {
|
||||
...initialOverrides,
|
||||
});
|
||||
|
||||
const setDocumentListner = () => {
|
||||
const setDocumentListener = () => {
|
||||
documentDispatch({ prevPath: documentState.path });
|
||||
const unsubscribe = db.doc(documentState.path).onSnapshot(
|
||||
(snapshot) => {
|
||||
@@ -58,9 +61,17 @@ const useDoc = (initialOverrides: any) => {
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
documentDispatch({
|
||||
loading: false,
|
||||
});
|
||||
if (options.createIfMissing)
|
||||
try {
|
||||
db.doc(documentState.path).set({}, { merge: true });
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Could not create ${documentState.path}`,
|
||||
e.message
|
||||
);
|
||||
}
|
||||
|
||||
documentDispatch({ loading: false });
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
@@ -76,7 +87,7 @@ const useDoc = (initialOverrides: any) => {
|
||||
const { path, prevPath, unsubscribe } = documentState;
|
||||
if (path && path !== prevPath) {
|
||||
if (unsubscribe) unsubscribe();
|
||||
setDocumentListner();
|
||||
setDocumentListener();
|
||||
}
|
||||
}, [documentState]);
|
||||
useEffect(
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
import useDoc from "./useDoc";
|
||||
import { db } from "../firebase";
|
||||
import { SETTINGS, TABLE_GROUP_SCHEMAS, TABLE_SCHEMAS } from "config/dbPaths";
|
||||
|
||||
const useSettings = () => {
|
||||
const [settingsState, documentDispatch] = useDoc({
|
||||
path: "_rowy_/settings",
|
||||
});
|
||||
const [settingsState, documentDispatch] = useDoc({ path: SETTINGS });
|
||||
useEffect(() => {
|
||||
//updates tables data on document change
|
||||
const { doc, tables } = settingsState;
|
||||
@@ -31,22 +30,26 @@ const useSettings = () => {
|
||||
}) => {
|
||||
const { tables } = settingsState;
|
||||
const { schemaSource, ...tableSettings } = data;
|
||||
const tableSchemaPath = `_rowy_/settings/${
|
||||
tableSettings.tableType !== "collectionGroup" ? "schema" : "groupSchema"
|
||||
const tableSchemaPath = `${
|
||||
tableSettings.tableType !== "collectionGroup"
|
||||
? TABLE_SCHEMAS
|
||||
: TABLE_GROUP_SCHEMAS
|
||||
}/${tableSettings.collection}`;
|
||||
const tableSchemaDocRef = db.doc(tableSchemaPath);
|
||||
|
||||
let columns = {};
|
||||
if (schemaSource) {
|
||||
const schemaSourcePath = `_rowy_/settings/${
|
||||
schemaSource.tableType !== "collectionGroup" ? "schema" : "groupSchema"
|
||||
const schemaSourcePath = `${
|
||||
tableSettings.tableType !== "collectionGroup"
|
||||
? TABLE_SCHEMAS
|
||||
: TABLE_GROUP_SCHEMAS
|
||||
}/${schemaSource.collection}`;
|
||||
const sourceDoc = await db.doc(schemaSourcePath).get();
|
||||
columns = sourceDoc.get("columns");
|
||||
}
|
||||
// updates the setting doc
|
||||
await db
|
||||
.doc("_rowy_/settings")
|
||||
.doc(SETTINGS)
|
||||
.set(
|
||||
{ tables: tables ? [...tables, tableSettings] : [tableSettings] },
|
||||
{ merge: true }
|
||||
@@ -65,7 +68,7 @@ const useSettings = () => {
|
||||
const { tables } = settingsState;
|
||||
const table = tables.filter((t) => t.collection === data.collection)[0];
|
||||
return Promise.all([
|
||||
db.doc("_rowy_/settings").set(
|
||||
db.doc(SETTINGS).set(
|
||||
{
|
||||
tables: tables
|
||||
? [
|
||||
@@ -80,7 +83,7 @@ const useSettings = () => {
|
||||
),
|
||||
//update the rowy collection doc with empty columns
|
||||
db
|
||||
.collection("_rowy_/settings/table")
|
||||
.collection(TABLE_SCHEMAS)
|
||||
.doc(data.collection)
|
||||
.set({ ...data }, { merge: true }),
|
||||
]);
|
||||
@@ -88,10 +91,10 @@ const useSettings = () => {
|
||||
const deleteTable = (collection: string) => {
|
||||
const { tables } = settingsState;
|
||||
|
||||
db.doc("_rowy_/settings").update({
|
||||
db.doc(SETTINGS).update({
|
||||
tables: tables.filter((table) => table.collection !== collection),
|
||||
});
|
||||
db.collection("_rowy_/settings/table").doc(collection).delete();
|
||||
db.collection(TABLE_SCHEMAS).doc(collection).delete();
|
||||
};
|
||||
const settingsActions = { createTable, updateTable, deleteTable };
|
||||
return [settingsState, settingsActions];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import queryString from "query-string";
|
||||
|
||||
import _find from "lodash/find";
|
||||
import { makeStyles, createStyles } from "@material-ui/styles";
|
||||
@@ -11,17 +12,17 @@ import {
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Link,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
// import SettingsIcon from "@material-ui/icons/Settings";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import Favorite from "@material-ui/icons/Favorite";
|
||||
import FavoriteBorder from "@material-ui/icons/FavoriteBorder";
|
||||
import SecurityIcon from "@material-ui/icons/SecurityOutlined";
|
||||
|
||||
import Navigation from "components/Navigation";
|
||||
import Logo from "assets/Logo";
|
||||
import StyledCard from "components/StyledCard";
|
||||
import HomeWelcomePrompt from "components/HomeWelcomePrompt";
|
||||
import EmptyState from "components/EmptyState";
|
||||
|
||||
import routes from "constants/routes";
|
||||
import { useRowyContext } from "contexts/RowyContext";
|
||||
@@ -31,12 +32,8 @@ import TableSettingsDialog, {
|
||||
TableSettingsDialogModes,
|
||||
} from "components/TableSettings";
|
||||
|
||||
import queryString from "query-string";
|
||||
import ProjectSettings from "components/ProjectSettings";
|
||||
import EmptyState from "components/EmptyState";
|
||||
import WIKI_LINKS from "constants/wikiLinks";
|
||||
import BuilderInstaller from "../components/BuilderInstaller";
|
||||
import HomeWelcomePrompt from "components/HomeWelcomePrompt";
|
||||
import { SETTINGS } from "config/dbPaths";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
@@ -136,9 +133,7 @@ export default function HomePage() {
|
||||
const [openProjectSettings, setOpenProjectSettings] = useState(false);
|
||||
const [openBuilderInstaller, setOpenBuilderInstaller] = useState(false);
|
||||
|
||||
const [settingsDocState, settingsDocDispatch] = useDoc({
|
||||
path: "_rowy_/settings",
|
||||
});
|
||||
const [settingsDocState, settingsDocDispatch] = useDoc({ path: SETTINGS });
|
||||
useEffect(() => {
|
||||
if (!settingsDocState.loading && !settingsDocState.doc) {
|
||||
settingsDocDispatch({
|
||||
@@ -151,22 +146,23 @@ export default function HomePage() {
|
||||
return (
|
||||
<EmptyState
|
||||
fullScreen
|
||||
Icon={SecurityIcon}
|
||||
message="Access Denied"
|
||||
description={
|
||||
<>
|
||||
<Typography variant="overline">
|
||||
<Typography>
|
||||
You do not have access to this project. Please contact the project
|
||||
owner.
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<Typography>
|
||||
If you are the project owner, please follow{" "}
|
||||
<a
|
||||
<Link
|
||||
href={WIKI_LINKS.securityRules}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
the instructions
|
||||
</a>{" "}
|
||||
these instructions
|
||||
</Link>{" "}
|
||||
to set up the project rules.
|
||||
</Typography>
|
||||
</>
|
||||
@@ -228,125 +224,95 @@ export default function HomePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Navigation
|
||||
title={
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Logo />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<main className={classes.root}>
|
||||
{sections && Object.keys(sections).length > 0 ? (
|
||||
<Container>
|
||||
{favs.length !== 0 && (
|
||||
<section id="favorites" className={classes.section}>
|
||||
<main className={classes.root}>
|
||||
{sections && Object.keys(sections).length > 0 ? (
|
||||
<Container>
|
||||
{favs.length !== 0 && (
|
||||
<section id="favorites" className={classes.section}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
className={classes.sectionHeader}
|
||||
>
|
||||
Favorites
|
||||
</Typography>
|
||||
<Divider className={classes.divider} />
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justifyContent="flex-start"
|
||||
className={classes.cardGrid}
|
||||
>
|
||||
{favs.map((table) => (
|
||||
<TableCard key={table.collection} table={table} />
|
||||
))}
|
||||
</Grid>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{sections &&
|
||||
Object.keys(sections).length > 0 &&
|
||||
Object.keys(sections).map((sectionName) => (
|
||||
<section
|
||||
key={sectionName}
|
||||
id={sectionName}
|
||||
className={classes.section}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
className={classes.sectionHeader}
|
||||
>
|
||||
Favorites
|
||||
{sectionName === "undefined" ? "Other" : sectionName}
|
||||
</Typography>
|
||||
|
||||
<Divider className={classes.divider} />
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justifyContent="flex-start"
|
||||
className={classes.cardGrid}
|
||||
>
|
||||
{favs.map((table) => (
|
||||
<TableCard key={table.collection} table={table} />
|
||||
{sections[sectionName].map((table, i) => (
|
||||
<TableCard key={`${i}-${table.collection}`} table={table} />
|
||||
))}
|
||||
</Grid>
|
||||
</section>
|
||||
)}
|
||||
))}
|
||||
|
||||
{sections &&
|
||||
Object.keys(sections).length > 0 &&
|
||||
Object.keys(sections).map((sectionName) => (
|
||||
<section
|
||||
key={sectionName}
|
||||
id={sectionName}
|
||||
className={classes.section}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
className={classes.sectionHeader}
|
||||
>
|
||||
{sectionName === "undefined" ? "Other" : sectionName}
|
||||
</Typography>
|
||||
|
||||
<Divider className={classes.divider} />
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
justifyContent="flex-start"
|
||||
className={classes.cardGrid}
|
||||
>
|
||||
{sections[sectionName].map((table, i) => (
|
||||
<TableCard
|
||||
key={`${i}-${table.collection}`}
|
||||
table={table}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section className={classes.section}>
|
||||
<Tooltip title="Create Table">
|
||||
<Fab
|
||||
className={classes.fab}
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={handleCreateTable}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
{/* <Tooltip title="Configure Rowy">
|
||||
<section className={classes.section}>
|
||||
<Tooltip title="Create Table">
|
||||
<Fab
|
||||
className={classes.configFab}
|
||||
className={classes.fab}
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={() => setOpenProjectSettings(true)}
|
||||
onClick={handleCreateTable}
|
||||
>
|
||||
<SettingsIcon />
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Tooltip> */}
|
||||
</section>
|
||||
</Container>
|
||||
) : (
|
||||
<Container>
|
||||
<HomeWelcomePrompt />
|
||||
<Fab
|
||||
className={classes.fab}
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={handleCreateTable}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Container>
|
||||
)}
|
||||
</main>
|
||||
</Tooltip>
|
||||
</section>
|
||||
</Container>
|
||||
) : (
|
||||
<Container>
|
||||
<HomeWelcomePrompt />
|
||||
<Fab
|
||||
className={classes.fab}
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={handleCreateTable}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Container>
|
||||
)}
|
||||
|
||||
<TableSettingsDialog
|
||||
clearDialog={clearDialog}
|
||||
mode={settingsDialogState.mode}
|
||||
data={settingsDialogState.data}
|
||||
/>
|
||||
{openProjectSettings && (
|
||||
<ProjectSettings
|
||||
handleClose={() => setOpenProjectSettings(false)}
|
||||
handleOpenBuilderInstaller={() => setOpenBuilderInstaller(true)}
|
||||
/>
|
||||
)}
|
||||
{openBuilderInstaller && (
|
||||
<BuilderInstaller handleClose={() => setOpenBuilderInstaller(false)} />
|
||||
)}
|
||||
</Navigation>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,95 @@
|
||||
import { Container, Typography } from "@material-ui/core";
|
||||
import { Container, Stack } from "@material-ui/core";
|
||||
|
||||
import Navigation from "components/Navigation";
|
||||
import SettingsSkeleton from "components/Settings/SettingsSkeleton";
|
||||
import SettingsSection from "components/Settings/SettingsSection";
|
||||
import About from "components/Settings/ProjectSettings/About";
|
||||
import Authentication from "components/Settings/ProjectSettings/Authentication";
|
||||
import FunctionsBuilder from "components/Settings/ProjectSettings/FunctionsBuilder";
|
||||
|
||||
import { SETTINGS, PUBLIC_SETTINGS } from "config/dbPaths";
|
||||
import useDoc from "hooks/useDoc";
|
||||
import { db } from "@src/firebase";
|
||||
import { useSnackContext } from "contexts/SnackContext";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export interface IProjectSettingsChildProps {
|
||||
settings: Record<string, any>;
|
||||
updateSettings: (data: Record<string, any>) => void;
|
||||
publicSettings: Record<string, any>;
|
||||
updatePublicSettings: (data: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export default function ProjectSettings() {
|
||||
const snack = useSnackContext();
|
||||
|
||||
const [settingsState] = useDoc({ path: SETTINGS }, { createIfMissing: true });
|
||||
const settings = settingsState.doc;
|
||||
const [updateSettings, , callPending] = useDebouncedCallback(
|
||||
(data: Record<string, any>) =>
|
||||
db
|
||||
.doc(SETTINGS)
|
||||
.update(data)
|
||||
.then(() =>
|
||||
snack.open({ message: "Saved", variant: "success", duration: 3000 })
|
||||
),
|
||||
1000
|
||||
);
|
||||
|
||||
const [publicSettingsState] = useDoc(
|
||||
{ path: PUBLIC_SETTINGS },
|
||||
{ createIfMissing: true }
|
||||
);
|
||||
const publicSettings = publicSettingsState.doc;
|
||||
const [updatePublicSettings, , callPendingPublic] = useDebouncedCallback(
|
||||
(data: Record<string, any>) =>
|
||||
db
|
||||
.doc(PUBLIC_SETTINGS)
|
||||
.update(data)
|
||||
.then(() =>
|
||||
snack.open({ message: "Saved", variant: "success", duration: 3000 })
|
||||
),
|
||||
1000
|
||||
);
|
||||
|
||||
const childProps: IProjectSettingsChildProps = {
|
||||
settings,
|
||||
updateSettings,
|
||||
publicSettings,
|
||||
updatePublicSettings,
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
callPending();
|
||||
callPendingPublic();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Navigation title="Project Settings">
|
||||
<Container style={{ height: "200vh" }}>
|
||||
<Typography component="h2" variant="h4">
|
||||
Project Settings
|
||||
</Typography>
|
||||
</Container>
|
||||
</Navigation>
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 2, pb: 7 }}>
|
||||
{settingsState.loading || publicSettingsState.loading ? (
|
||||
<Stack spacing={4}>
|
||||
<SettingsSkeleton />
|
||||
<SettingsSkeleton />
|
||||
<SettingsSkeleton />
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack spacing={4}>
|
||||
<SettingsSection title="About">
|
||||
<About />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Authentication">
|
||||
<Authentication {...childProps} />
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title="Functions Builder">
|
||||
<FunctionsBuilder {...childProps} />
|
||||
</SettingsSection>
|
||||
</Stack>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
13
src/pages/Settings/UserManagement.tsx
Normal file
13
src/pages/Settings/UserManagement.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Container, Stack } from "@material-ui/core";
|
||||
|
||||
import SettingsSection from "components/Settings/SettingsSection";
|
||||
|
||||
export default function UserManagement() {
|
||||
return (
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 2, pb: 7 }}>
|
||||
<Stack spacing={4}>
|
||||
<SettingsSection title="Users">TODO:</SettingsSection>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Container, Typography } from "@material-ui/core";
|
||||
import { Container, Stack } from "@material-ui/core";
|
||||
|
||||
import Navigation from "components/Navigation";
|
||||
import SettingsSection from "components/Settings/SettingsSection";
|
||||
|
||||
export default function UserSettings() {
|
||||
return (
|
||||
<Navigation title="Settings">
|
||||
<Container>
|
||||
<Typography component="h2" variant="h4">
|
||||
Settings
|
||||
</Typography>
|
||||
</Container>
|
||||
</Navigation>
|
||||
<Container maxWidth="sm" sx={{ px: 1, pt: 2, pb: 7 }}>
|
||||
<Stack spacing={4}>
|
||||
<SettingsSection title="Your Account">TODO:</SettingsSection>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,11 @@ export default function TablePage() {
|
||||
if (!tableState) return null;
|
||||
|
||||
return (
|
||||
<Navigation title={<Breadcrumbs />}>
|
||||
<Navigation
|
||||
title={<Breadcrumbs />}
|
||||
currentSection={currentSection}
|
||||
currentTable={currentTable}
|
||||
>
|
||||
<ActionParamsProvider>
|
||||
{tableState.loadingColumns && (
|
||||
<>
|
||||
|
||||
@@ -64,7 +64,9 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
padding: "0 var(--dialog-spacing)",
|
||||
},
|
||||
|
||||
paperWidthXs: { maxWidth: 360 },
|
||||
paperWidthXs: {
|
||||
[theme.breakpoints.up("sm")]: { maxWidth: 360 },
|
||||
},
|
||||
paperFullScreen: {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
@@ -130,10 +132,6 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
},
|
||||
},
|
||||
|
||||
MuiTypography: {
|
||||
defaultProps: { variant: "body2" },
|
||||
},
|
||||
|
||||
MuiTextField: {
|
||||
defaultProps: {
|
||||
variant: "filled",
|
||||
@@ -208,6 +206,7 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
|
||||
...theme.typography.caption,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -144,5 +144,10 @@ export const typography = ({
|
||||
lineHeight: 20 / 12,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiTypography: {
|
||||
defaultProps: { variant: "body2" },
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import _get from "lodash/get";
|
||||
import { TABLE_GROUP_SCHEMAS, TABLE_SCHEMAS } from "config/dbPaths";
|
||||
|
||||
/**
|
||||
* reposition an element in an array
|
||||
* @param arr array
|
||||
@@ -26,14 +28,12 @@ export const arrayMover = (
|
||||
return arr; // for testing purposes
|
||||
};
|
||||
|
||||
export const missingFieldsReducer = (data: any) => (
|
||||
acc: string[],
|
||||
curr: string
|
||||
) => {
|
||||
if (data[curr] === undefined) {
|
||||
return [...acc, curr];
|
||||
} else return acc;
|
||||
};
|
||||
export const missingFieldsReducer =
|
||||
(data: any) => (acc: string[], curr: string) => {
|
||||
if (data[curr] === undefined) {
|
||||
return [...acc, curr];
|
||||
} else return acc;
|
||||
};
|
||||
|
||||
export const sanitiseCallableName = (name: string) => {
|
||||
if (!name || typeof name !== "string") return "";
|
||||
@@ -96,8 +96,8 @@ export const generateBiggerId = (id: string) => {
|
||||
const formatPathRegex = /\/[^\/]+\/([^\/]+)/g;
|
||||
|
||||
export const formatPath = (tablePath: string) => {
|
||||
return `_rowy_/settings/${
|
||||
isCollectionGroup() ? "groupSchema" : "schema"
|
||||
return `${
|
||||
isCollectionGroup() ? TABLE_GROUP_SCHEMAS : TABLE_SCHEMAS
|
||||
}/${tablePath.replace(formatPathRegex, "/subTables/$1")}`;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user