move types to server

This commit is contained in:
Nate Kelley 2025-07-07 15:33:44 -06:00
parent dd01142273
commit 6d02d8900f
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
53 changed files with 395 additions and 235 deletions

View File

@ -9,6 +9,7 @@ export interface ListPermissionUsersResponse {
name: string;
email: string;
assigned: boolean;
avatar_url: string | null;
}
export interface DatasetPermissionOverviewUser {
@ -16,6 +17,7 @@ export interface DatasetPermissionOverviewUser {
name: string;
email: string;
can_query: boolean;
avatar_url: string | null;
lineage: {
name: string;
id: string;

View File

@ -1,25 +1,10 @@
export interface BusterOrganization {
created_at: string;
id: string;
deleted_at: string | null;
domain: string;
name: string;
updated_at: string;
role: BusterOrganizationRole;
}
import type { OrganizationRole } from '@buster/server-shared/organization';
export enum BusterOrganizationRole {
WORKSPACE_ADMIN = 'workspaceAdmin',
DATA_ADMIN = 'dataAdmin',
QUERIER = 'querier',
RESTRICTED_QUERIER = 'restrictedQuerier',
VIEWER = 'viewer'
}
export const BusterOrganizationRoleLabels = {
[BusterOrganizationRole.WORKSPACE_ADMIN]: 'Workspace Admin',
[BusterOrganizationRole.DATA_ADMIN]: 'Data Admin',
[BusterOrganizationRole.QUERIER]: 'Querier',
[BusterOrganizationRole.RESTRICTED_QUERIER]: 'Restricted Querier',
[BusterOrganizationRole.VIEWER]: 'Viewer'
export const BusterOrganizationRoleLabels: Record<OrganizationRole, string> = {
workspace_admin: 'Workspace Admin',
data_admin: 'Data Admin',
querier: 'Querier',
restricted_querier: 'Restricted Querier',
viewer: 'Viewer',
none: 'None'
};

View File

@ -1,14 +1,14 @@
import type { BusterDatasetListItem } from '../datasets';
import type { BusterOrganizationRole } from '../organizations';
import type { OrganizationRole } from '@buster/server-shared/organization';
export interface BusterPermissionListUser {
id: string;
email: string;
name: string;
role: BusterOrganizationRole;
role: OrganizationRole;
belongs_to: boolean;
//only shows up with no filters. To lazy to type this out better
team_role?: BusterOrganizationRole;
team_role?: OrganizationRole;
//???
team_count: number;
permission_group_count: number;
@ -18,7 +18,7 @@ export interface BusterPermissionUser {
id: string;
email: string;
name: string;
role: BusterOrganizationRole;
role: OrganizationRole;
created_at: string;
permission_group_count: number;
permission_groups: {
@ -33,7 +33,7 @@ export interface BusterPermissionUser {
id: string;
member_count: number;
name: string;
team_role: BusterOrganizationRole;
team_role: OrganizationRole;
}[];
updated_at: string;
edit_sql: boolean;
@ -49,7 +49,7 @@ export interface BusterPermissionListTeam {
name: string;
member_count: number;
permission_group_count: number;
team_role: BusterOrganizationRole;
team_role: OrganizationRole;
belongs_to: boolean;
}
@ -69,7 +69,7 @@ export interface BusterPermissionTeam {
email: string;
id: string;
name: string;
role: BusterOrganizationRole;
role: OrganizationRole;
}[];
permission_groups: {
dataset_count: number;
@ -113,7 +113,7 @@ export interface BusterPermissionGroup {
id: string;
member_count: number;
name: string;
team_role: BusterOrganizationRole;
team_role: OrganizationRole;
}[];
updated_by: string;
user_count: number;
@ -124,10 +124,10 @@ export interface BusterPermissionListUser {
id: string;
email: string;
name: string;
role: BusterOrganizationRole;
role: OrganizationRole;
belongs_to: boolean;
//only shows up with no filters. To lazy to type this out better
team_role?: BusterOrganizationRole;
team_role?: OrganizationRole;
//???
team_count: number;
permission_group_count: number;
@ -137,7 +137,7 @@ export interface BusterPermissionUser {
id: string;
email: string;
name: string;
role: BusterOrganizationRole;
role: OrganizationRole;
created_at: string;
permission_group_count: number;
permission_groups: {
@ -152,7 +152,7 @@ export interface BusterPermissionUser {
id: string;
member_count: number;
name: string;
team_role: BusterOrganizationRole;
team_role: OrganizationRole;
}[];
updated_at: string;
edit_sql: boolean;
@ -168,7 +168,7 @@ export interface BusterPermissionListTeam {
name: string;
member_count: number;
permission_group_count: number;
team_role: BusterOrganizationRole;
team_role: OrganizationRole;
belongs_to: boolean;
}
@ -188,7 +188,7 @@ export interface BusterPermissionTeam {
email: string;
id: string;
name: string;
role: BusterOrganizationRole;
role: OrganizationRole;
}[];
permission_groups: {
dataset_count: number;
@ -232,7 +232,7 @@ export interface BusterPermissionGroup {
id: string;
member_count: number;
name: string;
team_role: BusterOrganizationRole;
team_role: OrganizationRole;
}[];
updated_by: string;
user_count: number;

View File

@ -16,6 +16,7 @@ export interface GetPermissionGroupUsersResponse {
assigned: boolean;
email: string;
name: string;
avatar_url: string | null;
}
export interface GetPermissionGroupDatasetsResponse {

View File

@ -1,76 +1,12 @@
import type { BusterOrganization, BusterOrganizationRole } from '../organizations';
import type { BusterPermissionUser } from '../permission';
import type { ShareAssetType } from '@buster/server-shared/share';
export interface BusterUserPalette {
id: string;
palette: string[];
}
export enum TeamRole {
MANAGER = 'manager',
MEMBER = 'member',
NONE = 'none'
}
export interface BusterUserTeam {
id: string;
name: string;
edit_sql: boolean;
email_slack_enabled: boolean;
export_assets: boolean;
organization_id: string;
sharing_settings: BusterPermissionUser['sharing_setting'];
upload_csv: boolean;
updated_at: string;
created_at: string;
deleted_at: string | null;
role: TeamRole;
}
export interface BusterUserFavorite {
id: string;
asset_type: ShareAssetType;
index?: number;
name: string;
}
export type BusterUserFavoriteAsset = {
id: string;
ype: ShareAssetType;
index?: number;
title: string;
};
export interface BusterUser {
config: Record<string, unknown>;
created_at: string;
email: string;
favorites: BusterUserFavorite[];
id: string;
name: string;
updated_at: string;
}
export interface BusterUserResponse {
user: BusterUser;
teams: BusterUserTeam[];
organizations: BusterOrganization[] | null;
}
export interface BusterUserListItem {
email: string;
id: string;
name: string;
role: null;
}
import type { OrganizationRole } from '@buster/server-shared/organization';
export interface OrganizationUser {
id: string;
email: string;
name: string;
avatar_url: string | null;
status: 'active' | 'inactive';
role: BusterOrganizationRole;
role: OrganizationRole;
datasets: OrganizationUserDataset[];
}

View File

@ -1,4 +1,4 @@
import type { TeamRole } from './interfaces';
import type { TeamRole } from '@buster/server-shared/teams';
export interface BusterUserDatasetGroup {
id: string;

View File

@ -1,7 +1,7 @@
import type { BusterOrganization } from '@/api/asset_interfaces/organizations';
import type { OrganizationUser } from '@/api/asset_interfaces/users';
import { serverFetch } from '../../createServerInstance';
import { mainApi } from '../instances';
import type { Organization } from '@buster/server-shared/organization';
export const getOrganizationUsers = async ({
organizationId
@ -22,5 +22,5 @@ export const getOrganizationUsers_server = async ({
};
export const createOrganization = async (organization: { name: string }) => {
return mainApi.post<BusterOrganization>('/organizations', organization).then((res) => res.data);
return mainApi.post<Organization>('/organizations', organization).then((res) => res.data);
};

View File

@ -1,4 +1,4 @@
import type { BusterUserTeam } from '@/api/asset_interfaces/users';
import type { TeamListResponse, GetTeamListRequest } from '@buster/server-shared/teams';
import { mainApi } from '../instances';
export const createTeam = async (params: {
@ -10,12 +10,6 @@ export const createTeam = async (params: {
return mainApi.post<{ id: string }>('/teams', params).then((res) => res.data);
};
export const getTeamsList = async (params: {
page_size?: number;
page?: number;
permission_group_id?: string | null;
user_id?: string | null;
belongs_to?: boolean | null;
}) => {
return mainApi.get<BusterUserTeam[]>('/teams', { params }).then((res) => res.data);
export const getTeamsList = async (params: GetTeamListRequest) => {
return mainApi.get<TeamListResponse>('/teams', { params }).then((res) => res.data);
};

View File

@ -3,9 +3,9 @@ import type {
BusterUserDataset,
BusterUserDatasetGroup,
BusterUserPermissionGroup,
BusterUserTeamListItem,
TeamRole
BusterUserTeamListItem
} from '@/api/asset_interfaces/users';
import type { TeamRole } from '@buster/server-shared/teams';
import { serverFetch } from '../../../createServerInstance';
import { mainApi } from '../../instances';

View File

@ -1,26 +1,26 @@
import type { ShareAssetType } from '@buster/server-shared/share';
import type {
BusterUserFavorite,
BusterUserListItem,
BusterUserResponse,
OrganizationUser
} from '@/api/asset_interfaces/users';
import type { OrganizationUser } from '@/api/asset_interfaces/users';
import { BASE_URL } from '../config';
import { serverFetch } from '../../createServerInstance';
import { mainApi } from '../instances';
import type {
UserResponse,
UserFavoriteResponse,
UserListResponse
} from '@buster/server-shared/user';
export const getMyUserInfo = async (): Promise<BusterUserResponse> => {
return mainApi.get<BusterUserResponse>('/users').then((response) => response.data);
export const getMyUserInfo = async () => {
return mainApi.get<UserResponse>('/users').then((response) => response.data);
};
export const getMyUserInfo_server = async ({
jwtToken
}: {
jwtToken: string | undefined;
}): Promise<BusterUserResponse | null> => {
}): Promise<UserResponse | null> => {
if (!jwtToken) {
//If Anonymous user, it will fail, so we catch the error and return undefined
return await serverFetch<BusterUserResponse>('/users', {
return await serverFetch<UserResponse>('/users', {
method: 'GET'
});
}
@ -41,7 +41,7 @@ export const getMyUserInfo_server = async ({
...errorData
};
}
return response.json();
return (await response.json()) as UserResponse;
});
};
@ -82,11 +82,11 @@ export const inviteUser = async ({
//USER FAVORITES
export const getUserFavorites = async () => {
return mainApi.get<BusterUserFavorite[]>('/users/favorites').then((response) => response.data);
return mainApi.get<UserFavoriteResponse>('/users/favorites').then((response) => response.data);
};
export const getUserFavorites_server = async () => {
return serverFetch<BusterUserFavorite[]>('/users/favorites');
return serverFetch<UserFavoriteResponse>('/users/favorites');
};
export const createUserFavorite = async (
@ -98,19 +98,19 @@ export const createUserFavorite = async (
}[]
) => {
return mainApi
.post<BusterUserFavorite[]>('/users/favorites', payload)
.post<UserFavoriteResponse>('/users/favorites', payload)
.then((response) => response.data);
};
export const deleteUserFavorite = async (data: string[]) => {
return mainApi
.delete<BusterUserFavorite[]>('/users/favorites', { data })
.delete<UserFavoriteResponse>('/users/favorites', { data })
.then((response) => response.data);
};
export const updateUserFavorites = async (payload: string[]) => {
return mainApi
.put<BusterUserFavorite[]>('/users/favorites', payload)
.put<UserFavoriteResponse>('/users/favorites', payload)
.then((response) => response.data);
};
@ -122,10 +122,10 @@ export const getUserList = async (payload: {
page_size?: number;
}) => {
return mainApi
.get<BusterUserListItem[]>('/users', { params: payload })
.get<UserListResponse>('/users', { params: payload })
.then((response) => response.data);
};
export const getUserList_server = async (payload: Parameters<typeof getUserList>[0]) => {
return serverFetch<BusterUserListItem[]>('/users', { params: payload });
return serverFetch<UserListResponse>('/users', { params: payload });
};

View File

@ -3,22 +3,24 @@ import type {
BusterUserAttribute,
BusterUserDataset,
BusterUserDatasetGroup,
BusterUserFavorite,
BusterUserListItem,
BusterUserPermissionGroup,
BusterUserResponse,
BusterUserTeamListItem,
OrganizationUser
} from '@/api/asset_interfaces/users';
import type {
UserFavoriteResponse,
UserResponse,
UserListResponse
} from '@buster/server-shared/user';
const favoritesGetList = queryOptions<BusterUserFavorite[]>({
const favoritesGetList = queryOptions<UserFavoriteResponse>({
queryKey: ['myself', 'list', 'favorites'] as const,
staleTime: 1000 * 60 * 60, // 1 hour,
initialData: [],
initialDataUpdatedAt: 0
});
const userGetUserMyself = queryOptions<BusterUserResponse | null>({
const userGetUserMyself = queryOptions<UserResponse | null>({
queryKey: ['myself'] as const,
staleTime: 1000 * 60 * 60 // 1 hour
});
@ -54,7 +56,7 @@ const userGetUserDatasetGroups = (userId: string) =>
});
const userGetUserList = (params: { team_id: string; page?: number; page_size?: number }) =>
queryOptions<BusterUserListItem[]>({
queryOptions<UserListResponse>({
queryKey: ['users', 'list', params] as const
});

View File

@ -104,7 +104,12 @@ export const PermissionListUserContainer: React.FC<{
rows={rows}
showHeader={false}
showSelectAll={false}
emptyState={useMemo(() => <EmptyStateList text="No users found" />, [])}
emptyState={useMemo(
() => (
<EmptyStateList text="No users found" />
),
[]
)}
/>
</InfiniteListContainer>
</>
@ -113,7 +118,7 @@ export const PermissionListUserContainer: React.FC<{
PermissionListUserContainer.displayName = 'PermissionListUserContainer';
const UserInfoCell = React.memo(({ user }: { user: DatasetPermissionOverviewUser }) => {
return <ListUserItem name={user.name} email={user.email} />;
return <ListUserItem name={user.name} email={user.email} avatarURL={user.avatar_url} />;
});
UserInfoCell.displayName = 'UserInfoCell';

View File

@ -39,7 +39,9 @@ export const DatasetGroupUsersListContainer: React.FC<{
title: 'Name',
dataIndex: 'name',
render: (name, user: GetPermissionGroupUsersResponse) => {
return <ListUserItem name={name as string} email={user.email} />;
return (
<ListUserItem name={name as string} email={user.email} avatarURL={user.avatar_url} />
);
}
},
{

View File

@ -34,7 +34,7 @@ export const PermissionGroupUsersListContainer: React.FC<{
title: 'Name',
dataIndex: 'name',
render: (name: string, user: GetPermissionGroupUsersResponse) => {
return <ListUserItem name={name} email={user.email} />;
return <ListUserItem name={name} email={user.email} avatarURL={user.avatar_url} />;
}
},
{

View File

@ -24,7 +24,7 @@ export const ListUsersComponent: React.FC<{
title: 'Name',
dataIndex: 'name',
render: (name: string, user: OrganizationUser) => {
return <ListUserItem name={name} email={user.email} />;
return <ListUserItem name={name} email={user.email} avatarURL={user.avatar_url} />;
}
},
{

View File

@ -16,7 +16,7 @@ UserHeader.displayName = 'UserHeader';
const UserInfo: React.FC<{ user: OrganizationUser }> = ({ user }) => {
return (
<div className="flex items-center space-x-3">
<Avatar size={32} fallbackClassName="text-base" name={user.name} />
<Avatar size={32} fallbackClassName="text-base" name={user.name} image={user.avatar_url} />
<div className="flex flex-col space-y-0.5">
<Title as="h4">{user.name}</Title>
<Text size="sm" variant="secondary">

View File

@ -1,10 +1,5 @@
import React from 'react';
import {
BusterOrganizationRole,
BusterOrganizationRoleLabels,
type BusterUser,
type OrganizationUser
} from '@/api/asset_interfaces';
import { BusterOrganizationRoleLabels, type OrganizationUser } from '@/api/asset_interfaces';
import { useUpdateUser } from '@/api/buster_rest/users';
import {
Card,
@ -17,11 +12,12 @@ import { Select, type SelectItem } from '@/components/ui/select';
import { AppTooltip } from '@/components/ui/tooltip';
import { Text } from '@/components/ui/typography';
import { useMemoizedFn } from '@/hooks';
import { User } from '@buster/server-shared/user';
export const UserDefaultAccess: React.FC<{
user: OrganizationUser;
isAdmin: boolean;
myUser: BusterUser;
myUser: User;
refetchUser: () => void;
}> = ({ user, isAdmin, myUser, refetchUser }) => {
const { mutateAsync } = useUpdateUser();
@ -45,17 +41,17 @@ export const UserDefaultAccess: React.FC<{
};
const accessOptions: SelectItem<OrganizationUser['role']>[] = [
{ label: BusterOrganizationRoleLabels.dataAdmin, value: BusterOrganizationRole.DATA_ADMIN },
{ label: BusterOrganizationRoleLabels.data_admin, value: 'data_admin' },
{
label: BusterOrganizationRoleLabels.workspaceAdmin,
value: BusterOrganizationRole.WORKSPACE_ADMIN
label: BusterOrganizationRoleLabels.workspace_admin,
value: 'workspace_admin'
},
{ label: BusterOrganizationRoleLabels.querier, value: BusterOrganizationRole.QUERIER },
{ label: BusterOrganizationRoleLabels.querier, value: 'querier' },
{
label: BusterOrganizationRoleLabels.restrictedQuerier,
value: BusterOrganizationRole.RESTRICTED_QUERIER
label: BusterOrganizationRoleLabels.restricted_querier,
value: 'restricted_querier'
},
{ label: BusterOrganizationRoleLabels.viewer, value: BusterOrganizationRole.VIEWER }
{ label: BusterOrganizationRoleLabels.viewer, value: 'viewer' }
];
const DefaultAccessCard = React.memo(

View File

@ -2,7 +2,7 @@
import pluralize from 'pluralize';
import React, { useMemo, useState } from 'react';
import type { BusterUserTeamListItem, TeamRole } from '@/api/asset_interfaces';
import type { BusterUserTeamListItem } from '@/api/asset_interfaces';
import { useUpdateUserTeams } from '@/api/buster_rest/users/permissions';
import { PermissionAssignTeamRole } from '@/components/features/PermissionComponents';
import {
@ -11,6 +11,7 @@ import {
EmptyStateList,
InfiniteListContainer
} from '@/components/ui/list';
import type { TeamRole } from '@buster/server-shared/teams';
import { BusterInfiniteList } from '@/components/ui/list/BusterInfiniteList';
import { Text } from '@/components/ui/typography';
import { useMemoizedFn } from '@/hooks';

View File

@ -1,5 +1,5 @@
import React from 'react';
import type { TeamRole } from '@/api/asset_interfaces';
import type { TeamRole } from '@buster/server-shared/teams';
import { useUpdateUserTeams } from '@/api/buster_rest/users/permissions';
import { PermissionAssignTeamRoleButton } from '@/components/features/PermissionComponents';
import { BusterListSelectedOptionPopupContainer } from '@/components/ui/list';

View File

@ -6,9 +6,10 @@ export const OrganizationUserStatusText: Record<OrganizationUser['status'], stri
};
export const OrganizationUserRoleText: Record<OrganizationUser['role'], string> = {
dataAdmin: 'Data Admin',
workspaceAdmin: 'Workspace Admin',
data_admin: 'Data Admin',
workspace_admin: 'Workspace Admin',
querier: 'Querier',
restrictedQuerier: 'Restricted Querier',
viewer: 'Viewer'
restricted_querier: 'Restricted Querier',
viewer: 'Viewer',
none: 'None'
};

View File

@ -9,14 +9,19 @@ import { SettingsPageHeader } from '../../_components/SettingsPageHeader';
export default function ProfilePage() {
const user = useUserConfigContextSelector((state) => state.user);
if (!user) return null;
const { name, email, created_at } = user;
const { name, email, created_at, avatar_url } = user;
return (
<div>
<SettingsPageHeader title="Profile" description="Manage your profile & information" />
<div className="bg-background rounded-lg border shadow">
{/* Header Section */}
<div className="border-border/30 flex flex-col items-center gap-4 border-b p-6 sm:flex-row sm:items-start">
<Avatar name={name} className="h-12 w-12" fallbackClassName="text-2xl" />
<Avatar
name={name}
image={user.avatar_url}
className="h-12 w-12"
fallbackClassName="text-2xl"
/>
<div className="flex flex-col justify-center gap-1">
<Title as="h3" className="text-foreground">
{name}

View File

@ -1,19 +1,19 @@
import React from 'react';
import { TeamRole } from '@/api/asset_interfaces';
import { Select, type SelectItem } from '@/components/ui/select';
import type { TeamRole } from '@buster/server-shared/teams';
export const TEAM_ROLE_OPTIONS: SelectItem<TeamRole>[] = [
{
label: 'Manager',
value: TeamRole.MANAGER
value: 'manager'
},
{
label: 'Member',
value: TeamRole.MEMBER
value: 'member'
},
{
label: 'Not a Member',
value: TeamRole.NONE
value: 'none'
}
];

View File

@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import type { TeamRole } from '@/api/asset_interfaces';
import type { TeamRole } from '@buster/server-shared/teams';
import { Button } from '@/components/ui/buttons';
import { Dropdown, type DropdownProps } from '@/components/ui/dropdown';
import { CheckDouble } from '@/components/ui/icons';

View File

@ -9,10 +9,11 @@ export const IndividualSharePerson: React.FC<{
name?: string;
email: string;
role: ShareRole;
avatarURL: string | null;
onUpdateShareRole: (email: string, role: ShareRole | null) => void;
assetType: ShareAssetType;
disabled: boolean;
}> = React.memo(({ name, onUpdateShareRole, email, role, assetType, disabled }) => {
}> = React.memo(({ name, onUpdateShareRole, email, avatarURL, role, assetType, disabled }) => {
const isSameEmailName = name === email;
const onChangeShareLevel = useMemoizedFn((v: ShareRole | null) => {
@ -25,7 +26,7 @@ export const IndividualSharePerson: React.FC<{
data-testid={`share-person-${email}`}>
<div className="flex h-full items-center space-x-1.5 overflow-hidden">
<div className="flex h-full flex-col items-center justify-center">
<Avatar className="h-[24px] w-[24px]" name={name || email} />
<Avatar className="h-[24px] w-[24px]" name={name || email} image={avatarURL} />
</div>
<div className="flex flex-col space-y-0 overflow-hidden">
<Text truncate className="leading-1.3">

View File

@ -4,7 +4,7 @@ import type { User } from '@supabase/supabase-js';
import { useRouter } from 'next/navigation';
import type React from 'react';
import { useState } from 'react';
import type { BusterUserResponse } from '@/api/asset_interfaces/users';
import type { UserResponse } from '@buster/server-shared/user';
import { Button } from '@/components/ui/buttons';
import { SuccessCard } from '@/components/ui/card/SuccessCard';
import { Input } from '@/components/ui/inputs';
@ -17,7 +17,7 @@ import { PolicyCheck } from './PolicyCheck';
export const ResetPasswordForm: React.FC<{
supabaseUser: User;
busterUser: BusterUserResponse;
busterUser: UserResponse;
resetPassword: (d: { password: string }) => Promise<{ error: string } | undefined>;
}> = ({ supabaseUser, busterUser, resetPassword }) => {
const [loading, setLoading] = useState(false);

View File

@ -2,22 +2,24 @@ import React from 'react';
import { Avatar } from '@/components/ui/avatar';
import { Text } from '@/components/ui/typography';
export const ListUserItem = React.memo(({ name, email }: { name: string; email: string }) => {
return (
<div className="flex w-full items-center space-x-2">
<div className="flex items-center">
<Avatar size={24} name={name} />
</div>
export const ListUserItem = React.memo(
({ name, email, avatarURL }: { name: string; email: string; avatarURL: string | null }) => {
return (
<div className="flex w-full items-center space-x-2">
<div className="flex items-center">
<Avatar size={24} name={name} image={avatarURL} />
</div>
<div className="flex flex-col justify-center space-y-0">
{name && <Text>{name}</Text>}
{email && (
<Text variant="secondary" style={{ fontSize: 12 }}>
{email}
</Text>
)}
<div className="flex flex-col justify-center space-y-0">
{name && <Text>{name}</Text>}
{email && (
<Text variant="secondary" style={{ fontSize: 12 }}>
{email}
</Text>
)}
</div>
</div>
</div>
);
});
);
}
);
ListUserItem.displayName = 'ListUserItem';

View File

@ -30,7 +30,8 @@ export const SidebarUserFooter: React.FC = () => {
const handleSignOut = useSignOut();
if (!user) return null;
const { name, email } = user;
console.log(user);
const { name, email, avatar_url } = user;
if (!name || !email) return null;
@ -41,6 +42,7 @@ export const SidebarUserFooter: React.FC = () => {
<AvatarUserButton
username={name}
email={email}
avatarUrl={avatar_url}
className={cn(COLLAPSED_HIDDEN, 'w-full')}
/>
</div>

View File

@ -1,7 +1,7 @@
import { useParams } from 'next/navigation';
import { useMemo } from 'react';
import { ShareAssetType } from '@buster/server-shared/share';
import type { BusterUserFavorite } from '@/api/asset_interfaces/users';
import type { UserFavorite } from '@buster/server-shared/user';
import {
useDeleteUserFavorite,
useGetUserFavorites,
@ -27,7 +27,7 @@ export const useFavoriteSidebarPanel = () => {
updateUserFavorites(itemIds);
});
const isAssetActive = useMemoizedFn((favorite: BusterUserFavorite) => {
const isAssetActive = useMemoizedFn((favorite: UserFavorite) => {
const assetType = favorite.asset_type;
const id = favorite.id;

View File

@ -6,7 +6,7 @@ import { Tooltip } from '../tooltip/Tooltip';
import { Avatar as AvatarBase, AvatarFallback, AvatarImage } from './AvatarBase';
export interface AvatarProps {
image?: string | null;
image: string | null | undefined;
name?: string | null;
className?: string;
fallbackClassName?: string;

View File

@ -7,7 +7,7 @@ export const AvatarUserButton = React.forwardRef<
HTMLDivElement,
{
username?: string;
avatarUrl?: string;
avatarUrl?: string | null;
email?: string;
className?: string;
}

View File

@ -5,9 +5,9 @@ import type { PostHogConfig } from 'posthog-js';
import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
import React, { type PropsWithChildren, useEffect } from 'react';
import type { BusterUserTeam } from '@/api/asset_interfaces';
import { isDev } from '@/config';
import { useUserConfigContextSelector } from '../Users';
import type { Team } from '@buster/server-shared/teams';
const POSTHOG_KEY = process.env.NEXT_PUBLIC_POSTHOG_KEY;
const DEBUG_POSTHOG = false;
@ -44,7 +44,7 @@ const PosthogWrapper: React.FC<PropsWithChildren> = ({ children }) => {
const user = useUserConfigContextSelector((state) => state.user);
const userTeams = useUserConfigContextSelector((state) => state.userTeams);
const userOrganizations = useUserConfigContextSelector((state) => state.userOrganizations);
const team: BusterUserTeam | undefined = userTeams?.[0];
const team: Team | undefined = userTeams?.[0];
useEffect(() => {
if (POSTHOG_KEY && !isServer && user && posthog && team) {

View File

@ -38,7 +38,7 @@ export const PermissionListUsersContainer: React.FC<{
dataIndex: 'name',
width: 270,
render: (name: string, user: ListPermissionUsersResponse) => {
return <ListUserItem name={name} email={user.email} />;
return <ListUserItem name={name} email={user.email} avatarURL={user.avatar_url} />;
}
},
{

View File

@ -1,7 +1,16 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { BusterOrganizationRole } from '@/api/asset_interfaces/organizations';
import { NewChatWarning } from './NewChatWarning';
import { z } from 'zod/v4';
import type { OrganizationRole } from '@buster/server-shared/organization';
const OrganizationRoleSchema: Record<OrganizationRole, string> = {
data_admin: 'data_admin',
workspace_admin: 'workspace_admin',
querier: 'querier',
restricted_querier: 'restricted_querier',
viewer: 'viewer',
none: 'none'
};
const meta: Meta<typeof NewChatWarning> = {
title: 'Controllers/HomePage/NewChatWarning',
@ -30,7 +39,7 @@ const meta: Meta<typeof NewChatWarning> = {
},
userRole: {
control: 'select',
options: Object.values(BusterOrganizationRole),
options: Object.values(OrganizationRoleSchema),
description: "The user's role in the organization"
}
}
@ -45,7 +54,7 @@ export const NewUser: Story = {
hasDatasets: false,
hasDatasources: false,
isAdmin: true,
userRole: BusterOrganizationRole.DATA_ADMIN
userRole: OrganizationRoleSchema.data_admin
}
};
@ -55,7 +64,7 @@ export const CompleteSetup: Story = {
hasDatasets: true,
hasDatasources: true,
isAdmin: true,
userRole: BusterOrganizationRole.DATA_ADMIN
userRole: OrganizationRoleSchema.data_admin
}
};
@ -65,7 +74,7 @@ export const WorkspaceAdminCompleteSetup: Story = {
hasDatasets: true,
hasDatasources: true,
isAdmin: true,
userRole: BusterOrganizationRole.WORKSPACE_ADMIN
userRole: OrganizationRoleSchema.workspace_admin
}
};
@ -75,7 +84,7 @@ export const ViewerRole: Story = {
hasDatasets: false,
hasDatasources: false,
isAdmin: false,
userRole: BusterOrganizationRole.VIEWER
userRole: OrganizationRoleSchema.viewer
}
};
@ -84,7 +93,7 @@ export const QuerierRole: Story = {
hasDatasets: false,
hasDatasources: false,
isAdmin: false,
userRole: BusterOrganizationRole.QUERIER
userRole: OrganizationRoleSchema.querier
}
};
@ -93,6 +102,6 @@ export const RestrictedQuerierRole: Story = {
hasDatasets: false,
hasDatasources: false,
isAdmin: false,
userRole: BusterOrganizationRole.RESTRICTED_QUERIER
userRole: OrganizationRoleSchema.restricted_querier
}
};

View File

@ -5,12 +5,10 @@ import { ArrowUpRight, CircleCheck, AlertWarning } from '@/components/ui/icons';
import { Paragraph, Text } from '@/components/ui/typography';
import { cn } from '@/lib/classMerge';
import type { useNewChatWarning } from './useNewChatWarning';
import {
BusterOrganizationRoleLabels,
type BusterOrganizationRole
} from '@/api/asset_interfaces/organizations';
import { BusterOrganizationRoleLabels } from '@/api/asset_interfaces/organizations';
import type { OrganizationRole } from '@buster/server-shared/organization';
const translateRole = (role: BusterOrganizationRole) => {
const translateRole = (role: OrganizationRole) => {
return BusterOrganizationRoleLabels[role];
};
@ -157,7 +155,7 @@ const SetupItem = ({ number, status, title, description, link, linkText }: Setup
};
interface ContactAdminCardProps {
userRole?: BusterOrganizationRole;
userRole?: OrganizationRole;
}
const ContactAdminCard = ({ userRole }: ContactAdminCardProps) => {

View File

@ -1,7 +1,6 @@
import { BusterOrganizationRole } from '@/api/asset_interfaces/organizations';
import type { BusterUserResponse } from '@/api/asset_interfaces/users';
import type { UserResponse } from '@buster/server-shared/user';
export const checkIfUserIsAdmin = (userInfo?: BusterUserResponse | null): boolean => {
export const checkIfUserIsAdmin = (userInfo?: UserResponse | null): boolean => {
if (!userInfo) return false;
const userOrganization = userInfo?.organizations?.[0];
@ -10,8 +9,5 @@ export const checkIfUserIsAdmin = (userInfo?: BusterUserResponse | null): boolea
const userRole = userOrganization.role;
return (
userRole === BusterOrganizationRole.DATA_ADMIN ||
userRole === BusterOrganizationRole.WORKSPACE_ADMIN
);
return userRole === 'data_admin' || userRole === 'workspace_admin';
};

View File

@ -40,11 +40,24 @@
"./slack": {
"types": "./dist/slack/index.d.ts",
"default": "./dist/slack/index.js"
},
"./user": {
"types": "./dist/user/index.d.ts",
"default": "./dist/user/index.js"
},
"./organization": {
"types": "./dist/organization/index.d.ts",
"default": "./dist/organization/index.js"
},
"./teams": {
"types": "./dist/teams/index.d.ts",
"default": "./dist/teams/index.js"
}
},
"dependencies": {
"@buster/vitest-config": "workspace:*",
"@buster/typescript-config": "workspace:*",
"@buster/database": "workspace:*",
"zod": "catalog:"
},
"devDependencies": {

View File

@ -0,0 +1,2 @@
export * from './organization.types';
export * from './roles.types';

View File

@ -0,0 +1,14 @@
import { z } from 'zod/v4';
import { OrganizationRoleSchema } from './roles.types';
export const OrganizationSchema = z.object({
created_at: z.string(),
id: z.string(),
deleted_at: z.string().nullable(),
domain: z.string(),
name: z.string(),
updated_at: z.string(),
role: OrganizationRoleSchema,
});
export type Organization = z.infer<typeof OrganizationSchema>;

View File

@ -0,0 +1,6 @@
import { userOrganizationRoleEnum } from '@buster/database';
import { z } from 'zod/v4';
export const OrganizationRoleSchema = z.enum([...userOrganizationRoleEnum.enumValues, 'none']);
export type OrganizationRole = z.infer<typeof OrganizationRoleSchema>;

View File

@ -1,19 +1,25 @@
import { z } from 'zod/v4';
import { z } from "zod/v4";
export const ShareRoleSchema = z.enum([
'owner', //owner of the asset
'fullAccess', //same as owner, can share with others
'canEdit', //can edit, cannot share
'canFilter', //can filter dashboard
'canView', //can view asset
"owner", //owner of the asset
"fullAccess", //same as owner, can share with others
"canEdit", //can edit, cannot share
"canFilter", //can filter dashboard
"canView", //can view asset
]);
export const ShareAssetTypeSchema = z.enum(['metric', 'dashboard', 'collection', 'chat']);
export const ShareAssetTypeSchema = z.enum([
"metric",
"dashboard",
"collection",
"chat",
]);
export const ShareIndividualSchema = z.object({
email: z.string().email(),
email: z.string(),
role: ShareRoleSchema,
name: z.string().optional(),
avatar_url: z.string().nullable(),
});
export const ShareConfigSchema = z.object({

View File

@ -1,4 +1,4 @@
import { z } from 'zod';
import { z } from 'zod/v4';
// POST /api/v2/slack/auth/init
export const InitiateOAuthSchema = z.object({

View File

@ -0,0 +1,3 @@
export * from './teams.types';
export * from './responses';
export * from './requests';

View File

@ -0,0 +1,18 @@
import { z } from 'zod/v4';
export const CreateTeamRequestSchema = z.object({
name: z.string(),
description: z.string().optional(),
});
export type CreateTeamRequest = z.infer<typeof CreateTeamRequestSchema>;
export const GetTeamListRequestSchema = z.object({
page_size: z.number().optional(),
page: z.number().optional(),
permission_group_id: z.string().optional(),
user_id: z.string().optional(),
belongs_to: z.boolean().optional(),
});
export type GetTeamListRequest = z.infer<typeof GetTeamListRequestSchema>;

View File

@ -0,0 +1,6 @@
import { z } from 'zod/v4';
import { TeamSchema } from './teams.types';
export const TeamListResponseSchema = z.array(TeamSchema);
export type TeamListResponse = z.infer<typeof TeamListResponseSchema>;

View File

@ -0,0 +1,24 @@
import { teamRoleEnum } from '@buster/database';
import { z } from 'zod/v4';
import { SharingSettingSchema } from '../user/sharing-setting.types';
export const TeamRoleSchema = z.enum([...teamRoleEnum.enumValues, 'none']);
export type TeamRole = z.infer<typeof TeamRoleSchema>;
export const TeamSchema = z.object({
id: z.string(),
name: z.string(),
edit_sql: z.boolean(),
email_slack_enabled: z.boolean(),
export_assets: z.boolean(),
organization_id: z.string(),
sharing_settings: SharingSettingSchema,
upload_csv: z.boolean(),
updated_at: z.string(),
created_at: z.string(),
deleted_at: z.string().nullable(),
role: TeamRoleSchema,
});
export type Team = z.infer<typeof TeamSchema>;

View File

@ -0,0 +1,11 @@
import { z } from 'zod/v4';
import { ShareAssetTypeSchema } from '../share';
export const UserFavoriteSchema = z.object({
id: z.string(),
asset_type: ShareAssetTypeSchema,
index: z.number().optional(),
name: z.string(),
});
export type UserFavorite = z.infer<typeof UserFavoriteSchema>;

View File

@ -0,0 +1,7 @@
export * from './request.types';
export * from './responses.types';
export * from './users.types';
export * from './roles.types';
export * from '../teams/teams.types';
export * from './sharing-setting.types';
export * from './favorites.types';

View File

@ -0,0 +1,49 @@
import { z } from 'zod/v4';
import { OrganizationRoleSchema } from '../organization/roles.types';
import { ShareAssetTypeSchema } from '../share';
export const UserRequestSchema = z.object({
user_id: z.string(),
});
export type UserRequest = z.infer<typeof UserRequestSchema>;
export const UserUpdateRequestSchema = z.object({
user_id: z.string(),
name: z.string().optional(),
role: OrganizationRoleSchema.optional(),
});
export type UserUpdateRequest = z.infer<typeof UserUpdateRequestSchema>;
export const UserInviteRequestSchema = z.object({
emails: z.array(z.string().regex(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)),
team_ids: z.array(z.string()).optional(),
});
export type UserInviteRequest = z.infer<typeof UserInviteRequestSchema>;
export const UserCreateFavoriteRequestSchema = z.object({
id: z.string(),
asset_type: ShareAssetTypeSchema,
index: z.number().optional(),
name: z.string(),
});
export type UserCreateFavoriteRequest = z.infer<typeof UserCreateFavoriteRequestSchema>;
export const UserDeleteFavoriteRequestSchema = z.array(z.string());
export type UserDeleteFavoriteRequest = z.infer<typeof UserDeleteFavoriteRequestSchema>;
export const UserUpdateFavoriteRequestSchema = z.array(z.string());
export type UserUpdateFavoriteRequest = z.infer<typeof UserUpdateFavoriteRequestSchema>;
export const GetUserListRequestSchema = z.object({
team_id: z.string(),
page: z.number().optional(),
page_size: z.number().optional(),
});
export type GetUserListRequest = z.infer<typeof GetUserListRequestSchema>;

View File

@ -0,0 +1,27 @@
import { z } from 'zod/v4';
import { OrganizationSchema } from '../organization/organization.types';
import { OrganizationRoleSchema } from '../organization/roles.types';
import { TeamSchema } from '../teams/teams.types';
import { UserFavoriteSchema } from './favorites.types';
import { UserSchema } from './users.types';
export const UserResponseSchema = z.object({
user: UserSchema,
teams: z.array(TeamSchema),
organizations: z.array(OrganizationSchema).nullable(),
});
export const UserListResponseSchema = z.array(
z.object({
email: z.string(),
id: z.string(),
name: z.string(),
role: OrganizationRoleSchema.nullable(),
})
);
export const UserFavoriteResponseSchema = z.array(UserFavoriteSchema);
export type UserResponse = z.infer<typeof UserResponseSchema>;
export type UserListResponse = z.infer<typeof UserListResponseSchema>;
export type UserFavoriteResponse = z.infer<typeof UserFavoriteResponseSchema>;

View File

@ -0,0 +1,6 @@
import { userOrganizationRoleEnum } from '@buster/database';
import { z } from 'zod/v4';
export const UserOrganizationRoleSchema = z.enum([...userOrganizationRoleEnum.enumValues, 'none']);
export type UserOrganizationRole = z.infer<typeof UserOrganizationRoleSchema>;

View File

@ -0,0 +1,6 @@
import { sharingSettingEnum } from '@buster/database';
import { z } from 'zod/v4';
export const SharingSettingSchema = z.enum([...sharingSettingEnum.enumValues, 'none']);
export type SharingSetting = z.infer<typeof SharingSettingSchema>;

View File

@ -0,0 +1,21 @@
import { z } from "zod/v4";
import type { UserFavorite } from "./favorites.types";
import type { UserOrganizationRole } from "./roles.types";
export const UserSchema = z.object({
attributes: z.object({
organization_id: z.string(),
organization_role: z.custom<UserOrganizationRole>(),
user_email: z.string().email(),
user_id: z.string(),
}),
created_at: z.string(),
email: z.string().email(),
favorites: z.array(z.custom<UserFavorite>()),
id: z.string(),
name: z.string(),
avatar_url: z.string().nullable(),
updated_at: z.string(),
});
export type User = z.infer<typeof UserSchema>;

View File

@ -766,6 +766,9 @@ importers:
packages/server-shared:
dependencies:
'@buster/database':
specifier: workspace:*
version: link:../database
'@buster/typescript-config':
specifier: workspace:*
version: link:../typescript-config