Skip to content

Add new 'Add Channel' page for Notifications #403

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
10 changes: 10 additions & 0 deletions api/partner.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ export const getPartnerConnectionListeners = async (connectionId) => {
const response = await axiosAdmin.get(`/partner/connection/${connectionId}/listeners`)
return response.data
}

export const createPartnerConnection = async (data) => {
const response = await axiosAdmin.post('/partner/connections', data)
return response.data
}

export const deletePartnerConnection = async (connectionId) => {
const response = await axiosAdmin.delete(`/partner/connection/${connectionId}`)
return response.data
}
4 changes: 3 additions & 1 deletion components/Admin/BillingCountry.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export default function BillingCountry({ billingCountry, setBillingCountry, choo
{billingCountry && (
<>
Your billing country is{' '}
<a onClick={() => setChoosingCountry(true)}>{countries?.getNameTranslated?.(billingCountry) || billingCountry}</a>
<a onClick={() => setChoosingCountry(true)}>
{countries?.getNameTranslated?.(billingCountry) || billingCountry}
</a>
</>
)}
</>
Expand Down
13 changes: 13 additions & 0 deletions components/Admin/notifications/AddChannelButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Link from 'next/link'
import { FaPlus } from 'react-icons/fa'

export default function AddChannelButton() {
return (
<Link href="/admin/notifications/add-channel" className="btn btn-primary">
<button className="btn btn-primary">
<FaPlus />
Add channel
</button>
</Link>
)
}
3 changes: 3 additions & 0 deletions components/Admin/notifications/AddRuleButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function AddRuleButton() {
return <button className="btn btn-primary">Add rule</button>
}
104 changes: 0 additions & 104 deletions components/Admin/notifications/ChannelCard.js

This file was deleted.

56 changes: 56 additions & 0 deletions components/Admin/notifications/ChannelCard/ChannelCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useState } from 'react'

import { FaDiscord, FaEnvelope, FaSlack } from 'react-icons/fa'
import { FaXTwitter } from 'react-icons/fa6'

import Card from '@/components/UI/Card'
import { useDeleteNotificationChannel } from '@/hooks/useNotifications'
import { NOTIFICATION_CHANNEL_TYPES } from '@/lib/notificationChannels'

import ChannelDeleteDialog from './ChannelDeleteDialog'
import ChannelSpecificDetails from './ChannelSpecificDetails'

const iconMap = {
[NOTIFICATION_CHANNEL_TYPES.SLACK]: FaSlack,
[NOTIFICATION_CHANNEL_TYPES.DISCORD]: FaDiscord,
[NOTIFICATION_CHANNEL_TYPES.TWITTER]: FaXTwitter,
[NOTIFICATION_CHANNEL_TYPES.EMAIL]: FaEnvelope
}

export default function ChannelCard({ channel }) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const deleteChannel = useDeleteNotificationChannel()

const handleDelete = async () => {
await deleteChannel.mutate(channel.id)
setIsDeleteDialogOpen(false)
}

return (
<Card className="flex flex-col justify-between">
<div className="font-bold text-lg mb-2 capitalize flex items-center gap-2 w-full">
{channel.type &&
iconMap[channel.type] &&
React.createElement(iconMap[channel.type], {
className: 'inline-block w-4 h-4 text-gray-600 dark:text-gray-400'
})}{' '}
{channel.name || `Channel #${channel.id}`}
</div>

<ChannelSpecificDetails channel={channel} />
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2 w-full">
Used in {channel.rules.length} {channel.rules.length === 1 ? 'rule' : 'rules'}
</div>
<div className="flex justify-start">
<button className="btn btn-error" onClick={() => setIsDeleteDialogOpen(true)}>
Delete
</button>
</div>
<ChannelDeleteDialog
isOpen={isDeleteDialogOpen}
onClose={() => setIsDeleteDialogOpen(false)}
onDelete={handleDelete}
/>
</Card>
)
}
32 changes: 32 additions & 0 deletions components/Admin/notifications/ChannelCard/ChannelDeleteDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Dialog from '@/components/UI/Dialog'

