-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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(dashboard): integrations update and create flow #7281
Merged
Merged
Changes from all commits
Commits
Show all changes
124 commits
Select commit
Hold shift + click to select a range
3c289cd
feat: wip
scopsy b016b5d
feat: wip
scopsy 313cdad
Merge branch 'next' into feat-integrations-page
scopsy d36b41c
Update .source
scopsy 3dbeaae
fix: styles
scopsy 23b6e4a
Merge branch 'next' into feat-integrations-page
scopsy ce40aca
fix: refactor cards
scopsy 75fffc6
fix: paths
scopsy 1503299
fix: add items
scopsy 07ce663
feat: refactored cards
scopsy 824f36d
fix: buttons
scopsy 362578f
fix: added fields
scopsy e78b513
fix: integration styling
scopsy 9f61bbc
fix: scrolling
scopsy 1aa3886
Merge branch 'next' into feat-integrations-page
scopsy fb5fcbc
fix: reusabe update sidebar
scopsy 48f3173
fix: error
scopsy 481822d
fix: remove unused
scopsy 99b7eb8
fix: headears
scopsy 5f69c65
fix: provider config
scopsy 5dea17a
fix: order
scopsy 606660b
fix: minor adjustments
scopsy f9c9989
fix: minor period
scopsy eb606bc
Merge branch 'next' into feat-integrations-page
scopsy 7640653
Update integrations-list-page.tsx
scopsy 6ccb7d2
fix: inbox connected state
scopsy b00f235
demo provider
scopsy a11a03a
Merge branch 'next' into feat-integrations-page
scopsy bd9f69e
Merge branch 'next' into feat-integrations-page
scopsy 1e02633
fix: align wokrlfows list
scopsy 241cfd7
fix: minor fixes
scopsy 9b662be
fix: minor changes
scopsy ae1f6ed
fix: changes
scopsy 704c32f
fix: renaming
scopsy 844a547
fix: refactors
scopsy a76d20a
fix: items
scopsy d68a59b
fix: add base
scopsy 3d2591c
fix: locaitons
scopsy 95ecd6a
remove logos
scopsy 31f7e58
Delete
scopsy bf4bf94
fix: revert
scopsy c49519f
Merge branch 'feat-integrations-page' of https://github.com/novuhq/no…
scopsy 46dd4ac
Merge branch 'next' into feat-integrations-page
scopsy acc38fd
fix: minor fixes
scopsy 0328579
fix: items
scopsy 926915a
Merge branch 'next' into feat-integrations-page
scopsy 086d8d5
Merge branch 'next' into feat-integrations-page
scopsy 9a27ea1
feat: add segmented control
scopsy a109bfc
fix: env selection
scopsy ddb921c
fix: connect button
scopsy 7951524
fix: navigation
scopsy 025cc81
fix: primary check
scopsy a620d39
fix: feature flag
scopsy 36d9bf8
fix: novu providers fetch
scopsy 2175dba
fix: pr review
scopsy dccf725
fix: reload doc
scopsy 3e60fe8
fix: integration step import
scopsy 85f1d56
fix: integrations list page
scopsy 425a095
refactor: remove pr
scopsy 66c4cdf
Revert "fix: integrations list page"
scopsy 1e445c7
Revert "refactor: remove pr"
scopsy 9b7e1c2
fix: update src
scopsy 6030684
fix: types
scopsy a026fd6
fix: types
scopsy 4e41c9c
Merge branch 'feat-new-integrations-page-table' into feat-integration…
scopsy b842f4f
Revert "fix: types"
scopsy 3b5f586
fix: imports
scopsy 89d2ec4
fix: import
scopsy ee8bb1e
Merge branch 'next' into feat-new-integrations-page-table
scopsy b072e00
fix: revert file
scopsy 788dba3
Merge branch 'feat-new-integrations-page-table' of https://github.com…
scopsy ad7b420
Merge branch 'feat-new-integrations-page-table' into feat-integration…
scopsy 1a0eff0
fix: query keys
scopsy c7ca662
fix: types
scopsy c7315ca
fix: types
scopsy 7fe2f01
fix: refactor
scopsy 8843fce
fix: review
scopsy cfc4b78
fix: bug
scopsy b17543a
Merge branch 'next' into feat-new-integrations-page-table
scopsy 97f9f29
Merge branch 'feat-new-integrations-page-table' into feat-integration…
scopsy baa0df3
Merge branch 'next' into feat-new-integrations-page-table
scopsy 316efad
fix: pr comments
scopsy ca70188
Merge branch 'next' into feat-new-integrations-page-table
scopsy 8fece59
Merge branch 'feat-new-integrations-page-table' into feat-integration…
scopsy 1d0a181
fix: pr comments
scopsy 01a9189
fix: pr comments
scopsy aede118
fix: primary modal
scopsy 871a00e
fix: interim
scopsy a808a62
fix: refactor
scopsy 1d862f8
fix: items
scopsy a94b5ee
fix: flow
scopsy 4ecc577
fix: pr comments
scopsy 2e272cb
fix: pr comments
scopsy 4be39c4
fix: items
scopsy 0c4079b
fix: secret
scopsy 4465165
fix: pr comments
scopsy 6bb7678
fix: header title
scopsy 06ce8c4
fix: clicking on the inline toast
scopsy 189d0b2
fix: show error
scopsy 61cdacc
fix: connected passing
scopsy 7349523
Merge branch 'feat-new-integrations-page-table' into feat-integration…
scopsy b67bd97
Merge branch 'next' into feat-new-integrations-page-table
scopsy 79b68fb
fix: autocomplete
scopsy 1b9281e
fix: imports
scopsy 3300ec3
fix: paths
scopsy dedea45
fix: export
scopsy 7030627
fix: current env
scopsy 491d6c2
Merge branch 'feat-new-integrations-page-table' into feat-integration…
scopsy aae62cc
fix: rename
scopsy 6e1980d
Merge branch 'feat-integrations-page' of https://github.com/novuhq/no…
scopsy 8b4e997
Merge branch 'next' into feat-new-integrations-page-table
scopsy 2604b05
Merge branch 'next' into feat-new-integrations-page-table
scopsy 596ca8a
Merge branch 'next' into feat-new-integrations-page-table
scopsy 7adc69b
fix: lock file
scopsy 0f15abb
Delete pnpm-lock.yaml
scopsy b5c056e
fix: pnpm file
scopsy 413ae89
bull mq pro
scopsy 1f3c693
fix: pnpm
scopsy f59147c
fix:
scopsy fb494b4
Merge branch 'feat-new-integrations-page-table' into feat-integration…
scopsy 91aa132
Merge branch 'next' into feat-integrations-page
scopsy cbbf0fb
fix:
scopsy 31094fc
fix: add space validation
scopsy b97f518
fix: primaryselecion
scopsy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Submodule .source
updated
from dfa9f1 to 047015
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
apps/dashboard/src/components/integrations/components/channel-tabs.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/primitives/tabs'; | ||
import { CHANNEL_TYPE_TO_STRING } from '@/utils/channels'; | ||
import { IProviderConfig } from '@novu/shared'; | ||
import { IntegrationListItem } from './integration-list-item'; | ||
import { INTEGRATION_CHANNELS } from '../utils/channels'; | ||
|
||
type ChannelTabsProps = { | ||
integrationsByChannel: Record<string, IProviderConfig[]>; | ||
searchQuery: string; | ||
onIntegrationSelect: (integrationId: string) => void; | ||
}; | ||
|
||
export function ChannelTabs({ integrationsByChannel, searchQuery, onIntegrationSelect }: ChannelTabsProps) { | ||
return ( | ||
<Tabs defaultValue={INTEGRATION_CHANNELS[0]} className="flex h-full flex-col"> | ||
<TabsList variant="regular" className="bg-background sticky top-0 z-10 gap-6 border-t-0 !px-3"> | ||
{INTEGRATION_CHANNELS.map((channel) => ( | ||
<TabsTrigger key={channel} value={channel} variant="regular" className="!px-0 !py-3"> | ||
{CHANNEL_TYPE_TO_STRING[channel]} | ||
</TabsTrigger> | ||
))} | ||
</TabsList> | ||
|
||
{INTEGRATION_CHANNELS.map((channel) => ( | ||
<TabsContent key={channel} value={channel} className="flex-1"> | ||
{integrationsByChannel[channel]?.length > 0 ? ( | ||
<div className="flex flex-col gap-4 p-3"> | ||
{integrationsByChannel[channel].map((integration) => ( | ||
<IntegrationListItem | ||
key={integration.id} | ||
integration={integration} | ||
onClick={() => onIntegrationSelect(integration.id)} | ||
/> | ||
))} | ||
</div> | ||
) : ( | ||
<EmptyState channel={channel} searchQuery={searchQuery} /> | ||
)} | ||
</TabsContent> | ||
))} | ||
</Tabs> | ||
); | ||
} | ||
|
||
function EmptyState({ channel, searchQuery }: { channel: string; searchQuery: string }) { | ||
return ( | ||
<div className="text-muted-foreground flex min-h-[200px] items-center justify-center text-center"> | ||
{searchQuery ? ( | ||
<p>No {channel.toLowerCase()} integrations match your search</p> | ||
) : ( | ||
<p>No {channel.toLowerCase()} integrations available</p> | ||
)} | ||
</div> | ||
); | ||
} |
146 changes: 146 additions & 0 deletions
146
apps/dashboard/src/components/integrations/components/create-integration-sidebar.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { useNavigate, useParams } from 'react-router-dom'; | ||
import { providers as novuProviders } from '@novu/shared'; | ||
import { useCreateIntegration } from '@/hooks/use-create-integration'; | ||
import { useIntegrationList } from './hooks/use-integration-list'; | ||
import { useSidebarNavigationManager } from './hooks/use-sidebar-navigation-manager'; | ||
import { IntegrationSheet } from './integration-sheet'; | ||
import { ChannelTabs } from './channel-tabs'; | ||
import { IntegrationConfiguration } from './integration-configuration'; | ||
import { Button } from '../../../components/primitives/button'; | ||
import { handleIntegrationError } from './utils/handle-integration-error'; | ||
import { useSetPrimaryIntegration } from '../../../hooks/use-set-primary-integration'; | ||
import { SelectPrimaryIntegrationModal } from './modals/select-primary-integration-modal'; | ||
import { IntegrationFormData } from '../types'; | ||
import { useIntegrationPrimaryModal } from './hooks/use-integration-primary-modal'; | ||
import { useFetchIntegrations } from '@/hooks/use-fetch-integrations'; | ||
import { buildRoute, ROUTES } from '../../../utils/routes'; | ||
import { showSuccessToast } from '../../../components/primitives/sonner-helpers'; | ||
|
||
export type CreateIntegrationSidebarProps = { | ||
isOpened: boolean; | ||
}; | ||
|
||
export function CreateIntegrationSidebar({ isOpened }: CreateIntegrationSidebarProps) { | ||
const navigate = useNavigate(); | ||
const { providerId } = useParams(); | ||
|
||
const providers = novuProviders; | ||
const { mutateAsync: createIntegration, isPending } = useCreateIntegration(); | ||
const { mutateAsync: setPrimaryIntegration, isPending: isSettingPrimary } = useSetPrimaryIntegration(); | ||
const { integrations } = useFetchIntegrations(); | ||
|
||
const handleIntegrationSelect = (integrationId: string) => { | ||
navigate(buildRoute(ROUTES.INTEGRATIONS_CONNECT_PROVIDER, { providerId: integrationId }), { replace: true }); | ||
}; | ||
|
||
const handleBack = () => { | ||
navigate(ROUTES.INTEGRATIONS_CONNECT, { replace: true }); | ||
}; | ||
|
||
const { selectedIntegration, step, searchQuery, onIntegrationSelect, onBack } = useSidebarNavigationManager({ | ||
isOpened, | ||
initialProviderId: providerId, | ||
onIntegrationSelect: handleIntegrationSelect, | ||
onBack: handleBack, | ||
}); | ||
|
||
const { integrationsByChannel } = useIntegrationList(searchQuery); | ||
const provider = providers?.find((p) => p.id === (selectedIntegration || providerId)); | ||
const { | ||
isPrimaryModalOpen, | ||
setIsPrimaryModalOpen, | ||
pendingData, | ||
handleSubmitWithPrimaryCheck, | ||
handlePrimaryConfirm, | ||
existingPrimaryIntegration, | ||
isChannelSupportPrimary, | ||
} = useIntegrationPrimaryModal({ | ||
onSubmit: handleCreateIntegration, | ||
integrations, | ||
channel: provider?.channel, | ||
mode: 'create', | ||
}); | ||
|
||
async function handleCreateIntegration(data: IntegrationFormData) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Couldn't find a quick one for this, I don't think it's adds a lot value for the amount of work. Will skip it for now |
||
if (!provider) return; | ||
|
||
try { | ||
const integration = await createIntegration({ | ||
providerId: provider.id, | ||
channel: provider.channel, | ||
credentials: data.credentials, | ||
name: data.name, | ||
identifier: data.identifier, | ||
active: data.active, | ||
_environmentId: data.environmentId, | ||
}); | ||
|
||
if (data.primary && isChannelSupportPrimary && data.active) { | ||
await setPrimaryIntegration({ integrationId: integration.data._id }); | ||
} | ||
|
||
showSuccessToast('Integration created successfully'); | ||
|
||
navigate(ROUTES.INTEGRATIONS); | ||
} catch (error: unknown) { | ||
handleIntegrationError(error, 'create'); | ||
} | ||
} | ||
|
||
const handleClose = () => { | ||
navigate(ROUTES.INTEGRATIONS); | ||
}; | ||
|
||
return ( | ||
<> | ||
<IntegrationSheet | ||
isOpened={isOpened} | ||
onClose={handleClose} | ||
provider={provider} | ||
mode="create" | ||
step={step} | ||
onBack={onBack} | ||
> | ||
{step === 'select' ? ( | ||
<div className="scrollbar-custom flex-1 overflow-y-auto"> | ||
<ChannelTabs | ||
integrationsByChannel={integrationsByChannel} | ||
searchQuery={searchQuery} | ||
onIntegrationSelect={onIntegrationSelect} | ||
/> | ||
</div> | ||
) : provider ? ( | ||
<> | ||
<div className="scrollbar-custom flex-1 overflow-y-auto"> | ||
<IntegrationConfiguration | ||
isChannelSupportPrimary={isChannelSupportPrimary} | ||
provider={provider} | ||
onSubmit={handleSubmitWithPrimaryCheck} | ||
mode="create" | ||
/> | ||
</div> | ||
<div className="bg-background flex justify-end gap-2 border-t p-3"> | ||
<Button | ||
type="submit" | ||
form="integration-configuration-form" | ||
isLoading={isPending || isSettingPrimary} | ||
size="sm" | ||
> | ||
Create Integration | ||
</Button> | ||
</div> | ||
</> | ||
) : null} | ||
</IntegrationSheet> | ||
|
||
<SelectPrimaryIntegrationModal | ||
isOpen={isPrimaryModalOpen} | ||
onOpenChange={setIsPrimaryModalOpen} | ||
onConfirm={handlePrimaryConfirm} | ||
currentPrimaryName={existingPrimaryIntegration?.name} | ||
newPrimaryName={pendingData?.name ?? ''} | ||
isLoading={isPending || isSettingPrimary} | ||
/> | ||
</> | ||
); | ||
} |
84 changes: 84 additions & 0 deletions
84
apps/dashboard/src/components/integrations/components/hooks/use-integration-list.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { useMemo } from 'react'; | ||
import { ChannelTypeEnum, ChatProviderIdEnum, IProviderConfig, PushProviderIdEnum } from '@novu/shared'; | ||
import { providers, EmailProviderIdEnum, SmsProviderIdEnum } from '@novu/shared'; | ||
import { ProvidersIdEnum } from '@novu/shared'; | ||
|
||
export function useIntegrationList(searchQuery: string = '') { | ||
const filteredIntegrations = useMemo(() => { | ||
if (!providers) return []; | ||
|
||
const filtered = providers.filter( | ||
(provider: IProviderConfig) => | ||
provider.displayName.toLowerCase().includes(searchQuery.toLowerCase()) && | ||
provider.id !== EmailProviderIdEnum.Novu && | ||
provider.id !== SmsProviderIdEnum.Novu | ||
); | ||
|
||
const popularityOrder: Record<ChannelTypeEnum, ProvidersIdEnum[]> = { | ||
[ChannelTypeEnum.EMAIL]: [ | ||
EmailProviderIdEnum.SendGrid, | ||
EmailProviderIdEnum.Mailgun, | ||
EmailProviderIdEnum.Postmark, | ||
EmailProviderIdEnum.Mailjet, | ||
EmailProviderIdEnum.Mandrill, | ||
EmailProviderIdEnum.SES, | ||
EmailProviderIdEnum.Outlook365, | ||
EmailProviderIdEnum.CustomSMTP, | ||
], | ||
[ChannelTypeEnum.SMS]: [ | ||
SmsProviderIdEnum.Twilio, | ||
SmsProviderIdEnum.Plivo, | ||
SmsProviderIdEnum.SNS, | ||
SmsProviderIdEnum.Nexmo, | ||
SmsProviderIdEnum.Telnyx, | ||
SmsProviderIdEnum.Sms77, | ||
SmsProviderIdEnum.Infobip, | ||
SmsProviderIdEnum.Gupshup, | ||
], | ||
[ChannelTypeEnum.PUSH]: [ | ||
PushProviderIdEnum.FCM, | ||
PushProviderIdEnum.EXPO, | ||
PushProviderIdEnum.APNS, | ||
PushProviderIdEnum.OneSignal, | ||
], | ||
[ChannelTypeEnum.CHAT]: [ | ||
ChatProviderIdEnum.Slack, | ||
ChatProviderIdEnum.Discord, | ||
ChatProviderIdEnum.MsTeams, | ||
ChatProviderIdEnum.Mattermost, | ||
], | ||
[ChannelTypeEnum.IN_APP]: [], | ||
}; | ||
|
||
return filtered.sort((a, b) => { | ||
const channelOrder = popularityOrder[a.channel] || []; | ||
const indexA = channelOrder.indexOf(a.id); | ||
const indexB = channelOrder.indexOf(b.id); | ||
|
||
if (indexA !== -1 && indexB !== -1) { | ||
return indexA - indexB; | ||
} | ||
|
||
if (indexA !== -1) return -1; | ||
if (indexB !== -1) return 1; | ||
|
||
return 0; | ||
}); | ||
}, [providers, searchQuery]); | ||
|
||
const integrationsByChannel = useMemo(() => { | ||
return Object.values(ChannelTypeEnum).reduce( | ||
(acc, channel) => { | ||
acc[channel] = filteredIntegrations.filter((provider: IProviderConfig) => provider.channel === channel); | ||
|
||
return acc; | ||
}, | ||
{} as Record<ChannelTypeEnum, IProviderConfig[]> | ||
); | ||
}, [filteredIntegrations]); | ||
|
||
return { | ||
filteredIntegrations, | ||
integrationsByChannel, | ||
}; | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
something weird is happening when clicking on connect integration
Screen.Recording.2024-12-20.at.09.45.53.mov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not able to reproduce, might be some local thing going on. I will deploy this to staging and will see if it's still noticable