Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): add api to return config for admin UI #864

Merged
merged 3 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pkg/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package server

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
Expand Down
19 changes: 19 additions & 0 deletions ui/src/components/page-title/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 9 additions & 2 deletions ui/src/containers/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Flex>
<PageTitle title="Login" />
<Box style={{ width: "100%" }}>
<Flex
direction="column"
Expand All @@ -25,13 +32,13 @@ export default function Login() {
logo={
<Image
alt="logo"
src="logo.svg"
src={config?.logo || "logo.svg"}
width={80}
height={80}
style={{ borderRadius: "var(--pd-8)" }}
/>
}
title="Login to frontier"
title={`Login to ${config?.title || defaultConfig.title}`}
/>
<MagicLink open />
</Flex>
Expand Down
38 changes: 32 additions & 6 deletions ui/src/contexts/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, V1Beta1Organization>;

Expand All @@ -27,6 +28,7 @@ interface AppContextValue {
platformUsers?: V1Beta1ListPlatformUsersResponse;
fetchPlatformUsers: () => void;
loadMoreOrganizations: () => void;
config: Config;
}

const AppContextDefaultValue = {
Expand All @@ -40,7 +42,8 @@ const AppContextDefaultValue = {
serviceusers: [],
},
fetchPlatformUsers: () => {},
loadMoreOrganizations: () => {}
loadMoreOrganizations: () => {},
config: defaultConfig,
};

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

const [config, setConfig] = useState<Config>(defaultConfig);

const [page, setPage] = useState(1);
const [enabledOrgHasMoreData, setEnabledOrgHasMoreData] = useState(true);
const [disabledOrgHasMoreData, setDisabledOrgHasMoreData] = useState(true);
Expand All @@ -79,7 +84,11 @@ export const AppContextProvider: React.FC<PropsWithChildren> = 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) {
Expand Down Expand Up @@ -111,7 +120,10 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
}, [client, page, enabledOrgHasMoreData, disabledOrgHasMoreData]);

const loadMoreOrganizations = () => {
if (!isOrgListLoading && (enabledOrgHasMoreData || disabledOrgHasMoreData)) {
if (
!isOrgListLoading &&
(enabledOrgHasMoreData || disabledOrgHasMoreData)
) {
setPage((prevPage: number) => prevPage + 1);
}
};
Expand All @@ -136,6 +148,19 @@ export const AppContextProvider: React.FC<PropsWithChildren> = 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);
Expand All @@ -149,12 +174,12 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
setIsPlansLoading(false);
}
}

if (isAdmin) {
getPlans();
fetchPlatformUsers();
}
}, [client, isAdmin, fetchPlatformUsers]);
fetchConfig();
}, [client, isAdmin, fetchPlatformUsers, fetchConfig]);

const isLoading =
isOrgListLoading ||
Expand All @@ -180,6 +205,7 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
platformUsers,
fetchPlatformUsers,
loadMoreOrganizations,
config,
}}
>
{children}
Expand Down
22 changes: 15 additions & 7 deletions ui/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
]
{ 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",
};
Loading