Skip to content
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): Filters for activity feed #7255

Merged
merged 37 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
0481546
feat: add filters
scopsy Dec 9, 2024
f70c0da
fix: date search
scopsy Dec 9, 2024
eeedb91
default search
scopsy Dec 9, 2024
274808c
fix: refactor use search params
scopsy Dec 9, 2024
273ad54
fix: pick
scopsy Dec 9, 2024
009fc8d
feat: wip
scopsy Dec 9, 2024
9143683
fix: add params
scopsy Dec 9, 2024
c1a9e3a
feat: add selection
scopsy Dec 9, 2024
4e37cb9
fix: multi for workflows
scopsy Dec 9, 2024
daec718
feat: remove popover
scopsy Dec 10, 2024
e66f7b5
refactor component
scopsy Dec 10, 2024
9906bf3
fix: reorder components
scopsy Dec 10, 2024
fe47dbf
Update activity-table.tsx
scopsy Dec 10, 2024
f2a62df
fix: styling
scopsy Dec 10, 2024
776c853
feat: refactor sub components
scopsy Dec 10, 2024
66c59a1
fix: styling
scopsy Dec 10, 2024
03f819a
fix: styling
scopsy Dec 10, 2024
4fe3e5d
fix: design of cards and activity filters
scopsy Dec 10, 2024
091aa31
fix: refactor watch state
scopsy Dec 10, 2024
e245bdd
fix: imports
scopsy Dec 10, 2024
13c6b27
fix: remove unused
scopsy Dec 10, 2024
ea75779
Merge branch 'new-activity-feed-page' into v2-filters-test-playground
scopsy Dec 10, 2024
290a62b
feat: empty state
scopsy Dec 10, 2024
f0cea85
fix: flicker
scopsy Dec 10, 2024
bb1a3f5
Update activity-empty-state.tsx
scopsy Dec 10, 2024
d047bb7
Update activity-empty-state.tsx
scopsy Dec 10, 2024
580cf07
Merge branch 'new-activity-feed-page' into v2-filters-test-playground
scopsy Dec 10, 2024
474181a
fix: merge
scopsy Dec 10, 2024
223446e
Merge branch 'new-activity-feed-page' into v2-filters-test-playground
scopsy Dec 12, 2024
eb9b201
fix: merge
scopsy Dec 12, 2024
0566b89
fix: layout
scopsy Dec 12, 2024
cd05b65
fix: pr comments
scopsy Dec 12, 2024
3c716bf
fix: filternames
scopsy Dec 12, 2024
06057e7
fix: copy
scopsy Dec 12, 2024
a57232d
fix: pr fixes
scopsy Dec 12, 2024
70551d7
fix: range clicking
scopsy Dec 12, 2024
3f62c06
fix: pr comments
scopsy Dec 12, 2024
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
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,8 @@
"hsforms",
"touchpoint",
"Angularjs",
"navigatable"
"navigatable",
"facated"
],
"flagWords": [],
"patterns": [
Expand Down
15 changes: 15 additions & 0 deletions apps/api/src/app/notifications/dtos/activities-request.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { ChannelTypeEnum } from '@novu/shared';
import { IsDateString, IsOptional } from 'class-validator';

export class ActivitiesRequestDto {
@ApiPropertyOptional({
Expand Down Expand Up @@ -43,4 +44,18 @@ export class ActivitiesRequestDto {
required: false,
})
transactionId?: string;

@ApiPropertyOptional({
type: String,
required: false,
})
@IsOptional()
startDate?: string;

@ApiPropertyOptional({
type: String,
required: false,
})
@IsOptional()
endDate?: string;
}
2 changes: 2 additions & 0 deletions apps/api/src/app/notifications/notification.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export class NotificationsController {
search: query.search,
subscriberIds: subscribersQuery,
transactionId: query.transactionId,
startDate: query.startDate,
endDate: query.endDate,
})
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,12 @@ export class GetActivityFeedCommand extends EnvironmentWithUserCommand {
@IsOptional()
@IsString()
transactionId?: string;

@IsOptional()
@IsString()
startDate?: string;

@IsOptional()
@IsString()
endDate?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@ export class GetActivityFeed {
private async getFeedNotifications(command: GetActivityFeedCommand, subscriberIds: string[], LIMIT: number) {
const { data: notifications } = await this.notificationRepository.getFeed(
command.environmentId,
{ channels: command.channels, templates: command.templates, subscriberIds, transactionId: command.transactionId },
{
channels: command.channels,
templates: command.templates,
subscriberIds,
transactionId: command.transactionId,
startDate: command.startDate,
endDate: command.endDate,
},
command.page * LIMIT,
LIMIT
);
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
Expand Down
25 changes: 22 additions & 3 deletions apps/dashboard/src/api/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export interface IActivityFilters {
email?: string;
subscriberId?: string;
transactionId?: string;
startDate?: string;
endDate?: string;
}

interface ActivityResponse {
Expand All @@ -18,29 +20,46 @@ interface ActivityResponse {
export function getActivityList(
environment: IEnvironment,
page = 0,
filters?: IActivityFilters
filters?: IActivityFilters,
signal?: AbortSignal
): Promise<ActivityResponse> {
const searchParams = new URLSearchParams();
searchParams.append('page', page.toString());

if (filters?.channels?.length) {
searchParams.append('channels', filters.channels.join(','));
filters.channels.forEach((channel) => {
searchParams.append('channels', channel);
});
}

if (filters?.workflows?.length) {
searchParams.append('templates', filters.workflows.join(','));
filters.workflows.forEach((workflow) => {
searchParams.append('templates', workflow);
});
}

if (filters?.email) {
searchParams.append('emails', filters.email);
}

if (filters?.subscriberId) {
searchParams.append('subscriberIds', filters.subscriberId);
}

if (filters?.transactionId) {
searchParams.append('transactionId', filters.transactionId);
}

if (filters?.startDate) {
searchParams.append('startDate', filters.startDate);
}
if (filters?.endDate) {
searchParams.append('endDate', filters.endDate);
}

return get<ActivityResponse>(`/notifications?${searchParams.toString()}`, {
environment,
signal,
});
}

Expand Down
22 changes: 12 additions & 10 deletions apps/dashboard/src/api/api.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ const request = async <T>(
method?: HttpMethod;
headers?: HeadersInit;
version?: 'v1' | 'v2';
signal?: AbortSignal;
}
): Promise<T> => {
const { body, environment, headers, method = 'GET', version = 'v1' } = options || {};
const { body, environment, headers, method = 'GET', version = 'v1', signal } = options || {};
try {
const jwt = await getToken();
const config: RequestInit = {
Expand All @@ -35,6 +36,7 @@ const request = async <T>(
...(environment && { 'Novu-Environment-Id': environment._id }),
...headers,
},
signal,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added support for aborting requests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using the signal in this PR?

};

if (body) {
Expand Down Expand Up @@ -65,26 +67,26 @@ const request = async <T>(
}
};

type RequestOptions = { body?: unknown; environment?: IEnvironment };
type RequestOptions = { body?: unknown; environment?: IEnvironment; signal?: AbortSignal };

export const get = <T>(endpoint: string, { environment }: RequestOptions = {}) =>
request<T>(endpoint, { method: 'GET', environment });
export const get = <T>(endpoint: string, { environment, signal }: RequestOptions = {}) =>
request<T>(endpoint, { method: 'GET', environment, signal });
export const post = <T>(endpoint: string, options: RequestOptions) =>
request<T>(endpoint, { method: 'POST', ...options });
export const put = <T>(endpoint: string, options: RequestOptions) =>
request<T>(endpoint, { method: 'PUT', ...options });
export const del = <T>(endpoint: string, { environment }: RequestOptions = {}) =>
request<T>(endpoint, { method: 'DELETE', environment });
export const del = <T>(endpoint: string, { environment, signal }: RequestOptions = {}) =>
request<T>(endpoint, { method: 'DELETE', environment, signal });
export const patch = <T>(endpoint: string, options: RequestOptions) =>
request<T>(endpoint, { method: 'PATCH', ...options });

export const getV2 = <T>(endpoint: string, { environment }: RequestOptions = {}) =>
request<T>(endpoint, { version: 'v2', method: 'GET', environment });
export const getV2 = <T>(endpoint: string, { environment, signal }: RequestOptions = {}) =>
request<T>(endpoint, { version: 'v2', method: 'GET', environment, signal });
export const postV2 = <T>(endpoint: string, options: RequestOptions) =>
request<T>(endpoint, { version: 'v2', method: 'POST', ...options });
export const putV2 = <T>(endpoint: string, options: RequestOptions) =>
request<T>(endpoint, { version: 'v2', method: 'PUT', ...options });
export const delV2 = <T>(endpoint: string, { environment }: RequestOptions = {}) =>
request<T>(endpoint, { version: 'v2', method: 'DELETE', environment });
export const delV2 = <T>(endpoint: string, { environment, signal }: RequestOptions = {}) =>
request<T>(endpoint, { version: 'v2', method: 'DELETE', environment, signal });
export const patchV2 = <T>(endpoint: string, options: RequestOptions) =>
request<T>(endpoint, { version: 'v2', method: 'PATCH', ...options });
174 changes: 174 additions & 0 deletions apps/dashboard/src/components/activity/activity-empty-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { Button } from '@/components/primitives/button';
import { cn } from '@/utils/ui';
import { PlayCircleIcon } from 'lucide-react';
import { RiCloseCircleLine } from 'react-icons/ri';
import { motion, AnimatePresence } from 'motion/react';
import { ExternalLink } from '../shared/external-link';
import { useNavigate } from 'react-router-dom';
import { buildRoute, ROUTES } from '@/utils/routes';
import { useEnvironment } from '@/context/environment/hooks';

interface ActivityEmptyStateProps {
className?: string;
emptySearchResults?: boolean;
onClearFilters?: () => void;
}

export function ActivityEmptyState({ className, emptySearchResults, onClearFilters }: ActivityEmptyStateProps) {
const navigate = useNavigate();
const { currentEnvironment } = useEnvironment();

const handleNavigateToWorkflows = () => {
navigate(buildRoute(ROUTES.WORKFLOWS, { environmentSlug: currentEnvironment?.slug ?? '' }));
};

return (
<AnimatePresence mode="wait">
<motion.div
key="empty-state"
className={cn('flex h-full w-full items-center justify-center', className)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
duration: 0.15,
ease: [0.4, 0, 0.2, 1],
}}
>
<motion.div
initial={{ opacity: 0, scale: 0.98, y: 5 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.98, y: 5 }}
transition={{
duration: 0.25,
delay: 0.1,
ease: [0.4, 0, 0.2, 1],
}}
className="flex flex-col items-center gap-6"
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
duration: 0.2,
delay: 0.2,
}}
className="relative"
>
<ActivityIllustration />
</motion.div>

<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.2,
delay: 0.25,
}}
className="flex flex-col items-center gap-1 text-center"
>
<h2 className="text-foreground-900 text-lg font-medium">
{emptySearchResults ? 'No activity match that filter' : 'No activity in the past 30 days'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not mistaken, the past 30 days should either be past 30 or past 90, depending on the plan. Or we can just rephrase it to No activity in the selected interval.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any change to the default interval will result in the emptySearchResults this message only target the default state result. And mostly targeting new signups

</h2>
<p className="text-foreground-600 max-w-md text-sm font-normal">
{emptySearchResults
? 'Try changing your filter to see more activity or trigger notifications that match your search criteria.'
: "Your activity feed is empty. Once events start appearing, you'll be able to track notifications, troubleshoot issues, and view delivery details."}
</p>
</motion.div>

{emptySearchResults && onClearFilters && (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.2,
delay: 0.3,
}}
className="flex gap-6"
>
<Button variant="outline" className="gap-2" onClick={onClearFilters}>
<RiCloseCircleLine className="h-4 w-4" />
Clear Filters
</Button>
</motion.div>
)}

{!emptySearchResults && (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.2,
delay: 0.3,
}}
className="flex gap-6"
>
<ExternalLink href="https://docs.novu.co" variant="documentation" target="_blank">
View Docs
</ExternalLink>
<Button variant="primary" className="gap-2" onClick={handleNavigateToWorkflows}>
<PlayCircleIcon className="h-4 w-4" />
Trigger Workflow
</Button>
</motion.div>
)}
</motion.div>
</motion.div>
</AnimatePresence>
);
}

