Skip to content
Open
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
254 changes: 251 additions & 3 deletions src/TenantTabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import React, { FC, useState, useEffect } from 'react';
import { Box, Typography } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { TenantTabTarget, UpdateTenantTabSelectionFn } from './tenantTab';
import { fetchDatabases, fetchTables, fetchSchema } from './api';
import {
fetchDatabases,
fetchTables,
fetchSchema,
fetchTenantDetail,
ITenantDetailResponse
} from './api';

interface ITenantTabContentProps {
target: TenantTabTarget;
Expand Down Expand Up @@ -44,15 +50,19 @@ const tableStyles = {
borderBottom: '1px solid var(--jp-border-color2)',
fontWeight: 600,
fontSize: '12px',
color: 'var(--jp-ui-font-color0)'
color: 'var(--jp-ui-font-color2)',
textTransform: 'uppercase' as const,
letterSpacing: '0.5px'
},
header: {
display: 'grid',
backgroundColor: 'var(--jp-layout-color2)',
borderBottom: '1px solid var(--jp-border-color2)',
fontWeight: 600,
fontSize: '12px',
color: 'var(--jp-ui-font-color0)'
color: 'var(--jp-ui-font-color2)',
textTransform: 'uppercase' as const,
letterSpacing: '0.5px'
},
headerCell: {
padding: '8px 12px',
Expand Down Expand Up @@ -103,6 +113,7 @@ const tableStyles = {
height: '18px',
px: 0.75,
ml: 0.75,
my: -0.25,
borderRadius: 'var(--jp-border-radius)',
border: '1px solid var(--jp-border-color2)',
bgcolor: 'var(--jp-layout-color1)',
Expand All @@ -112,6 +123,224 @@ const tableStyles = {
}
};

/** Tenant information panel */
const TenantInfo: FC<{
detail: ITenantDetailResponse | undefined;
isLoading: boolean;
error: Error | null;
}> = ({ detail, isLoading, error }) => {
if (isLoading) {
return (
<Box sx={{ ...tableStyles.emptyState, py: 2 }}>
Loading tenant information...
</Box>
);
}
if (error) {
return (
<Box
sx={{
...tableStyles.emptyState,
color: 'var(--jp-error-color1)',
py: 2
}}
>
Error loading tenant info: {error.message}
</Box>
);
}
if (!detail) {
return null;
}

const { metadata, stewards, members } = detail;

const infoCardStyle = {
flex: 1,
minWidth: 0,
border: '1px solid var(--jp-border-color2)',
borderRadius: '4px',
overflow: 'hidden'
};

const infoCardHeader = {
padding: '8px 12px',
backgroundColor: 'var(--jp-layout-color2)',
borderBottom: '1px solid var(--jp-border-color2)',
fontWeight: 600,
fontSize: '12px',
color: 'var(--jp-ui-font-color2)',
textTransform: 'uppercase' as const,
letterSpacing: '0.5px'
};

const infoCardBody = {
padding: '8px 12px',
fontSize: '12px',
color: 'var(--jp-ui-font-color0)'
};

return (
<Box
sx={{
display: 'flex',
gap: 1.5,
flexWrap: 'wrap'
}}
>
{/* Tenant Info */}
<Box sx={infoCardStyle}>
<Box sx={infoCardHeader}>Tenant Info</Box>
<Box sx={infoCardBody}>
<Box sx={{ fontWeight: 600, fontSize: '13px' }}>
{metadata.display_name || metadata.tenant_name}
</Box>
{metadata.display_name &&
metadata.display_name !== metadata.tenant_name && (
<Box sx={{ color: 'var(--jp-ui-font-color2)', mt: 0.25 }}>
{metadata.tenant_name}
</Box>
)}
{metadata.organization && (
<Box
component="span"
title="Organization"
sx={{
fontSize: '12px',
mt: 0.25,
display: 'block'
}}
>
{metadata.organization}
</Box>
)}
{metadata.website && (
<Box sx={{ mt: 0.5, fontSize: '11px' }}>
<a
href={metadata.website}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'var(--jp-brand-color1)' }}
>
{metadata.website}
</a>
</Box>
)}
</Box>
</Box>

{/* Description */}
<Box sx={{ ...infoCardStyle }}>
<Box sx={infoCardHeader}>Description</Box>
<Box sx={infoCardBody}>
{metadata.description || (
<Box
component="span"
sx={{ color: 'var(--jp-ui-font-color3)', fontStyle: 'italic' }}
>
No description
</Box>
)}
</Box>
</Box>

{/* Data Steward(s) */}
<Box sx={infoCardStyle}>
<Box sx={infoCardHeader}>
Data Steward{stewards.length !== 1 ? 's' : ''}
</Box>
<Box sx={infoCardBody}>
{stewards.length === 0 && (
<Box
component="span"
sx={{ color: 'var(--jp-ui-font-color3)', fontStyle: 'italic' }}
>
None assigned
</Box>
)}
{stewards.map(steward => (
<Box
key={steward.username}
sx={{ mb: 0.5, '&:last-child': { mb: 0 } }}
>
<Box sx={{ fontWeight: 600 }}>{steward.display_name}</Box>
{steward.email && (
<Box
sx={{ color: 'var(--jp-ui-font-color2)', fontSize: '11px' }}
>
{steward.email}
</Box>
)}
</Box>
))}
</Box>
</Box>

{/* Members */}
<Box sx={infoCardStyle}>
<Box
sx={{
...infoCardHeader,
display: 'flex',
justifyContent: 'space-between'
}}
>
<Box>Members</Box>
<Box component="span" sx={tableStyles.countPill}>
{detail.member_count}
</Box>
</Box>
<Box
sx={{
...infoCardBody,
maxHeight: '80px',
overflow: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
gap: '2px 8px'
}}
>
{members.map(member => (
<Box
key={member.username}
sx={{
display: 'flex',
alignItems: 'center',
fontSize: '11px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
<Box
component="span"
sx={{
width: '6px',
height: '6px',
borderRadius: '50%',
border: '1.5px solid var(--jp-ui-font-color2)',
backgroundColor:
member.access_level === 'read_write'
? 'var(--jp-ui-font-color2)'
: 'transparent',
mr: 0.5,
flexShrink: 0
}}
title={
member.access_level === 'read_write'
? 'Read/Write'
: 'Read Only'
}
/>
{member.display_name} ({member.username})
</Box>
))}
</Box>
</Box>
</Box>
);
};

/** Databases list component */
const DatabasesList: FC<{
databases: string[];
Expand Down Expand Up @@ -388,6 +617,12 @@ export const TenantTabContent: FC<ITenantTabContentProps> = ({
});
}, [onSelectionChange, selectedDatabase, selectedTable, target.tenant]);

const tenantDetailQuery = useQuery({
queryKey: ['tenantDetail', target.tenant],
queryFn: () => fetchTenantDetail(target.tenant!),
enabled: !!target.tenant
});

const databasesQuery = useQuery({
queryKey: ['databases', target.tenant],
queryFn: () => fetchDatabases(target.tenant)
Expand Down Expand Up @@ -467,6 +702,19 @@ export const TenantTabContent: FC<ITenantTabContentProps> = ({
gap: 2
}}
>
{/* Tenant Information */}
{target.tenant && (
<TenantInfo
detail={tenantDetailQuery.data}
isLoading={tenantDetailQuery.isLoading}
error={
tenantDetailQuery.error instanceof Error
? tenantDetailQuery.error
: null
}
/>
)}

{/* Top section: Databases and Tables */}
<Box sx={{ display: 'flex', gap: 2, flex: 1, minHeight: 0 }}>
{/* Databases */}
Expand Down
58 changes: 58 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,53 @@ export interface IGroupsResponse {
group_count: number;
}

/** Tenant metadata */
export interface ITenantMetadata {
tenant_name: string;
display_name: string;
description: string | null;
website: string | null;
organization: string | null;
created_by: string;
created_at: string;
updated_at: string;
updated_by: string;
}

/** Tenant steward */
export interface ITenantSteward {
username: string;
display_name: string;
email: string | null;
assigned_by: string;
assigned_at: string;
}

/** Tenant member */
export interface ITenantMember {
username: string;
display_name: string;
email: string | null;
access_level: 'read_write' | 'read_only';
is_steward: boolean;
}

/** Tenant storage paths */
export interface ITenantStoragePaths {
general_warehouse: string | null;
sql_warehouse: string | null;
namespace_prefix: string | null;
}

/** Full tenant detail response */
export interface ITenantDetailResponse {
metadata: ITenantMetadata;
stewards: ITenantSteward[];
members: ITenantMember[];
member_count: number;
storage_paths: ITenantStoragePaths;
}

/** Error response from server */
interface IErrorResponse {
error: string;
Expand Down Expand Up @@ -96,3 +143,14 @@ export async function fetchSchema(
const params = new URLSearchParams({ database, table });
return serverGet<string[]>('schema', params);
}

/**
* Fetch tenant detail including metadata, stewards, and members.
* @param tenant - Tenant name
*/
export async function fetchTenantDetail(
tenant: string
): Promise<ITenantDetailResponse> {
const params = new URLSearchParams({ tenant });
return serverGet<ITenantDetailResponse>('tenant-detail', params);
}
Loading
Loading