added inegration routes

This commit is contained in:
Nate Kelley 2025-09-03 23:18:19 -06:00
parent bcc95954e9
commit c1a79deff3
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
11 changed files with 653 additions and 1 deletions

View File

@ -0,0 +1,2 @@
export * from './queryRequests';
export * from './request';

View File

@ -0,0 +1,60 @@
import type { CreateS3IntegrationRequest } from '@buster/server-shared/s3-integrations';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { s3IntegrationsQueryKeys } from '@/api/query_keys/s3Integrations';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { createS3Integration, deleteS3Integration, getS3Integration } from './request';
// GET /api/v2/s3-integrations
export const useGetS3Integration = (enabled = true) => {
return useQuery({
...s3IntegrationsQueryKeys.s3IntegrationGet,
queryFn: getS3Integration,
enabled,
});
};
// POST /api/v2/s3-integrations
export const useCreateS3Integration = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateS3IntegrationRequest) => createS3Integration(data),
onSuccess: () => {
// Invalidate the integration query to refetch the updated data
queryClient.invalidateQueries({
queryKey: s3IntegrationsQueryKeys.s3IntegrationGet.queryKey,
refetchType: 'all',
});
},
});
};
// DELETE /api/v2/s3-integrations/:id
export const useDeleteS3Integration = () => {
const queryClient = useQueryClient();
const { openConfirmModal } = useBusterNotifications();
const mutationFn = useMemoizedFn(async (id: string) => {
return openConfirmModal({
title: 'Remove Storage Integration',
content:
'Are you sure you want to remove the storage integration? This will disconnect your storage bucket from Buster.',
primaryButtonProps: {
text: 'Remove',
},
onOk: async () => deleteS3Integration(id),
});
});
return useMutation({
mutationFn,
onSuccess: () => {
// Invalidate the integration query to refetch the updated data
queryClient.invalidateQueries({
queryKey: s3IntegrationsQueryKeys.s3IntegrationGet.queryKey,
refetchType: 'all',
});
},
});
};

View File

@ -0,0 +1,29 @@
import type {
CreateS3IntegrationRequest,
GetS3IntegrationResponse,
CreateS3IntegrationResponse,
DeleteS3IntegrationResponse
} from '@buster/server-shared/s3-integrations';
import { mainApiV2 } from '../instances';
// Using mainApiV2 for v2 endpoints
// GET /api/v2/s3-integrations
export const getS3Integration = async (): Promise<GetS3IntegrationResponse> => {
const response = await mainApiV2.get('/s3-integrations');
return response.data;
};
// POST /api/v2/s3-integrations
export const createS3Integration = async (
data: CreateS3IntegrationRequest
): Promise<CreateS3IntegrationResponse> => {
const response = await mainApiV2.post('/s3-integrations', data);
return response.data;
};
// DELETE /api/v2/s3-integrations/:id
export const deleteS3Integration = async (id: string): Promise<DeleteS3IntegrationResponse> => {
const response = await mainApiV2.delete(`/s3-integrations/${id}`);
return response.data;
};

View File

@ -0,0 +1,15 @@
import type { GetS3IntegrationResponse } from '@buster/server-shared/s3-integrations';
import { queryOptions } from '@tanstack/react-query';
export const s3IntegrationGet = queryOptions<GetS3IntegrationResponse>({
queryKey: ['s3-integrations', 'get'] as const,
});
export const s3IntegrationsList = queryOptions<GetS3IntegrationResponse>({
queryKey: ['s3-integrations', 'list'] as const,
});
export const s3IntegrationsQueryKeys = {
s3IntegrationGet,
s3IntegrationsList,
};

View File

@ -0,0 +1,3 @@
export const IntegrationSkeleton = () => {
return <div className="bg-gray-light/20 h-30 w-full animate-pulse rounded"></div>;
};

View File

