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",
+};