diff --git a/README.md b/README.md index 2ef2a992b..8e87fb551 100644 --- a/README.md +++ b/README.md @@ -219,3 +219,5 @@ To configure Keycloak: - KEYCLOAK_ISSUER: http://localhost:8080/realms/master [keycloak-admin]: http://localhost:8080/admin/master/console/#/ + +Remember to add NEXTAUTH_SECRET variable to your .env file! diff --git a/app/components/header.tsx b/app/components/header.tsx index af6a071a5..c9c61f2b6 100644 --- a/app/components/header.tsx +++ b/app/components/header.tsx @@ -15,8 +15,7 @@ import { DataSourceMenu } from "@/components/data-source-menu"; import Flex from "@/components/flex"; import { LanguageMenu } from "@/components/language-menu"; import { SOURCE_OPTIONS } from "@/domain/datasource/constants"; - -import LoginMenu from "./login-menu"; +import { LoginMenu } from "@/login/components/login-menu"; const DEFAULT_HEADER_PROGRESS = 100; diff --git a/app/configurator/components/layout.tsx b/app/configurator/components/layout.tsx index 74faa2c39..cf29ed9dc 100644 --- a/app/configurator/components/layout.tsx +++ b/app/configurator/components/layout.tsx @@ -3,6 +3,7 @@ import { makeStyles } from "@mui/styles"; import clsx from "clsx"; import React from "react"; +import { HEADER_HEIGHT } from "@/components/header"; import { DRAWER_WIDTH } from "@/configurator/components/drawer"; const useStyles = makeStyles((theme: Theme) => ({ @@ -28,10 +29,9 @@ const useStyles = makeStyles((theme: Theme) => ({ }, panelLayout: { position: "fixed", - // FIXME replace 96px with actual header size - top: 96, + top: HEADER_HEIGHT, width: "100%", - height: "calc(100vh - 96px)", + height: `calc(100vh - ${HEADER_HEIGHT}px)`, display: "grid", gridTemplateColumns: `${DRAWER_WIDTH}px minmax(22rem, 1fr)`, gridTemplateRows: "auto minmax(0, 1fr)", diff --git a/app/db/config.ts b/app/db/config.ts index eff7467d2..24ce8cbb1 100644 --- a/app/db/config.ts +++ b/app/db/config.ts @@ -4,7 +4,7 @@ import { Config, Prisma, User } from "@prisma/client"; -import { ChartConfig } from "@/configurator"; +import { ChartConfig, ConfiguratorStatePublished } from "@/configurator"; import { migrateConfiguratorState } from "@/utils/chart-config/versioning"; import { createChartId } from "../utils/create-chart-id"; @@ -57,13 +57,16 @@ const ensureFiltersOrder = (chartConfig: ChartConfig) => { }; }; -type ChartJsonConfig = { - dataSet: string; - chartConfig: Prisma.JsonObject; -}; - -const parseDbConfig = (d: Config) => { - const data = d.data as ChartJsonConfig; +const parseDbConfig = ( + d: Config +): { + id: number; + key: string; + data: ConfiguratorStatePublished; + created_at: Date; + user_id: number | null; +} => { + const data = d.data as ConfiguratorStatePublished; const migratedData = migrateConfiguratorState(data); return { @@ -84,7 +87,7 @@ const parseDbConfig = (d: Config) => { export const getConfig = async (key: string) => { const config = await prisma.config.findFirst({ where: { - key: key, + key, }, }); diff --git a/app/components/login-menu.tsx b/app/login/components/login-menu.tsx similarity index 52% rename from app/components/login-menu.tsx rename to app/login/components/login-menu.tsx index 32b7bfb98..0611341d9 100644 --- a/app/components/login-menu.tsx +++ b/app/login/components/login-menu.tsx @@ -1,5 +1,5 @@ import { Box, Button, Typography } from "@mui/material"; -import { getProviders, signIn, signOut, useSession } from "next-auth/react"; +import { getProviders, signIn, useSession } from "next-auth/react"; import Link from "next/link"; import { useEffect, useState } from "react"; @@ -12,63 +12,49 @@ const useProviders = () => { status: "loading", data: undefined as Providers | undefined, }); + useEffect(() => { const run = async () => { const providers = await getProviders(); setState({ status: "loaded", data: providers }); }; + run(); }, []); + return state; }; -function LoginMenu() { +export const LoginMenu = () => { const { data: session, status: sessionStatus } = useSession(); const { data: providers, status: providersStatus } = useProviders(); + if (sessionStatus === "loading" || providersStatus === "loading") { return null; } + if (!providers || !Object.keys(providers).length) { return null; } + return ( {session ? ( - <> - - Signed in as{" "} - - {session.user?.name} - {" "} - {" - "} - - - + + + {session.user?.name} + {" "} + ) : ( - <> - - Not signed in - {" - "} - - - + )} ); -} - -export default LoginMenu; +}; diff --git a/app/login/components/profile-content-tabs.tsx b/app/login/components/profile-content-tabs.tsx new file mode 100644 index 000000000..2995c1772 --- /dev/null +++ b/app/login/components/profile-content-tabs.tsx @@ -0,0 +1,88 @@ +import { TabContext, TabList, TabPanel } from "@mui/lab"; +import { Box, Tab, Theme, Typography } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import clsx from "clsx"; +import React from "react"; + +import { ParsedConfig } from "@/db/config"; +import { ProfileVisualizationsTable } from "@/login/components/profile-tables"; +import { useRootStyles } from "@/login/utils"; +import useEvent from "@/utils/use-event"; + +const useStyles = makeStyles((theme) => ({ + section: { + marginTop: theme.spacing(6), + }, + tabList: { + minHeight: "fit-content", + + "& .MuiTabs-flexContainer": { + height: "fit-content", + }, + }, + tabPanel: { + padding: 0, + }, + tabPanelContent: { + padding: theme.spacing(6), + }, + tab: { + height: 48, + minHeight: 0, + padding: "0 1rem", + textTransform: "none", + }, +})); + +type ProfileContentTabsProps = { + userConfigs: ParsedConfig[]; +}; + +export const ProfileContentTabs = (props: ProfileContentTabsProps) => { + const { userConfigs } = props; + const [value, setValue] = React.useState("Home"); + const handleChange = useEvent((_: React.SyntheticEvent, v: string) => { + setValue(v); + }); + const rootClasses = useRootStyles(); + const classes = useStyles(); + + return ( + + + + + + {["Home", "My visualizations", "My favorite datasets"].map( + (d) => ( + + ) + )} + + + + + + + + + + + + + + + + + My favorite datasets + + + + ); +}; diff --git a/app/login/components/profile-header.tsx b/app/login/components/profile-header.tsx new file mode 100644 index 000000000..5d9c6e8ac --- /dev/null +++ b/app/login/components/profile-header.tsx @@ -0,0 +1,54 @@ +import { Box, Button, Link, Theme, Typography } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import { User } from "@prisma/client"; +import clsx from "clsx"; +import { signOut } from "next-auth/react"; + +import { useRootStyles } from "@/login/utils"; + +const useStyles = makeStyles((theme) => ({ + section: { + paddingTop: theme.spacing(6), + backgroundColor: theme.palette.muted.main, + }, + topRow: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + }, + browseButton: { + width: "fit-content", + }, +})); + +type ProfileHeaderProps = { + user: User; +}; + +export const ProfileHeader = (props: ProfileHeaderProps) => { + const { user } = props; + const rootClasses = useRootStyles(); + const classes = useStyles(); + + return ( + + + + {user.name} + + + + + + ); +}; diff --git a/app/login/components/profile-tables.tsx b/app/login/components/profile-tables.tsx new file mode 100644 index 000000000..aa49aa0b8 --- /dev/null +++ b/app/login/components/profile-tables.tsx @@ -0,0 +1,100 @@ +import { + Box, + Link, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import NextLink from "next/link"; + +import { ParsedConfig } from "@/db/config"; +import { useDataCubeMetadataQuery } from "@/graphql/query-hooks"; +import { Icon } from "@/icons"; +import { useRootStyles } from "@/login/utils"; +import { useLocale } from "@/src"; + +type ProfileVisualizationsTableProps = { + userConfigs: ParsedConfig[]; +}; + +export const ProfileVisualizationsTable = ( + props: ProfileVisualizationsTableProps +) => { + const { userConfigs } = props; + const rootClasses = useRootStyles(); + + return ( + + My visualizations + {userConfigs.length > 0 ? ( + + + Chart type + Name + Dataset + Actions + + + {userConfigs.map((d) => ( + + ))} + +
+ ) : ( + + No charts yet,{" "} + + create one + + . + + )} +
+ ); +}; + +type RowProps = { + config: ParsedConfig; +}; + +const Row = (props: RowProps) => { + const { config } = props; + const locale = useLocale(); + const [{ data }] = useDataCubeMetadataQuery({ + variables: { + iri: config.data.dataSet, + sourceType: config.data.dataSource.type, + sourceUrl: config.data.dataSource.url, + locale, + }, + }); + + return ( + + + {config.data.chartConfigs.length > 1 + ? "multi" + : config.data.chartConfigs[0].chartType} + + {config.data.meta.title[locale]} + {data?.dataCubeByIri?.title ?? ""} + + + + + + + + + + + + + + + + ); +}; diff --git a/app/login/utils.ts b/app/login/utils.ts new file mode 100644 index 000000000..aa5389ce9 --- /dev/null +++ b/app/login/utils.ts @@ -0,0 +1,21 @@ +import { Theme } from "@mui/material"; +import { makeStyles } from "@mui/styles"; + +import { HEADER_HEIGHT } from "@/components/header"; + +export const useRootStyles = makeStyles((theme) => ({ + root: { + marginTop: `${HEADER_HEIGHT}px`, + backgroundColor: theme.palette.muted.main, + }, + section: { + display: "flex", + flexDirection: "column", + padding: `0 ${theme.spacing(6)}`, + }, + sectionContent: { + width: "100%", + maxWidth: 1400, + margin: "0 auto", + }, +})); diff --git a/app/pages/profile.tsx b/app/pages/profile.tsx index b79fc7b72..73566df6a 100644 --- a/app/pages/profile.tsx +++ b/app/pages/profile.tsx @@ -1,25 +1,15 @@ -import { - Box, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Typography, -} from "@mui/material"; +import { Box } from "@mui/material"; import { User } from "@prisma/client"; import { GetServerSideProps } from "next"; import { getServerSession } from "next-auth"; -import Link from "next/link"; import { AppLayout } from "@/components/layout"; -import { - PanelLayout, - PanelMiddleWrapper, -} from "@/configurator/components/layout"; import { ParsedConfig, getUserConfigs } from "@/db/config"; import { Serialized, deserializeProps, serializeProps } from "@/db/serialize"; import { findBySub } from "@/db/user"; +import { ProfileContentTabs } from "@/login/components/profile-content-tabs"; +import { ProfileHeader } from "@/login/components/profile-header"; +import { useRootStyles } from "@/login/utils"; import { nextAuthOptions } from "./api/auth/[...nextauth]"; @@ -31,11 +21,14 @@ type PageProps = { export const getServerSideProps: GetServerSideProps = async ( ctx ) => { - const session = await getServerSession(ctx.req, ctx.res, nextAuthOptions); + const { req, res } = ctx; + const session = await getServerSession(req, res, nextAuthOptions); const userSub = session?.user.sub; + if (userSub) { const user = await findBySub(userSub); const userConfigs = await getUserConfigs(user.id); + return { props: serializeProps({ user, @@ -54,51 +47,14 @@ export const getServerSideProps: GetServerSideProps = async ( const ProfilePage = (props: Serialized) => { const { user, userConfigs } = deserializeProps(props); + const rootClasses = useRootStyles(); return ( - - - - Hello {user.name} 👋 - - Charts - - {userConfigs.length > 0 ? ( - - - Type - Title - Link - - - {userConfigs.map((uc) => { - return ( - - {uc.data.chartConfig.chartType} - {uc.data.meta.title.en} - - - See chart - - - - ); - })} - -
- ) : ( - - No charts yet,{" "} - - create one - - . - - )} -
-
-
+ + + +
); };