@ -0,0 +1,306 @@
'use client';
import React, { useMemo } from 'react';
type SlackSharingPermission = 'shareWithWorkspace' | 'shareWithChannel' | 'noSharing';
import pluralize from 'pluralize';
import {
useGetSlackChannels,
useGetSlackIntegration,
useInitiateSlackOAuth,
useRemoveSlackIntegration,
useUpdateSlackIntegration,
} from '@/api/buster_rest/slack/queryRequests';
import { Button } from '@/components/ui/buttons';
import { StatusCard } from '@/components/ui/card/StatusCard';
import {
createDropdownItems,
Dropdown,
type IDropdownItem,
type IDropdownItems,
} from '@/components/ui/dropdown';
import { ChevronDown } from '@/components/ui/icons';
import { SlackIcon } from '@/components/ui/icons/customIcons/SlackIcon';
import LinkSlash from '@/components/ui/icons/NucleoIconOutlined/link-slash';
import Refresh2 from '@/components/ui/icons/NucleoIconOutlined/refresh-2';
import { Text } from '@/components/ui/typography';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { SettingsCards } from '../settings/SettingsCard';
import { IntegrationSkeleton } from './IntegrationSkeleton';
export const SlackIntegrations = React.memo(() => {
const {
data: slackIntegration,
isFetched: isFetchedSlackIntegration,
error: slackIntegrationError,
} = useGetSlackIntegration();
const isConnected = slackIntegration?.connected ?? false;
const cards = useMemo(() => {
const sections = [
<ConnectSlackCard key="connect-slack-card" />,
isConnected && <ConnectedSlackChannels key="connected-slack-channels" />,
isConnected && <SlackSharingPermissions key="slack-sharing-permissions" />,
].filter(Boolean);
return [{ sections }];
}, [isConnected]);
if (slackIntegrationError) {
return <StatusCard message="Error fetching Slack integration." variant={'danger'} />;
}
if (!isFetchedSlackIntegration) {
return <IntegrationSkeleton />;
}
return <SettingsCards title="Slack" description="Connect Buster with Slack" cards={cards} />;
});
SlackIntegrations.displayName = 'SlackIntegrations';
const ConnectSlackCard = React.memo(() => {
const { data: slackIntegration } = useGetSlackIntegration();
const { mutate: initiateSlackOAuth } = useInitiateSlackOAuth();
const isConnected = slackIntegration?.connected;
const needsReinstall = slackIntegration?.status === 're_install_required';
return (
<div className="flex items-center justify-between gap-x-2">
<div className="flex space-x-2">
<div className="bg-item-select flex items-center justify-center rounded p-2">
<SlackIcon size={16} />
</div>
<div className="flex flex-col space-y-0.5">
<Text>Slack account</Text>
<Text variant="secondary" size={'xs'}>
Link your Slack account to use Buster from Slack
</Text>
</div>
</div>
{isConnected ? (
needsReinstall ? (
<Button
prefix={<SlackIcon size={16} />}
onClick={() => initiateSlackOAuth()}
size={'tall'}
className="border-yellow-500 bg-yellow-50 text-yellow-700 hover:bg-yellow-100"
>
Re-install Required
</Button>
) : (
<ConnectedDropdown />
)
) : (
<Button prefix={<SlackIcon size={16} />} onClick={() => initiateSlackOAuth()} size={'tall'}>
Connect Slack
</Button>
)}
</div>
);
});
ConnectSlackCard.displayName = 'ConnectSlackCard';
const ConnectedDropdown = React.memo(() => {
const { mutate: removeSlackIntegration, isPending } = useRemoveSlackIntegration();
const dropdownItems: IDropdownItems = [
{
value: 'disconnect',
label: 'Disconnect',
icon: <LinkSlash />,
onClick: () => {
removeSlackIntegration();
},
loading: isPending,
},
];
return (
<Dropdown items={dropdownItems} align="end" side="bottom" selectType="single">
<div className="hover:bg-item-hover flex! cursor-pointer items-center space-x-1.5 rounded p-1.5">
<div className="bg-success-foreground h-2.5 w-2.5 rounded-full" />
<Text className="select-none">Connected</Text>
</div>
</Dropdown>
);
});
ConnectedDropdown.displayName = 'ConnectedDropdown';
const ConnectedSlackChannels = React.memo(() => {
const { data: slackIntegration, isLoading: isLoadingSlackIntegration } = useGetSlackIntegration();
const {
data: slackChannelsData,
isLoading: isLoadingSlackChannels,
isRefetching: isRefetchingSlackChannels,
refetch: refetchSlackChannels,
isFetched: isFetchedSlackChannels,
error: slackChannelsError,
} = useGetSlackChannels();
const { mutate: updateSlackIntegration } = useUpdateSlackIntegration();
const channels = slackChannelsData?.channels || [];
const selectedChannelId = slackIntegration?.integration?.default_channel?.id;
const items = useMemo(() => {
return channels.map((channel) => ({
label: channel.name,
value: channel.id,
selected: channel.id === selectedChannelId,
}));
}, [channels, selectedChannelId]);
const numberOfSelectedChannels = useMemo(() => {
return items.filter((item) => item.selected).length;
}, [items]);
const onSelect = useMemoizedFn((channelId: string) => {
const channel = channels.find((channel) => channel.id === channelId);
if (!channel) return;
updateSlackIntegration({
default_channel: channel,
});
});
const showLoadingButton =
isLoadingSlackChannels || isLoadingSlackIntegration || isRefetchingSlackChannels;
return (
<div className="flex items-center justify-between space-x-4">
<div className="flex flex-col space-y-0.5">
<Text>Alerts channel</Text>
<Text variant="secondary" size={'xs'}>
Select which Slack channel Buster should send alerts to
</Text>
</div>
<div className="flex min-w-0 flex-1 items-center justify-end space-x-2">
{!slackChannelsError ? (
<>
{isFetchedSlackChannels && (
<Button
size={'tall'}
variant="ghost"
loading={showLoadingButton}
suffix={
!showLoadingButton && (
<span className="flex items-center justify-center text-base">
<Refresh2 />
</span>
)
}
onClick={() => refetchSlackChannels()}
>
Refresh
</Button>
)}
<Dropdown
selectType="multiple"
items={items}
onSelect={onSelect}
menuHeader="Search channels"
className="w-fit min-w-40"
>
<WeirdFakeSelectButtonForBlake
label={
numberOfSelectedChannels > 0
? `${numberOfSelectedChannels} ${pluralize('channel', numberOfSelectedChannels)} selected`
: 'Select a channel'
}
/>
</Dropdown>
</>
) : (
<div className="flex items-center space-x-2">
<Text variant="danger" size={'xs'}>
Error fetching channels.
</Text>
</div>
)}
</div>
</div>
);
});
ConnectedSlackChannels.displayName = 'ConnectedSlackChannels';
const SlackSharingPermissions = React.memo(() => {
const { data: slackIntegration } = useGetSlackIntegration();
const { mutate: updateSlackIntegration } = useUpdateSlackIntegration();
const selectedOption: SlackSharingPermission =
slackIntegration?.integration?.default_sharing_permissions || 'noSharing';
const sharingOptions: IDropdownItem<SlackSharingPermission>[] = (
createDropdownItems([
{
label: 'Workspace',
value: 'shareWithWorkspace' satisfies SlackSharingPermission,
secondaryLabel:
'All workspace members will have access to any chat created from any channel.',
},
// {
// label: 'Channel',
// value: 'shareWithChannel',
// secondaryLabel: 'All channel members will have access to any chat created from that channel.'
// },
{
label: 'None',
value: 'noSharing' satisfies SlackSharingPermission,
secondaryLabel: 'Only the user who sent the request will have access to their chat.',
},
]) satisfies IDropdownItem<SlackSharingPermission>[]
).map((option) => ({
...option,
selected: option.value === selectedOption,
}));
const selectedLabel = sharingOptions.find((option) => option.selected)?.label || 'Select option';
const handleSelect = useMemoizedFn((value: string) => {
updateSlackIntegration({
default_sharing_permissions: value as SlackSharingPermission,
});
});
return (
<div className="flex items-center justify-between space-x-4">
<div className="flex flex-col space-y-0.5">
<Text>Auto-share chats with other users</Text>
<Text variant="secondary" size={'xs'}>
Specify how chats are auto-shared when created from Slack channels
</Text>
</div>
<Dropdown
items={sharingOptions}
onSelect={handleSelect}
align="end"
side="bottom"
selectType="single"
>
<WeirdFakeSelectButtonForBlake label={selectedLabel} />
</Dropdown>
</div>
);
});
SlackSharingPermissions.displayName = 'SlackSharingPermissions';
const WeirdFakeSelectButtonForBlake = ({ label }: { label: string | React.ReactNode }) => {
return (
<div className="bg-background hover:bg-item-hover flex min-w-32 cursor-pointer items-center justify-between space-x-2 rounded border px-3 py-1.5 transition-colors">
<Text size="sm" className="truncate">
{label}
</Text>
<span className="text-icon-color flex items-center">
<ChevronDown />
</span>
</div>
);
};

