diff --git a/pkg/server/config.go b/pkg/server/config.go index bd67e20e8..9725620d5 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -26,7 +26,9 @@ type GRPCConfig struct { func (cfg Config) grpcAddr() string { return fmt.Sprintf("%s:%d", cfg.Host, cfg.GRPC.Port) } type UIConfig struct { - Port int `yaml:"port" mapstructure:"port"` + Port int `yaml:"port" mapstructure:"port"` + Title string `yaml:"title" mapstructure:"title"` + Logo string `yaml:"logo" mapstructure:"logo"` } type Config struct { diff --git a/pkg/server/server.go b/pkg/server/server.go index 7fcf270a5..fe5e2dca3 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -54,6 +55,11 @@ const ( grpcDialTimeout = 5 * time.Second ) +type UIConfigApiResponse struct { + Title string `json:"title"` + Logo string `json:"logo"` +} + func ServeUI(ctx context.Context, logger log.Logger, uiConfig UIConfig, apiServerConfig Config) { isUIPortNotExits := uiConfig.Port == 0 if isUIPortNotExits { @@ -82,6 +88,15 @@ func ServeUI(ctx context.Context, logger log.Logger, uiConfig UIConfig, apiServe } proxy := httputil.NewSingleHostReverseProxy(remote) + http.HandleFunc("/configs", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + confResp := UIConfigApiResponse{ + Title: uiConfig.Title, + Logo: uiConfig.Logo, + } + json.NewEncoder(w).Encode(confResp) + }) + http.HandleFunc("/frontier-api/", handler(proxy)) http.Handle("/", http.StripPrefix("/", spaHandler)) } diff --git a/ui/src/components/page-title/index.tsx b/ui/src/components/page-title/index.tsx new file mode 100644 index 000000000..79bce9537 --- /dev/null +++ b/ui/src/components/page-title/index.tsx @@ -0,0 +1,19 @@ +import { useContext, useEffect } from "react"; +import { AppContext } from "~/contexts/App"; +import { defaultConfig } from "~/utils/constants"; + +interface PageTitleProps { + title?: string; + appName?: string; +} + +export default function PageTitle({ title, appName }: PageTitleProps) { + const { config } = useContext(AppContext); + const titleAppName = appName || config?.title || defaultConfig?.title; + const fullTitle = title ? `${title} | ${titleAppName}` : titleAppName; + + useEffect(() => { + document.title = fullTitle; + }, [fullTitle]); + return null; +} diff --git a/ui/src/containers/login.tsx b/ui/src/containers/login.tsx index 55a4608bc..834091208 100644 --- a/ui/src/containers/login.tsx +++ b/ui/src/containers/login.tsx @@ -2,10 +2,17 @@ import { Box, Flex, Image } from "@raystack/apsara"; import { Header, MagicLink } from "@raystack/frontier/react"; +import { useContext } from "react"; +import PageTitle from "~/components/page-title"; +import { AppContext } from "~/contexts/App"; +import { defaultConfig } from "~/utils/constants"; export default function Login() { + const { config } = useContext(AppContext); + return ( + } - title="Login to frontier" + title={`Login to ${config?.title || defaultConfig.title}`} /> diff --git a/ui/src/contexts/App.tsx b/ui/src/contexts/App.tsx index 4dc536412..a61e550b0 100644 --- a/ui/src/contexts/App.tsx +++ b/ui/src/contexts/App.tsx @@ -12,9 +12,10 @@ import { V1Beta1Plan, } from "@raystack/frontier"; import { useFrontier } from "@raystack/frontier/react"; +import { Config, defaultConfig } from "~/utils/constants"; // TODO: Setting this to 1000 initially till APIs support filters and sorting. -const page_size = 1000 +const page_size = 1000; type OrgMap = Record; @@ -27,6 +28,7 @@ interface AppContextValue { platformUsers?: V1Beta1ListPlatformUsersResponse; fetchPlatformUsers: () => void; loadMoreOrganizations: () => void; + config: Config; } const AppContextDefaultValue = { @@ -40,7 +42,8 @@ const AppContextDefaultValue = { serviceusers: [], }, fetchPlatformUsers: () => {}, - loadMoreOrganizations: () => {} + loadMoreOrganizations: () => {}, + config: defaultConfig, }; export const AppContext = createContext( @@ -66,6 +69,8 @@ export const AppContextProvider: React.FC = function ({ const [platformUsers, setPlatformUsers] = useState(); + const [config, setConfig] = useState(defaultConfig); + const [page, setPage] = useState(1); const [enabledOrgHasMoreData, setEnabledOrgHasMoreData] = useState(true); const [disabledOrgHasMoreData, setDisabledOrgHasMoreData] = useState(true); @@ -79,7 +84,11 @@ export const AppContextProvider: React.FC = function ({ try { const [orgResp, disabledOrgResp] = await Promise.all([ client?.adminServiceListAllOrganizations({ page_num: page, page_size }), - client?.adminServiceListAllOrganizations({ state: "disabled", page_num: page, page_size }), + client?.adminServiceListAllOrganizations({ + state: "disabled", + page_num: page, + page_size, + }), ]); if (orgResp?.data?.organizations?.length) { @@ -111,7 +120,10 @@ export const AppContextProvider: React.FC = function ({ }, [client, page, enabledOrgHasMoreData, disabledOrgHasMoreData]); const loadMoreOrganizations = () => { - if (!isOrgListLoading && (enabledOrgHasMoreData || disabledOrgHasMoreData)) { + if ( + !isOrgListLoading && + (enabledOrgHasMoreData || disabledOrgHasMoreData) + ) { setPage((prevPage: number) => prevPage + 1); } }; @@ -136,6 +148,19 @@ export const AppContextProvider: React.FC = function ({ } }, [client]); + const fetchConfig = useCallback(async () => { + setIsPlatformUsersLoading(true); + try { + const resp = await fetch("/configs"); + const data = (await resp?.json()) as Config; + setConfig(data); + } catch (err) { + console.error(err); + } finally { + setIsPlatformUsersLoading(false); + } + }, []); + useEffect(() => { async function getPlans() { setIsPlansLoading(true); @@ -149,12 +174,12 @@ export const AppContextProvider: React.FC = function ({ setIsPlansLoading(false); } } - if (isAdmin) { getPlans(); fetchPlatformUsers(); } - }, [client, isAdmin, fetchPlatformUsers]); + fetchConfig(); + }, [client, isAdmin, fetchPlatformUsers, fetchConfig]); const isLoading = isOrgListLoading || @@ -180,6 +205,7 @@ export const AppContextProvider: React.FC = function ({ platformUsers, fetchPlatformUsers, loadMoreOrganizations, + config, }} > {children} diff --git a/ui/src/utils/constants.ts b/ui/src/utils/constants.ts index 41ccfb8e7..1e0093050 100644 --- a/ui/src/utils/constants.ts +++ b/ui/src/utils/constants.ts @@ -4,11 +4,19 @@ export const PERMISSIONS = { OrganizationNamespace: "app/organization", } as const; - export const SUBSCRIPTION_STATUSES = [ - {label: 'Active', value: 'active'}, - {label: 'Trialing', value: 'trialing'}, - {label: 'Past due', value: 'past_due'}, - {label: 'Canceled', value: 'canceled'}, - {label: 'Ended', value: 'ended'} -] \ No newline at end of file + { label: "Active", value: "active" }, + { label: "Trialing", value: "trialing" }, + { label: "Past due", value: "past_due" }, + { label: "Canceled", value: "canceled" }, + { label: "Ended", value: "ended" }, +]; + +export interface Config { + title: string; + logo?: string; +} + +export const defaultConfig: Config = { + title: "Frontier Admin", +};