function ActivityIllustration() {
return (
<svg width="137" height="126" viewBox="0 0 137 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="135" height="45" rx="7.5" stroke="#CACFD8" strokeDasharray="5 3" />
<rect x="5" y="5" width="127" height="37" rx="5.5" fill="white" />
<rect x="5" y="5" width="127" height="37" rx="5.5" stroke="#F2F5F8" />
<path
d="M68.5 29.5C65.1862 29.5 62.5 26.8138 62.5 23.5C62.5 20.1862 65.1862 17.5 68.5 17.5C71.8138 17.5 74.5 20.1862 74.5 23.5C74.5 26.8138 71.8138 29.5 68.5 29.5ZM68.5 28.3C69.773 28.3 70.9939 27.7943 71.8941 26.8941C72.7943 25.9939 73.3 24.773 73.3 23.5C73.3 22.227 72.7943 21.0061 71.8941 20.1059C70.9939 19.2057 69.773 18.7 68.5 18.7C67.227 18.7 66.0061 19.2057 65.1059 20.1059C64.2057 21.0061 63.7 22.227 63.7 23.5C63.7 24.773 64.2057 25.9939 65.1059 26.8941C66.0061 27.7943 67.227 28.3 68.5 28.3ZM67.6732 21.349L70.6006 23.3002C70.6335 23.3221 70.6605 23.3518 70.6792 23.3867C70.6979 23.4215 70.7076 23.4605 70.7076 23.5C70.7076 23.5395 70.6979 23.5785 70.6792 23.6133C70.6605 23.6482 70.6335 23.6779 70.6006 23.6998L67.6726 25.651C67.6365 25.6749 67.5946 25.6886 67.5513 25.6907C67.5081 25.6927 67.465 25.683 67.4268 25.6626C67.3886 25.6422 67.3567 25.6118 67.3344 25.5747C67.312 25.5376 67.3002 25.4951 67.3 25.4518V21.5482C67.3001 21.5048 67.3119 21.4622 67.3343 21.425C67.3567 21.3878 67.3887 21.3574 67.427 21.3369C67.4653 21.3165 67.5084 21.3068 67.5518 21.3089C67.5951 21.3111 67.6371 21.3249 67.6732 21.349Z"
fill="#CACFD8"
/>
<rect x="1" y="80" width="135" height="45" rx="7.5" stroke="#CACFD8" />
<rect x="5" y="84" width="127" height="37" rx="5.5" fill="white" />
<rect x="5" y="84" width="127" height="37" rx="5.5" stroke="#F2F5F8" />
<path
d="M16.5 98.5C16.5 95.1863 19.1863 92.5 22.5 92.5H30.5C33.8137 92.5 36.5 95.1863 36.5 98.5V106.5C36.5 109.814 33.8137 112.5 30.5 112.5H22.5C19.1863 112.5 16.5 109.814 16.5 106.5V98.5Z"
fill="#FBFBFB"
/>
<path
d="M26.4996 97.3572C26.144 97.3572 25.8568 97.6445 25.8568 98V98.3857C24.3902 98.6831 23.2853 99.9808 23.2853 101.536V101.913C23.2853 102.858 22.9378 103.77 22.311 104.477L22.1623 104.644C21.9936 104.832 21.9534 105.104 22.0559 105.335C22.1583 105.566 22.3893 105.714 22.6425 105.714H30.3568C30.6099 105.714 30.8389 105.566 30.9434 105.335C31.0478 105.104 31.0056 104.832 30.8369 104.644L30.6882 104.477C30.0614 103.77 29.7139 102.86 29.7139 101.913V101.536C29.7139 99.9808 28.609 98.6831 27.1425 98.3857V98C27.1425 97.6445 26.8552 97.3572 26.4996 97.3572ZM27.4097 107.267C27.6507 107.026 27.7853 106.699 27.7853 106.357H26.4996H25.2139C25.2139 106.699 25.3485 107.026 25.5896 107.267C25.8306 107.508 26.1581 107.643 26.4996 107.643C26.8411 107.643 27.1686 107.508 27.4097 107.267Z"
fill="#E1E4EA"
/>
<rect x="44.5" y="96.5" width="44" height="5" rx="2.5" fill="url(#paint0_linear_7279_27982)" />
<rect x="44.5" y="103.5" width="77" height="5" rx="2.5" fill="url(#paint1_linear_7279_27982)" />
<path d="M68.5 76.625V49.375" stroke="#E1E4EA" strokeWidth="0.75" strokeLinejoin="bevel" />
<defs>
<linearGradient
id="paint0_linear_7279_27982"
x1="33.8626"
y1="98.6257"
x2="95.511"
y2="98.6257"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#F1EFEF" />
<stop offset="0.48" stopColor="#F9F8F8" />
<stop offset="0.992158" stopColor="#F9F8F8" stopOpacity="0.75" />
</linearGradient>
<linearGradient
id="paint1_linear_7279_27982"
x1="25.8846"
y1="105.626"
x2="133.769"
y2="105.626"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#F1EFEF" />
<stop offset="0.48" stopColor="#F9F8F8" />
<stop offset="0.992158" stopColor="#F9F8F8" stopOpacity="0.75" />
</linearGradient>
</defs>
</svg>
);
}
Loading
Loading