View File

@ -0,0 +1,144 @@
import { useNavigate } from '@tanstack/react-router';
import React, { useMemo } from 'react';
import { useDeleteS3Integration, useGetS3Integration } from '@/api/buster_rest/s3-integrations';
import { Button } from '@/components/ui/buttons';
import { StatusCard } from '@/components/ui/card/StatusCard';
import { Dropdown, type IDropdownItems } from '@/components/ui/dropdown';
import Bucket from '@/components/ui/icons/NucleoIconOutlined/bucket';
import LinkSlash from '@/components/ui/icons/NucleoIconOutlined/link-slash';
import { Text } from '@/components/ui/typography';
import { SettingsCards } from '../settings/SettingsCard';
import { IntegrationSkeleton } from './IntegrationSkeleton';
export const StorageIntegrations = React.memo(() => {
const {
data: s3Integration,
isFetched: isFetchedS3Integration,
error: s3IntegrationError,
} = useGetS3Integration();
const isConnected = s3Integration !== null;
const cards = useMemo(() => {
const sections = [
<ConnectStorageCard key="connect-storage-card" />,
isConnected && <StorageConfiguration key="storage-configuration" />,
].filter(Boolean);
return [{ sections }];
}, [isConnected]);
if (s3IntegrationError) {
return <StatusCard message="Error fetching storage integration." variant={'danger'} />;
}
if (!isFetchedS3Integration) {
return <IntegrationSkeleton />;
}
return (
<SettingsCards
title="Object Storage"
description="Connect an S3 compatible storage bucket to Buster"
cards={cards}
/>
);
});
StorageIntegrations.displayName = 'StorageIntegrations';
const ConnectStorageCard = React.memo(() => {
const { data: s3Integration } = useGetS3Integration();
const navigate = useNavigate();
const isConnected = s3Integration !== null;
const handleConnect = () => {
navigate({ to: '/app/settings/storage/add' });
};
return (
<div className="flex items-center justify-between gap-x-2">
<div className="flex space-x-2">
<div className="bg-item-select flex items-center justify-center rounded p-2">
<Bucket strokewidth={1.5} />
</div>
<div className="flex flex-col space-y-0.5">
<Text>Storage account</Text>
<Text variant="secondary" size={'xs'}>
Link your storage bucket to use file storage with Buster
</Text>
</div>
</div>
{isConnected ? (
<ConnectedDropdown />
) : (
<Button prefix={<Bucket strokewidth={1.5} />} onClick={handleConnect} size={'tall'}>
Connect Storage
</Button>
)}
</div>
);
});
ConnectStorageCard.displayName = 'ConnectStorageCard';
const ConnectedDropdown = React.memo(() => {
const { data: s3Integration } = useGetS3Integration();
const { mutate: deleteS3Integration, isPending } = useDeleteS3Integration();
const dropdownItems: IDropdownItems = [
{
value: 'disconnect',
label: 'Disconnect',
icon: <LinkSlash />,
onClick: () => {
if (s3Integration?.id) {
deleteS3Integration(s3Integration.id);
}
},
loading: isPending,
},
];
return (
<Dropdown items={dropdownItems} align="end" side="bottom" selectType="single">
<div className="hover:bg-item-hover flex! cursor-pointer items-center space-x-1.5 rounded p-1.5">
<div className="bg-success-foreground h-2.5 w-2.5 rounded-full" />
<Text className="select-none">Connected</Text>
</div>
</Dropdown>
);
});
ConnectedDropdown.displayName = 'ConnectedDropdown';
const StorageConfiguration = React.memo(() => {
const { data: s3Integration } = useGetS3Integration();
if (!s3Integration) return null;
const providerLabels = {
s3: 'AWS S3',
r2: 'Cloudflare R2',
gcs: 'Google Cloud Storage',
};
return (
<div className="flex items-center justify-between space-x-4">
<div className="flex flex-col space-y-0.5">
<Text>Storage provider</Text>
<Text variant="secondary" size={'xs'}>
Currently connected to {providerLabels[s3Integration.provider]}
</Text>
</div>
<div className="flex items-center space-x-2">
<Text size="sm" className="text-icon-color">
{s3Integration.bucketName || providerLabels[s3Integration.provider]}
</Text>
</div>
</div>
);
});
StorageConfiguration.displayName = 'StorageConfiguration';

