Skip to content

Commit d1881e7

Browse files
authored
feat(ui): add api to return config for admin UI (#864)
* feat: add api to return config for ui * feat: set app config to context * feat: add support for logo in config
1 parent 7391163 commit d1881e7

File tree

6 files changed

+93
-16
lines changed

6 files changed

+93
-16
lines changed

pkg/server/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ type GRPCConfig struct {
2626
func (cfg Config) grpcAddr() string { return fmt.Sprintf("%s:%d", cfg.Host, cfg.GRPC.Port) }
2727

2828
type UIConfig struct {
29-
Port int `yaml:"port" mapstructure:"port"`
29+
Port int `yaml:"port" mapstructure:"port"`
30+
Title string `yaml:"title" mapstructure:"title"`
31+
Logo string `yaml:"logo" mapstructure:"logo"`
3032
}
3133

3234
type Config struct {

pkg/server/server.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"net/http"
@@ -54,6 +55,11 @@ const (
5455
grpcDialTimeout = 5 * time.Second
5556
)
5657

58+
type UIConfigApiResponse struct {
59+
Title string `json:"title"`
60+
Logo string `json:"logo"`
61+
}
62+
5763
func ServeUI(ctx context.Context, logger log.Logger, uiConfig UIConfig, apiServerConfig Config) {
5864
isUIPortNotExits := uiConfig.Port == 0
5965
if isUIPortNotExits {
@@ -82,6 +88,15 @@ func ServeUI(ctx context.Context, logger log.Logger, uiConfig UIConfig, apiServe
8288
}
8389

8490
proxy := httputil.NewSingleHostReverseProxy(remote)
91+
http.HandleFunc("/configs", func(w http.ResponseWriter, r *http.Request) {
92+
w.Header().Set("Content-Type", "application/json")
93+
confResp := UIConfigApiResponse{
94+
Title: uiConfig.Title,
95+
Logo: uiConfig.Logo,
96+
}
97+
json.NewEncoder(w).Encode(confResp)
98+
})
99+
85100
http.HandleFunc("/frontier-api/", handler(proxy))
86101
http.Handle("/", http.StripPrefix("/", spaHandler))
87102
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useContext, useEffect } from "react";
2+
import { AppContext } from "~/contexts/App";
3+
import { defaultConfig } from "~/utils/constants";
4+
5+
interface PageTitleProps {
6+
title?: string;
7+
appName?: string;
8+
}
9+
10+
export default function PageTitle({ title, appName }: PageTitleProps) {
11+
const { config } = useContext(AppContext);
12+
const titleAppName = appName || config?.title || defaultConfig?.title;
13+
const fullTitle = title ? `${title} | ${titleAppName}` : titleAppName;
14+
15+
useEffect(() => {
16+
document.title = fullTitle;
17+
}, [fullTitle]);
18+
return null;
19+
}

ui/src/containers/login.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
import { Box, Flex, Image } from "@raystack/apsara";
44
import { Header, MagicLink } from "@raystack/frontier/react";
5+
import { useContext } from "react";
6+
import PageTitle from "~/components/page-title";
7+
import { AppContext } from "~/contexts/App";
8+
import { defaultConfig } from "~/utils/constants";
59

610
export default function Login() {
11+
const { config } = useContext(AppContext);
12+
713
return (
814
<Flex>
15+
<PageTitle title="Login" />
916
<Box style={{ width: "100%" }}>
1017
<Flex
1118
direction="column"
@@ -25,13 +32,13 @@ export default function Login() {
2532
logo={
2633
<Image
2734
alt="logo"
28-
src="logo.svg"
35+
src={config?.logo || "logo.svg"}
2936
width={80}
3037
height={80}
3138
style={{ borderRadius: "var(--pd-8)" }}
3239
/>
3340
}
34-
title="Login to frontier"
41+
title={`Login to ${config?.title || defaultConfig.title}`}
3542
/>
3643
<MagicLink open />
3744
</Flex>

ui/src/contexts/App.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
V1Beta1Plan,
1313
} from "@raystack/frontier";
1414
import { useFrontier } from "@raystack/frontier/react";
15+
import { Config, defaultConfig } from "~/utils/constants";
1516

1617
// TODO: Setting this to 1000 initially till APIs support filters and sorting.
17-
const page_size = 1000
18+
const page_size = 1000;
1819

1920
type OrgMap = Record<string, V1Beta1Organization>;
2021

@@ -27,6 +28,7 @@ interface AppContextValue {
2728
platformUsers?: V1Beta1ListPlatformUsersResponse;
2829
fetchPlatformUsers: () => void;
2930
loadMoreOrganizations: () => void;
31+
config: Config;
3032
}
3133

3234
const AppContextDefaultValue = {
@@ -40,7 +42,8 @@ const AppContextDefaultValue = {
4042
serviceusers: [],
4143
},
4244
fetchPlatformUsers: () => {},
43-
loadMoreOrganizations: () => {}
45+
loadMoreOrganizations: () => {},
46+
config: defaultConfig,
4447
};
4548

4649
export const AppContext = createContext<AppContextValue>(
@@ -66,6 +69,8 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
6669
const [platformUsers, setPlatformUsers] =
6770
useState<V1Beta1ListPlatformUsersResponse>();
6871

72+
const [config, setConfig] = useState<Config>(defaultConfig);
73+
6974
const [page, setPage] = useState(1);
7075
const [enabledOrgHasMoreData, setEnabledOrgHasMoreData] = useState(true);
7176
const [disabledOrgHasMoreData, setDisabledOrgHasMoreData] = useState(true);
@@ -79,7 +84,11 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
7984
try {
8085
const [orgResp, disabledOrgResp] = await Promise.all([
8186
client?.adminServiceListAllOrganizations({ page_num: page, page_size }),
82-
client?.adminServiceListAllOrganizations({ state: "disabled", page_num: page, page_size }),
87+
client?.adminServiceListAllOrganizations({
88+
state: "disabled",
89+
page_num: page,
90+
page_size,
91+
}),
8392
]);
8493

8594
if (orgResp?.data?.organizations?.length) {
@@ -111,7 +120,10 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
111120
}, [client, page, enabledOrgHasMoreData, disabledOrgHasMoreData]);
112121

113122
const loadMoreOrganizations = () => {
114-
if (!isOrgListLoading && (enabledOrgHasMoreData || disabledOrgHasMoreData)) {
123+
if (
124+
!isOrgListLoading &&
125+
(enabledOrgHasMoreData || disabledOrgHasMoreData)
126+
) {
115127
setPage((prevPage: number) => prevPage + 1);
116128
}
117129
};
@@ -136,6 +148,19 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
136148
}
137149
}, [client]);
138150

151+
const fetchConfig = useCallback(async () => {
152+
setIsPlatformUsersLoading(true);
153+
try {
154+
const resp = await fetch("/configs");
155+
const data = (await resp?.json()) as Config;
156+
setConfig(data);
157+
} catch (err) {
158+
console.error(err);
159+
} finally {
160+
setIsPlatformUsersLoading(false);
161+
}
162+
}, []);
163+
139164
useEffect(() => {
140165
async function getPlans() {
141166
setIsPlansLoading(true);
@@ -149,12 +174,12 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
149174
setIsPlansLoading(false);
150175
}
151176
}
152-
153177
if (isAdmin) {
154178
getPlans();
155179
fetchPlatformUsers();
156180
}
157-
}, [client, isAdmin, fetchPlatformUsers]);
181+
fetchConfig();
182+
}, [client, isAdmin, fetchPlatformUsers, fetchConfig]);
158183

159184
const isLoading =
160185
isOrgListLoading ||
@@ -180,6 +205,7 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
180205
platformUsers,
181206
fetchPlatformUsers,
182207
loadMoreOrganizations,
208+
config,
183209
}}
184210
>
185211
{children}

ui/src/utils/constants.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@ export const PERMISSIONS = {
44
OrganizationNamespace: "app/organization",
55
} as const;
66

7-
87
export const SUBSCRIPTION_STATUSES = [
9-
{label: 'Active', value: 'active'},
10-
{label: 'Trialing', value: 'trialing'},
11-
{label: 'Past due', value: 'past_due'},
12-
{label: 'Canceled', value: 'canceled'},
13-
{label: 'Ended', value: 'ended'}
14-
]
8+
{ label: "Active", value: "active" },
9+
{ label: "Trialing", value: "trialing" },
10+
{ label: "Past due", value: "past_due" },
11+
{ label: "Canceled", value: "canceled" },
12+
{ label: "Ended", value: "ended" },
13+
];
14+
15+
export interface Config {
16+
title: string;
17+
logo?: string;
18+
}
19+
20+
export const defaultConfig: Config = {
21+
title: "Frontier Admin",
22+
};

0 commit comments

Comments
 (0)