export default function ChannelDeleteDialog({ isOpen, onClose, onDelete }) {
return (
<Dialog
isOpen={isOpen}
onClose={onClose}
title="Delete channel"
size="small"
>
<div className="text-gray-600 dark:text-gray-400 mb-6">
Are you sure you want to delete this channel? This action cannot be undone.
</div>
<div className="flex justify-end gap-3">
<button
className="btn btn-secondary"
onClick={onClose}
type="button"
>
Cancel
</button>
<button
className="btn btn-error"
onClick={onDelete}
type="button"
>
Delete
</button>
</div>
</Dialog>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NOTIFICATION_CHANNEL_TYPES } from '@/lib/notificationChannels'

export default function ChannelSpecificDetails({ channel }) {
if (!channel.settings) {
return null
}
switch (channel.type) {
case NOTIFICATION_CHANNEL_TYPES.SLACK:
return (
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2 flex items-center gap-1 w-full">
<span className="inline-block text-xs font-bold truncate max-w-xs">{channel.settings.webhook || 'N/A'}</span>
</div>
)
case NOTIFICATION_CHANNEL_TYPES.DISCORD:
return (
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2 w-full">
<div className="flex items-center gap-1">
<span className="inline-block text-xs font-bold truncate max-w-xs">
{channel.settings.webhook || 'N/A'}
</span>
</div>
<div className="flex items-center gap-1">
Username:{' '}
<span className="inline-block text-xs font-bold truncate max-w-xs">
{channel.settings.username || 'N/A'}
</span>
</div>
</div>
)
case NOTIFICATION_CHANNEL_TYPES.EMAIL:
return (
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
<div className="flex items-center gap-1">
Webhook:{' '}
<span className="inline-block text-xs font-bold truncate max-w-xs">
{channel.settings.webhook || 'N/A'}
</span>
</div>
</div>
)
case NOTIFICATION_CHANNEL_TYPES.TWITTER:
return (
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2 w-full">
<div className="flex items-center gap-1">
Consumer key:{' '}
<span className="inline-block text-xs font-mono truncate max-w-xs">
{channel.settings.consumer_key ? channel.settings.consumer_key.slice(-8) : 'N/A'}
</span>
</div>
<div className="flex items-center gap-1">
Consumer secret:{' '}
<span className="inline-block text-xs font-mono truncate max-w-xs">
{channel.settings.consumer_secret ? channel.settings.consumer_secret.slice(-8) : 'N/A'}
</span>
</div>
<div className="flex items-center gap-1">
Access token key:{' '}
<span className="inline-block text-xs font-mono truncate max-w-xs">
{channel.settings.access_token_key ? channel.settings.access_token_key.slice(-8) : 'N/A'}
</span>
</div>
<div className="flex items-center gap-1">
<span>Access token secret:</span>
<span className="inline-block text-xs font-mono truncate max-w-xs">
{channel.settings.access_token_secret ? channel.settings.access_token_secret.slice(-8) : 'N/A'}
</span>
</div>
</div>
)
default:
return null
}
}
1 change: 1 addition & 0 deletions components/Admin/notifications/ChannelCard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ChannelCard'
30 changes: 12 additions & 18 deletions components/Admin/notifications/EmptyState.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import { useTranslation } from 'next-i18next'
import Image from 'next/image'

export default function EmptyState() {
const { t } = useTranslation('admin')

return (
<div className="flex flex-col items-center justify-center py-12 px-4">
<div className="w-48 h-48 relative mb-6">
<Image
src="/images/empty-state.svg"
alt="No notifications"
layout="fill"
objectFit="contain"
/>
</div>
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">
{t('notifications.empty.title', 'No notification rules or channels yet')}
</h2>
export default function EmptyState({ action, title, description, showImage = false }) {
return (
<div className="flex flex-col items-center justify-center py-12 px-4">
{showImage && (
<div className="w-48 h-48 relative mb-6">
<Image src="/images/empty-state.svg" alt="No notifications" layout="fill" objectFit="contain" />
</div>
)
)}
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">{title}</h2>
<p className="text-gray-600 dark:text-gray-400">{description}</p>
{action}
</div>
)
}
30 changes: 15 additions & 15 deletions components/Admin/notifications/ErrorState.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ import { useTranslation } from 'next-i18next'
import Image from 'next/image'

export default function ErrorState() {
const { t } = useTranslation('admin')
const { t } = useTranslation('admin')

return (
<div className="flex flex-col items-center justify-center py-12 px-4">
<div className="w-48 h-48 relative mb-6">
<Image src="/images/error-state.svg" alt="Error" layout="fill" objectFit="contain" />
</div>
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">
{t('notifications.error.title', 'Error loading notifications')}
</h2>
<p className="text-gray-600 dark:text-gray-400 text-center max-w-md mb-6">
{t('notifications.error.description', 'Please try again later.')}
</p>
</div>
)
}
return (
<div className="flex flex-col items-center justify-center py-12 px-4">
<div className="w-48 h-48 relative mb-6">
<Image src="/images/error-state.svg" alt="Error" layout="fill" objectFit="contain" />
</div>
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">
{t('notifications.error.title', 'Error loading notifications')}
</h2>
<p className="text-gray-600 dark:text-gray-400 text-center max-w-md mb-6">
{t('notifications.error.description', 'Please try again later.')}
</p>
</div>
)
}
Loading