View File

@ -54,6 +54,8 @@ import { Route as AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesIndex
import { Route as AppAppAssetReportsReportIdLayoutIndexRouteImport } from './routes/app/_app/_asset/reports.$reportId/_layout/index'
import { Route as AppAppAssetMetricsMetricIdLayoutIndexRouteImport } from './routes/app/_app/_asset/metrics.$metricId/_layout/index'
import { Route as AppAppAssetDashboardsDashboardIdLayoutIndexRouteImport } from './routes/app/_app/_asset/dashboards.$dashboardId/_layout/index'
import { Route as AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRouteImport } from './routes/app/_settings/_restricted_layout/_admin_only/settings.storage.storageId'
import { Route as AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRouteImport } from './routes/app/_settings/_restricted_layout/_admin_only/settings.storage.add'
import { Route as AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRouteImport } from './routes/app/_settings/_restricted_layout/_admin_only/settings.datasources.add'
import { Route as AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesDatasourceIdRouteImport } from './routes/app/_settings/_restricted_layout/_admin_only/settings.datasources.$datasourceId'
import { Route as AppAppAssetReportsReportIdLayoutContentRouteImport } from './routes/app/_app/_asset/reports.$reportId/_layout/content'
@ -401,6 +403,20 @@ const AppAppAssetDashboardsDashboardIdLayoutIndexRoute =
path: '/',
getParentRoute: () => AppAppAssetDashboardsDashboardIdLayoutRoute,
} as any)
const AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRoute =
AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRouteImport.update(
{
id: '/settings/storage/storageId',
path: '/settings/storage/storageId',
getParentRoute: () => AppSettingsRestricted_layoutAdmin_onlyRoute,
} as any,
)
const AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRoute =
AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRouteImport.update({
id: '/settings/storage/add',
path: '/settings/storage/add',
getParentRoute: () => AppSettingsRestricted_layoutAdmin_onlyRoute,
} as any)
const AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRoute =
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRouteImport.update(
{
@ -719,6 +735,8 @@ export interface FileRoutesByFullPath {
'/app/reports/$reportId/content': typeof AppAppAssetReportsReportIdLayoutContentRoute
'/app/settings/datasources/$datasourceId': typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesDatasourceIdRoute
'/app/settings/datasources/add': typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRoute
'/app/settings/storage/add': typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRoute
'/app/settings/storage/storageId': typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRoute
'/app/dashboards/$dashboardId/': typeof AppAppAssetDashboardsDashboardIdLayoutIndexRoute
'/app/metrics/$metricId/': typeof AppAppAssetMetricsMetricIdLayoutIndexRoute
'/app/reports/$reportId/': typeof AppAppAssetReportsReportIdLayoutIndexRoute
@ -794,6 +812,8 @@ export interface FileRoutesByTo {
'/app/reports/$reportId/content': typeof AppAppAssetReportsReportIdLayoutContentRoute
'/app/settings/datasources/$datasourceId': typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesDatasourceIdRoute
'/app/settings/datasources/add': typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRoute
'/app/settings/storage/add': typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRoute
'/app/settings/storage/storageId': typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRoute
'/app/settings/datasources': typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesIndexRoute
'/app/chats/$chatId/dashboards/$dashboardId': typeof AppAppAssetChatsChatIdDashboardsDashboardIdLayoutIndexRoute
'/app/chats/$chatId/metrics/$metricId': typeof AppAppAssetChatsChatIdMetricsMetricIdLayoutIndexRoute
@ -869,6 +889,8 @@ export interface FileRoutesById {
'/app/_app/_asset/reports/$reportId/_layout/content': typeof AppAppAssetReportsReportIdLayoutContentRoute
'/app/_settings/_restricted_layout/_admin_only/settings/datasources/$datasourceId': typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesDatasourceIdRoute
'/app/_settings/_restricted_layout/_admin_only/settings/datasources/add': typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRoute
'/app/_settings/_restricted_layout/_admin_only/settings/storage/add': typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRoute
'/app/_settings/_restricted_layout/_admin_only/settings/storage/storageId': typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRoute
'/app/_app/_asset/dashboards/$dashboardId/_layout/': typeof AppAppAssetDashboardsDashboardIdLayoutIndexRoute
'/app/_app/_asset/metrics/$metricId/_layout/': typeof AppAppAssetMetricsMetricIdLayoutIndexRoute
'/app/_app/_asset/reports/$reportId/_layout/': typeof AppAppAssetReportsReportIdLayoutIndexRoute
@ -954,6 +976,8 @@ export interface FileRouteTypes {
| '/app/reports/$reportId/content'
| '/app/settings/datasources/$datasourceId'
| '/app/settings/datasources/add'
| '/app/settings/storage/add'
| '/app/settings/storage/storageId'
| '/app/dashboards/$dashboardId/'
| '/app/metrics/$metricId/'
| '/app/reports/$reportId/'
@ -1029,6 +1053,8 @@ export interface FileRouteTypes {
| '/app/reports/$reportId/content'
| '/app/settings/datasources/$datasourceId'
| '/app/settings/datasources/add'
| '/app/settings/storage/add'
| '/app/settings/storage/storageId'
| '/app/settings/datasources'
| '/app/chats/$chatId/dashboards/$dashboardId'
| '/app/chats/$chatId/metrics/$metricId'
@ -1103,6 +1129,8 @@ export interface FileRouteTypes {
| '/app/_app/_asset/reports/$reportId/_layout/content'
| '/app/_settings/_restricted_layout/_admin_only/settings/datasources/$datasourceId'
| '/app/_settings/_restricted_layout/_admin_only/settings/datasources/add'
| '/app/_settings/_restricted_layout/_admin_only/settings/storage/add'
| '/app/_settings/_restricted_layout/_admin_only/settings/storage/storageId'
| '/app/_app/_asset/dashboards/$dashboardId/_layout/'
| '/app/_app/_asset/metrics/$metricId/_layout/'
| '/app/_app/_asset/reports/$reportId/_layout/'
@ -1529,6 +1557,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppAppAssetDashboardsDashboardIdLayoutIndexRouteImport
parentRoute: typeof AppAppAssetDashboardsDashboardIdLayoutRoute
}
'/app/_settings/_restricted_layout/_admin_only/settings/storage/storageId': {
id: '/app/_settings/_restricted_layout/_admin_only/settings/storage/storageId'
path: '/settings/storage/storageId'
fullPath: '/app/settings/storage/storageId'
preLoaderRoute: typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRouteImport
parentRoute: typeof AppSettingsRestricted_layoutAdmin_onlyRoute
}
'/app/_settings/_restricted_layout/_admin_only/settings/storage/add': {
id: '/app/_settings/_restricted_layout/_admin_only/settings/storage/add'
path: '/settings/storage/add'
fullPath: '/app/settings/storage/add'
preLoaderRoute: typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRouteImport
parentRoute: typeof AppSettingsRestricted_layoutAdmin_onlyRoute
}
'/app/_settings/_restricted_layout/_admin_only/settings/datasources/add': {
id: '/app/_settings/_restricted_layout/_admin_only/settings/datasources/add'
path: '/settings/datasources/add'
@ -2286,6 +2328,8 @@ interface AppSettingsRestricted_layoutAdmin_onlyRouteChildren {
AppSettingsRestricted_layoutAdmin_onlySettingsWorkspaceRoute: typeof AppSettingsRestricted_layoutAdmin_onlySettingsWorkspaceRoute
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesDatasourceIdRoute: typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesDatasourceIdRoute
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRoute: typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRoute
AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRoute: typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRoute
AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRoute: typeof AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRoute
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesIndexRoute: typeof AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesIndexRoute
}
@ -2303,6 +2347,10 @@ const AppSettingsRestricted_layoutAdmin_onlyRouteChildren: AppSettingsRestricted
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesDatasourceIdRoute,
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRoute:
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesAddRoute,
AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRoute:
AppSettingsRestricted_layoutAdmin_onlySettingsStorageAddRoute,
AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRoute:
AppSettingsRestricted_layoutAdmin_onlySettingsStorageStorageIdRoute,
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesIndexRoute:
AppSettingsRestricted_layoutAdmin_onlySettingsDatasourcesIndexRoute,
}

View File

@ -1,4 +1,7 @@
import { createFileRoute } from '@tanstack/react-router';
import { SlackIntegrations } from '@/components/features/integrations/SlackIntegrations';
import { StorageIntegrations } from '@/components/features/integrations/StorageIntegrations';
import { SettingsPageHeader } from '@/components/features/settings';
export const Route = createFileRoute(
'/app/_settings/_restricted_layout/_admin_only/settings/integrations'
@ -15,5 +18,17 @@ export const Route = createFileRoute(
});
function RouteComponent() {
return <div>Hello "/app/settings/integrations"!</div>;
return (
<>
<SettingsPageHeader
title="Integrations"
description="Connect Buster with other apps and services"
/>
<div className="flex flex-col space-y-6">
<SlackIntegrations />
<StorageIntegrations />
</div>
</>
);
}

View File

@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute(
'/app/_settings/_restricted_layout/_admin_only/settings/storage/add',
)({
component: RouteComponent,
})
function RouteComponent() {
return (
<div>
Hello "/app/_settings/_restricted_layout/_admin_only/storage/add"!
</div>
)
}

View File

@ -0,0 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute(
'/app/_settings/_restricted_layout/_admin_only/settings/storage/storageId',
)({
component: RouteComponent,
})
function RouteComponent() {
return (
<div>
Hello "/app/_settings/_restricted_layout/_admin_only/storage/storageId"!
</div>
)
}