initial slack build out (no apis)

This commit is contained in:
Nate Kelley 2025-07-09 11:46:27 -06:00
parent 12fa022cfe
commit 66cae400d6
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
19 changed files with 130 additions and 37 deletions

View File

@ -7,7 +7,7 @@ import { Button } from '@/components/ui/buttons';
import { Plus } from '@/components/ui/icons'; import { Plus } from '@/components/ui/icons';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { useDebounceSearch } from '@/hooks/useDebounceSearch'; import { useDebounceSearch } from '@/hooks/useDebounceSearch';
import { SettingsPageHeader } from '../../_components/SettingsPageHeader'; import { SettingsPageHeader } from '@/components/features/settings';
import { ListDatasetGroupsComponent } from './ListDatasetGroupsComponent'; import { ListDatasetGroupsComponent } from './ListDatasetGroupsComponent';
export default function Page() { export default function Page() {

View File

@ -10,7 +10,7 @@ import { Button } from '@/components/ui/buttons';
import { Plus } from '@/components/ui/icons'; import { Plus } from '@/components/ui/icons';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { useDebounceSearch } from '@/hooks/useDebounceSearch'; import { useDebounceSearch } from '@/hooks/useDebounceSearch';
import { SettingsPageHeader } from '../../_components/SettingsPageHeader'; import { SettingsPageHeader } from '@/components/features/settings';
import { ListPermissionGroupsComponent } from './ListPermissionGroupsComponent'; import { ListPermissionGroupsComponent } from './ListPermissionGroupsComponent';
export default function Page() { export default function Page() {

View File

@ -9,7 +9,7 @@ import { useInviteModalStore } from '@/context/BusterAppLayout';
import { useUserConfigContextSelector } from '@/context/Users'; import { useUserConfigContextSelector } from '@/context/Users';
import { useMemoizedFn } from '@/hooks'; import { useMemoizedFn } from '@/hooks';
import { useDebounceSearch } from '@/hooks/useDebounceSearch'; import { useDebounceSearch } from '@/hooks/useDebounceSearch';
import { SettingsPageHeader } from '../../_components/SettingsPageHeader'; import { SettingsPageHeader } from '@/components/features/settings';
import { ListUsersComponent } from './ListUsersComponent'; import { ListUsersComponent } from './ListUsersComponent';
export default function Page() { export default function Page() {

View File

@ -1,4 +1,4 @@
import { SettingsPageHeader } from '../../../_components/SettingsPageHeader'; import { SettingsPageHeader } from '@/components/features/settings';
import { ApiKeysController } from './ApiKeysController'; import { ApiKeysController } from './ApiKeysController';
import type { Metadata } from 'next'; import type { Metadata } from 'next';

View File

@ -1,4 +1,4 @@
import { SettingsPageHeader } from '../../../_components/SettingsPageHeader'; import { SettingsPageHeader } from '@/components/features/settings';
import { DatasourceList } from './_DatasourceList'; import { DatasourceList } from './_DatasourceList';
export default function Page() { export default function Page() {

View File

@ -0,0 +1,18 @@
import { SlackIntegrations } from '@/components/features/integrations/SlackIntegrations';
import { SettingsPageHeader } from '@/components/features/settings';
import { SettingsCards } from '@/components/features/settings';
export default function IntegrationsPage() {
return (
<>
<SettingsPageHeader
title="Integrations"
description="Connect Buster with other apps and services"
/>
<div className="flex flex-col space-y-6">
<SlackIntegrations />
</div>
</>
);
}

View File

@ -1,23 +1,21 @@
import { InviteLinks } from '@/components/features/security/InviteLinks'; import { InviteLinks } from '@/components/features/security/InviteLinks';
import { SettingsPageHeader } from '../../../_components/SettingsPageHeader'; import { SettingsPageHeader } from '@/components/features/settings';
import { ApprovedEmailDomains } from '@/components/features/security/ApprovedEmailDomains'; import { ApprovedEmailDomains } from '@/components/features/security/ApprovedEmailDomains';
import { WorkspaceRestrictions } from '@/components/features/security/WorkspaceRestrictions'; import { WorkspaceRestrictions } from '@/components/features/security/WorkspaceRestrictions';
export default function Page() { export default function Page() {
return ( return (
<div className="flex flex-col space-y-4"> <>
<div> <SettingsPageHeader
<SettingsPageHeader title="Security"
title="Security" description="Manage security and general permission settings"
description="Manage security and general permission settings" />
/>
<div className="flex flex-col space-y-6"> <div className="flex flex-col space-y-6">
<InviteLinks /> <InviteLinks />
<ApprovedEmailDomains /> <ApprovedEmailDomains />
<WorkspaceRestrictions /> <WorkspaceRestrictions />
</div>
</div> </div>
</div> </>
); );
} }

View File

@ -4,7 +4,7 @@ import { Avatar } from '@/components/ui/avatar';
import { Text, Title } from '@/components/ui/typography'; import { Text, Title } from '@/components/ui/typography';
import { useUserConfigContextSelector } from '@/context/Users/BusterUserConfigProvider'; import { useUserConfigContextSelector } from '@/context/Users/BusterUserConfigProvider';
import { formatDate } from '@/lib/date'; import { formatDate } from '@/lib/date';
import { SettingsPageHeader } from '../../_components/SettingsPageHeader'; import { SettingsPageHeader } from '@/components/features/settings';
export default function ProfilePage() { export default function ProfilePage() {
const user = useUserConfigContextSelector((state) => state.user); const user = useUserConfigContextSelector((state) => state.user);

View File

@ -0,0 +1,38 @@
import React from 'react';
import { SettingsCards } from '../settings/SettingsCard';
import { SlackIcon } from '@/components/ui/icons/customIcons/SlackIcon';
import { Text } from '@/components/ui/typography';
import { Button } from '@/components/ui/buttons';
export const SlackIntegrations = React.memo(() => {
return (
<SettingsCards
title="Slack"
description="Connect Buster with Slack"
cards={[{ sections: [<ConnectSlackCard />] }]}
/>
);
});
SlackIntegrations.displayName = 'SlackIntegrations';
const ConnectSlackCard = React.memo(() => {
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>
<Button prefix={<SlackIcon size={16} />} size={'tall'}>
Connect Slack
</Button>
</div>
);
});

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { SecurityCards } from './SecurityCards'; import { SettingsCards } from '../settings/SettingsCard';
import { Input } from '@/components/ui/inputs'; import { Input } from '@/components/ui/inputs';
import { Button } from '@/components/ui/buttons'; import { Button } from '@/components/ui/buttons';
import { Text } from '@/components/ui/typography'; import { Text } from '@/components/ui/typography';
@ -71,7 +71,7 @@ export const ApprovedEmailDomains = React.memo(() => {
); );
return ( return (
<SecurityCards <SettingsCards
title="Approved email domains" title="Approved email domains"
description="Anyone with an email address at these domains is allowed to sign up for this workspace" description="Anyone with an email address at these domains is allowed to sign up for this workspace"
cards={[{ sections }]} cards={[{ sections }]}
@ -91,7 +91,7 @@ const AddDomainInput = React.memo(
const handleAddDomain = useMemoizedFn(async () => { const handleAddDomain = useMemoizedFn(async () => {
const domain = newDomain.trim(); const domain = newDomain.trim();
if (!domain) return; if (!domain) return;
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]\.[a-zA-Z]{2,}$/; const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]\.[a-zA-Z]{2,}$/;
if (!domainRegex.test(domain)) { if (!domainRegex.test(domain)) {
openErrorMessage('Please enter a valid domain name'); openErrorMessage('Please enter a valid domain name');

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { SecurityCards } from './SecurityCards'; import { SettingsCards } from '../settings/SettingsCard';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/inputs'; import { Input } from '@/components/ui/inputs';
import { Button } from '@/components/ui/buttons'; import { Button } from '@/components/ui/buttons';
@ -34,7 +34,7 @@ export const InviteLinks = React.memo(() => {
}; };
return ( return (
<SecurityCards <SettingsCards
title="Invite links" title="Invite links"
description="A uniquely generated invite link allows anyone with the link to join your workspace" description="A uniquely generated invite link allows anyone with the link to join your workspace"
cards={[ cards={[

View File

@ -1,9 +1,8 @@
'use client'; 'use client';
import React, { useMemo, type ReactNode } from 'react'; import React, { useMemo, type ReactNode } from 'react';
import { SecurityCards } from './SecurityCards'; import { SettingsCards } from '../settings/SettingsCard';
import { Text } from '@/components/ui/typography'; import { Text } from '@/components/ui/typography';
import { Button } from '@/components/ui/buttons';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { import {
useGetWorkspaceSettings, useGetWorkspaceSettings,
@ -45,7 +44,7 @@ export const WorkspaceRestrictions = React.memo(() => {
); );
return ( return (
<SecurityCards <SettingsCards
title="Workspace restrictions" title="Workspace restrictions"
description="Restrict the workspace to only allow users with an email address at these domains" description="Restrict the workspace to only allow users with an email address at these domains"
cards={[{ sections }]} cards={[{ sections }]}

View File

@ -1,12 +1,12 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { SecurityCards } from './SecurityCards'; import { SettingsCards } from './SettingsCard';
import { Button } from '@/components/ui/buttons'; import { Button } from '@/components/ui/buttons';
import { Pill } from '@/components/ui/pills/Pill'; import { Pill } from '@/components/ui/pills/Pill';
import { Text } from '@/components/ui/typography'; import { Text } from '@/components/ui/typography';
const meta: Meta<typeof SecurityCards> = { const meta: Meta<typeof SettingsCards> = {
title: 'Features/SecurityCards', title: 'Features/SettingsCards',
component: SecurityCards, component: SettingsCards,
parameters: { parameters: {
layout: 'padded', layout: 'padded',
docs: { docs: {
@ -20,7 +20,7 @@ const meta: Meta<typeof SecurityCards> = {
}; };
export default meta; export default meta;
type Story = StoryObj<typeof SecurityCards>; type Story = StoryObj<typeof SettingsCards>;
// Mock data for different use cases // Mock data for different use cases
const basicSections = [ const basicSections = [

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Title, Paragraph } from '@/components/ui/typography'; import { Title, Paragraph } from '@/components/ui/typography';
import { cn } from '@/lib/classMerge'; import { cn } from '@/lib/classMerge';
interface SecurityCardsProps { interface SettingsCardsProps {
title: string; title: string;
description: string; description: string;
cards: { cards: {
@ -10,7 +10,7 @@ interface SecurityCardsProps {
}[]; }[];
} }
export const SecurityCards: React.FC<SecurityCardsProps> = ({ title, description, cards }) => { export const SettingsCards: React.FC<SettingsCardsProps> = ({ title, description, cards }) => {
return ( return (
<div className="flex flex-col space-y-3.5"> <div className="flex flex-col space-y-3.5">
<div className="flex flex-col space-y-1.5"> <div className="flex flex-col space-y-1.5">
@ -20,17 +20,17 @@ export const SecurityCards: React.FC<SecurityCardsProps> = ({ title, description
<Paragraph variant="secondary">{description}</Paragraph> <Paragraph variant="secondary">{description}</Paragraph>
</div> </div>
{cards.map((card, index) => ( {cards.map((card, index) => (
<SecurityCard key={index} sections={card.sections} /> <SettingsCard key={index} sections={card.sections} />
))} ))}
</div> </div>
); );
}; };
const SecurityCard = ({ sections }: { sections: React.ReactNode[] }) => { const SettingsCard = ({ sections }: { sections: React.ReactNode[] }) => {
return ( return (
<div className="flex flex-col rounded border"> <div className="flex flex-col rounded border">
{sections.map((section, index) => ( {sections.map((section, index) => (
<div key={index} className={cn(index !== sections.length - 1 && 'border-b', 'px-4 py-2.5')}> <div key={index} className={cn(index !== sections.length - 1 && 'border-b', 'px-4 py-3.5')}>
{section} {section}
</div> </div>
))} ))}

View File

@ -1,2 +1,3 @@
export * from './SettingsEmptyState'; export * from './SettingsEmptyState';
export * from './SettingsPageHeader'; export * from './SettingsPageHeader';
export * from './SettingsCard';

View File

@ -39,6 +39,11 @@ const workspaceItems = (currentParentRoute: BusterRoutes): ISidebarGroup => ({
label: 'Data Sources', label: 'Data Sources',
route: createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES }), route: createBusterRoute({ route: BusterRoutes.SETTINGS_DATASOURCES }),
id: BusterRoutes.SETTINGS_DATASOURCES id: BusterRoutes.SETTINGS_DATASOURCES
},
{
label: 'Integrations',
route: createBusterRoute({ route: BusterRoutes.SETTINGS_INTEGRATIONS }),
id: createBusterRoute({ route: BusterRoutes.SETTINGS_INTEGRATIONS })
} }
].map((item) => ({ ].map((item) => ({
...item, ...item,

View File

@ -0,0 +1,34 @@
import React from 'react';
export const SlackIcon: React.FC<
React.SVGProps<SVGSVGElement> & {
size?: number;
}
> = ({ className, size = 24, ...props }) => {
return (
<svg
width={size}
height={size}
viewBox="0 0 127 127"
xmlns="http://www.w3.org/2000/svg"
className={className}
{...props}>
<path
d="M27.2 80c0 7.3-5.9 13.2-13.2 13.2C6.7 93.2.8 87.3.8 80c0-7.3 5.9-13.2 13.2-13.2h13.2V80zm6.6 0c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2v33c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V80z"
fill="#E01E5A"
/>
<path
d="M47 27c-7.3 0-13.2-5.9-13.2-13.2C33.8 6.5 39.7.6 47 .6c7.3 0 13.2 5.9 13.2 13.2V27H47zm0 6.7c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H13.9C6.6 60.1.7 54.2.7 46.9c0-7.3 5.9-13.2 13.2-13.2H47z"
fill="#36C5F0"
/>
<path
d="M99.9 46.9c0-7.3 5.9-13.2 13.2-13.2 7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H99.9V46.9zm-6.6 0c0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V13.8C66.9 6.5 72.8.6 80.1.6c7.3 0 13.2 5.9 13.2 13.2v33.1z"
fill="#2EB67D"
/>
<path
d="M80.1 99.8c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2-7.3 0-13.2-5.9-13.2-13.2V99.8h13.2zm0-6.6c-7.3 0-13.2-5.9-13.2-13.2 0-7.3 5.9-13.2 13.2-13.2h33.1c7.3 0 13.2 5.9 13.2 13.2 0 7.3-5.9 13.2-13.2 13.2H80.1z"
fill="#ECB22E"
/>
</svg>
);
};