diff --git a/web/packages/design/src/theme/themes/bblpTheme.ts b/web/packages/design/src/theme/themes/bblpTheme.ts
index 0ead5373bcb06..4c40be56e23b9 100644
--- a/web/packages/design/src/theme/themes/bblpTheme.ts
+++ b/web/packages/design/src/theme/themes/bblpTheme.ts
@@ -102,6 +102,22 @@ const colors: ThemeColors = {
'rgba(0, 162, 35, 0.18)',
'rgba(0, 162, 35, 0.25)',
],
+ // TODO rudream: update bblp interactive tonal colors.
+ danger: [
+ 'rgba(255, 98, 87, 0.1)',
+ 'rgba(255, 98, 87, 0.18)',
+ 'rgba(255, 98, 87, 0.25)',
+ ],
+ alert: [
+ 'rgba(255, 171, 0, 0.1)',
+ 'rgba(255, 171, 0, 0.18)',
+ 'rgba(255, 171, 0, 0.25)',
+ ],
+ informational: [
+ 'rgba(0, 158, 255, 0.1)',
+ 'rgba(0, 158, 255, 0.18)',
+ 'rgba(0, 158, 255, 0.25)',
+ ],
neutral: neutralColors,
},
},
@@ -176,6 +192,13 @@ const colors: ThemeColors = {
active: '#D64D22',
},
+ // TODO rudream: update bblp accent colors.
+ accent: {
+ main: 'rgba(0, 158, 255, 1)',
+ hover: 'rgba(51, 177, 255, 1)',
+ active: 'rgba(102, 197, 255, 1)',
+ },
+
notice: {
background: '#282828', // elevated
},
diff --git a/web/packages/design/src/theme/themes/darkTheme.ts b/web/packages/design/src/theme/themes/darkTheme.ts
index 50ca78cc55c92..4ef002ce0c008 100644
--- a/web/packages/design/src/theme/themes/darkTheme.ts
+++ b/web/packages/design/src/theme/themes/darkTheme.ts
@@ -102,6 +102,21 @@ const colors: ThemeColors = {
'rgba(0, 191, 166, 0.18)',
'rgba(0, 191, 166, 0.25)',
],
+ danger: [
+ 'rgba(255, 98, 87, 0.1)',
+ 'rgba(255, 98, 87, 0.18)',
+ 'rgba(255, 98, 87, 0.25)',
+ ],
+ alert: [
+ 'rgba(255, 171, 0, 0.1)',
+ 'rgba(255, 171, 0, 0.18)',
+ 'rgba(255, 171, 0, 0.25)',
+ ],
+ informational: [
+ 'rgba(0, 158, 255, 0.1)',
+ 'rgba(0, 158, 255, 0.18)',
+ 'rgba(0, 158, 255, 0.25)',
+ ],
neutral: neutralColors,
},
},
@@ -182,6 +197,12 @@ const colors: ThemeColors = {
active: '#FFCD66',
},
+ accent: {
+ main: 'rgba(0, 158, 255, 1)',
+ hover: 'rgba(51, 177, 255, 1)',
+ active: 'rgba(102, 197, 255, 1)',
+ },
+
notice: {
background: '#344179', // elevated
},
diff --git a/web/packages/design/src/theme/themes/lightTheme.ts b/web/packages/design/src/theme/themes/lightTheme.ts
index bd5173f6f81c1..aad4e680aae51 100644
--- a/web/packages/design/src/theme/themes/lightTheme.ts
+++ b/web/packages/design/src/theme/themes/lightTheme.ts
@@ -101,6 +101,21 @@ const colors: ThemeColors = {
'rgba(0, 125, 107, 0.18)',
'rgba(0, 125, 107, 0.25)',
],
+ danger: [
+ 'rgba(204, 55, 45, 0.1)',
+ 'rgba(204, 55, 45, 0.18)',
+ 'rgba(204, 55, 45, 0.25)',
+ ],
+ alert: [
+ 'rgba(255, 171, 0, 0.1)',
+ 'rgba(255, 171, 0, 0.18)',
+ 'rgba(255, 171, 0, 0.25)',
+ ],
+ informational: [
+ 'rgba(0, 115, 186, 0.1)',
+ 'rgba(0, 115, 186, 0.18)',
+ 'rgba(0, 115, 186, 0.25)',
+ ],
neutral: neutralColors,
},
},
@@ -181,6 +196,12 @@ const colors: ThemeColors = {
active: '#996700',
},
+ accent: {
+ main: 'rgba(0, 115, 186, 1)',
+ hover: 'rgba(0, 92, 149, 1)',
+ active: 'rgba(0, 69, 112, 1)',
+ },
+
notice: {
background: blue[50],
},
diff --git a/web/packages/design/src/theme/themes/types.ts b/web/packages/design/src/theme/themes/types.ts
index f50af4564ee67..1c691ad68a106 100644
--- a/web/packages/design/src/theme/themes/types.ts
+++ b/web/packages/design/src/theme/themes/types.ts
@@ -60,6 +60,9 @@ export type ThemeColors = {
primary: string[];
neutral: string[];
success: string[];
+ danger: string[];
+ alert: string[];
+ informational: string[];
};
};
@@ -143,6 +146,12 @@ export type ThemeColors = {
active: string;
};
+ accent: {
+ main: string;
+ hover: string;
+ active: string;
+ };
+
notice: {
background: string;
};
diff --git a/web/packages/design/src/theme/typography.js b/web/packages/design/src/theme/typography.js
index dc2a19a1a493a..44610a0b89e2c 100644
--- a/web/packages/design/src/theme/typography.js
+++ b/web/packages/design/src/theme/typography.js
@@ -85,6 +85,11 @@ const typography = {
fontSize: '10px',
lineHeight: '16px',
},
+ subtitle3: {
+ fontSize: '10px',
+ fontWeight: regular,
+ lineHeight: '14px',
+ },
};
export default typography;
diff --git a/web/packages/teleport/src/Notifications/Notification.story.tsx b/web/packages/teleport/src/Notifications/Notification.story.tsx
new file mode 100644
index 0000000000000..4eb726b49cfd7
--- /dev/null
+++ b/web/packages/teleport/src/Notifications/Notification.story.tsx
@@ -0,0 +1,185 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+
+import { Flex } from 'design';
+
+import {
+ NotificationSubKind,
+ Notification as NotificationType,
+} from 'teleport/services/notifications';
+
+import { Notification } from './Notification';
+
+export default {
+ title: 'Teleport/Notifications',
+};
+
+export const Notifications = () => {
+ return (
+ props.theme.colors.levels.surface};
+ padding: 24px;
+ width: fit-content;
+ height: fit-content;
+ flex-direction: column;
+ gap: 24px;
+ `}
+ >
+ {mockNotifications.map(notification => {
+ return (
+
+ );
+ })}
+
+ );
+};
+
+const mockNotifications: NotificationType[] = [
+ {
+ id: '1',
+ title: '',
+ description: '',
+ subKind: NotificationSubKind.AccessRequestApproved,
+ createdDate: new Date(Date.now() - 30 * 1000), // 30 seconds ago
+ clicked: false,
+ labels: [
+ {
+ name: 'requested-resources',
+ value: 'node-1,node-2,db-1,db-2,db-3',
+ },
+ { name: 'reviewer', value: 'joe' },
+ ],
+ },
+ {
+ id: '2',
+ title: '',
+ description: '',
+ subKind: NotificationSubKind.AccessRequestApproved,
+ createdDate: new Date(Date.now() - 4 * 60 * 1000), // 4 minutes ago
+ clicked: false,
+ labels: [
+ {
+ name: 'requested-role',
+ value: 'auditor',
+ },
+ { name: 'reviewer', value: 'joe' },
+ ],
+ },
+ {
+ id: '3',
+ title: '',
+ description: '',
+ subKind: NotificationSubKind.AccessRequestDenied,
+ createdDate: new Date(Date.now() - 15 * 60 * 1000), // 15 minutes ago
+ clicked: false,
+ labels: [
+ {
+ name: 'requested-role',
+ value: 'auditor',
+ },
+ { name: 'reviewer', value: 'joe' },
+ ],
+ },
+ {
+ id: '4',
+ title: '',
+ description: '',
+ subKind: NotificationSubKind.AccessRequestPending,
+ createdDate: new Date(Date.now() - 3 * 60 * 60 * 1000), // 3 hours ago
+ clicked: true,
+ labels: [
+ {
+ name: 'requested-resources',
+ value: 'db-2,node-5',
+ },
+ { name: 'requester', value: 'bob' },
+ ],
+ },
+ {
+ id: '5',
+ title: '',
+ description: '',
+ subKind: NotificationSubKind.AccessRequestPending,
+ createdDate: new Date(Date.now() - 15 * 60 * 60 * 1000), // 15 hours ago
+ clicked: true,
+ labels: [
+ {
+ name: 'requested-role',
+ value: 'intern',
+ },
+ { name: 'requester', value: 'bob' },
+ ],
+ },
+ {
+ id: '6',
+ title: '',
+ description: '',
+ subKind: NotificationSubKind.AccessRequestNowAssumable,
+ createdDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // 1 day ago
+ clicked: true,
+ labels: [
+ {
+ name: 'requested-resources',
+ value: 'db-2,node-5',
+ },
+ { name: 'requester', value: 'bob' },
+ ],
+ },
+ {
+ id: '7',
+ title: '',
+ description: '',
+ subKind: NotificationSubKind.AccessRequestNowAssumable,
+ createdDate: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // 3 days ago
+ clicked: false,
+ labels: [
+ {
+ name: 'requested-resources',
+ value: 'node-5',
+ },
+ { name: 'requester', value: 'bob' },
+ ],
+ },
+ {
+ id: '8',
+ title: '',
+ description: '',
+ subKind: NotificationSubKind.AccessRequestNowAssumable,
+ createdDate: new Date(Date.now() - 2 * 7 * 24 * 60 * 60 * 1000), // 2 weeks ago
+ clicked: true,
+ labels: [
+ {
+ name: 'requested-role',
+ value: 'auditor',
+ },
+ { name: 'requester', value: 'bob' },
+ ],
+ },
+ {
+ id: '9',
+ title: 'This is an example user-created warning notification',
+ description: 'This is the text content of a warning notification.',
+ subKind: NotificationSubKind.UserCreatedWarning,
+ createdDate: new Date(Date.now() - 93 * 24 * 60 * 60 * 1000), // 3 months ago
+ clicked: true,
+ labels: [],
+ },
+];
diff --git a/web/packages/teleport/src/Notifications/Notification.tsx b/web/packages/teleport/src/Notifications/Notification.tsx
new file mode 100644
index 0000000000000..2fa7fcb2fa53e
--- /dev/null
+++ b/web/packages/teleport/src/Notifications/Notification.tsx
@@ -0,0 +1,291 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React, { useState } from 'react';
+import styled from 'styled-components';
+import { formatDistanceToNowStrict } from 'date-fns';
+
+import * as Icons from 'design/Icon';
+
+import Text from 'design/Text';
+import { ButtonSecondary } from 'design/Button';
+import { MenuIcon, MenuItem } from 'shared/components/MenuAction';
+import Dialog, {
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from 'design/Dialog';
+import { Theme } from 'design/theme/themes/types';
+
+import { Notification as NotificationType } from 'teleport/services/notifications';
+
+import {
+ NotificationContent,
+ notificationContentFactory,
+} from './notificationContentFactory';
+
+export function Notification({
+ notification,
+}: {
+ notification: NotificationType;
+}) {
+ const content = notificationContentFactory(notification);
+
+ // Whether to show the text content dialog. This is only ever used for user-created notifications which only contain informational text
+ // and don't redirect to any page.
+ const [showTextContentDialog, setShowTextContentDialog] = useState(false);
+
+ let AccentIcon;
+ switch (content.type) {
+ case 'success':
+ case 'success-alt':
+ AccentIcon = Icons.Check;
+ break;
+ case 'informational':
+ AccentIcon = Icons.Info;
+ break;
+ case 'warning':
+ AccentIcon = Icons.Warning;
+ break;
+ case 'failure':
+ AccentIcon = Icons.Cross;
+ break;
+ }
+
+ const formattedDate = formatDate(notification.createdDate);
+
+ function onMarkAsClicked() {
+ // TODO rudream - add mark as clicked functionality
+ }
+
+ function onHideNotification() {
+ // TODO rudream - add hide notification functionality
+ }
+
+ function onNotificationClick() {
+ if (content.kind === 'text') {
+ setShowTextContentDialog(true);
+ return;
+ }
+ // TODO rudream - add notification redirect functionality
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {content.title}
+ {content.kind === 'redirect' && content.quickAction && (
+
+ {content.quickAction.buttonText}
+
+ )}
+
+
+ {formattedDate}
+
+
+
+
+
+
+
+ {content.kind === 'text' && (
+
+ )}
+ >
+ );
+}
+
+// formatDate returns how long ago the provided date is in a readable and concise format, ie. "2h ago"
+function formatDate(date: Date) {
+ let distance = formatDistanceToNowStrict(date);
+
+ distance = distance
+ .replace(/seconds?/g, 's')
+ .replace(/minutes?/g, 'm')
+ .replace(/hours?/g, 'h')
+ .replace(/days?/g, 'd')
+ .replace(/months?/g, 'mo')
+ .replace(/years?/g, 'y')
+ .replace(' ', '');
+
+ return `${distance} ago`;
+}
+
+const Container = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: ${props => props.theme.space[3]}px;
+ max-width: 400px;
+ padding: ${props => props.theme.space[3]}px;
+ border-radius: ${props => props.theme.radii[3]}px;
+ cursor: pointer;
+
+ background: ${props => props.theme.colors.interactive.tonal.primary[0]};
+ &:hover {
+ background: ${props => props.theme.colors.interactive.tonal.primary[1]};
+ }
+
+ ${props =>
+ props.clicked &&
+ `
+ background: ${props.theme.colors.interactive.tonal.neutral[0]};
+ &:hover {
+ background: ${props.theme.colors.interactive.tonal.neutral[1]};
+ }
+ `}
+`;
+
+const ContentContainer = styled.div`
+ display: flex;
+ justify-content: space-between;
+ gap: ${props => props.theme.space[2]}px;
+ width: 100%;
+`;
+
+const ContentBody = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ gap: ${props => props.theme.space[2]}px;
+`;
+
+const SideContent = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ gap: ${props => props.theme.space[2]}px;
+ align-items: center;
+ white-space: nowrap;
+`;
+
+const GraphicContainer = styled.div`
+ height: 48px;
+ width: 48px;
+ position: relative;
+ flex-shrink: 0;
+`;
+
+function getIconColors(
+ theme: Theme,
+ type: NotificationContent['type']
+): {
+ primary: string;
+ secondary: string;
+} {
+ switch (type) {
+ case 'success':
+ return {
+ primary: theme.colors.success.main,
+ secondary: theme.colors.interactive.tonal.success[0],
+ };
+ case 'success-alt':
+ return {
+ primary: theme.colors.accent.main,
+ secondary: theme.colors.interactive.tonal.informational[0],
+ };
+ case 'informational':
+ return {
+ primary: theme.colors.brand,
+ secondary: theme.colors.interactive.tonal.primary[0],
+ };
+ case `warning`:
+ return {
+ primary: theme.colors.warning.main,
+ secondary: theme.colors.interactive.tonal.alert[0],
+ };
+ case 'failure':
+ return {
+ primary: theme.colors.error.main,
+ secondary: theme.colors.interactive.tonal.danger[0],
+ };
+ }
+}
+
+const MainIconContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 38px;
+ height: 38px;
+ border-radius: ${props => props.theme.radii[3]}px;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ left: 0;
+
+ border: ${props => props.theme.borders[1]};
+ border-color: ${props => getIconColors(props.theme, props.type).primary};
+
+ background-color: ${props =>
+ getIconColors(props.theme, props.type).secondary};
+`;
+
+const AccentIconContainer = styled.div`
+ height: 18px;
+ width: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: ${props => props.theme.radii[2]}px;
+ position: absolute;
+ z-index: 2;
+ bottom: 0;
+ right: 0;
+
+ background-color: ${props => getIconColors(props.theme, props.type).primary};
+`;
diff --git a/web/packages/teleport/src/Notifications/notificationContentFactory.tsx b/web/packages/teleport/src/Notifications/notificationContentFactory.tsx
new file mode 100644
index 0000000000000..fa2256e04bfc5
--- /dev/null
+++ b/web/packages/teleport/src/Notifications/notificationContentFactory.tsx
@@ -0,0 +1,231 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import * as Icons from 'design/Icon';
+import { IconProps } from 'design/Icon/Icon';
+import React from 'react';
+
+import {
+ Notification as NotificationType,
+ NotificationSubKind,
+} from 'teleport/services/notifications';
+import { Label } from 'teleport/types';
+
+export function notificationContentFactory({
+ subKind,
+ description,
+ labels,
+ ...notification
+}: NotificationType): NotificationContent {
+ let notificationContent: NotificationContent;
+
+ switch (subKind) {
+ case NotificationSubKind.DefaultInformational:
+ case NotificationSubKind.UserCreatedInformational:
+ notificationContent = {
+ kind: 'text',
+ title: notification.title,
+ textContent: description,
+ type: 'informational',
+ icon: Icons.Notification,
+ };
+ break;
+
+ case NotificationSubKind.DefaultWarning:
+ case NotificationSubKind.UserCreatedWarning:
+ notificationContent = {
+ kind: 'text',
+ title: notification.title,
+ textContent: description,
+ type: 'warning',
+ icon: Icons.Notification,
+ };
+ break;
+
+ case NotificationSubKind.AccessRequestApproved: {
+ let title;
+
+ const reviewer = getLabelValue(labels, 'reviewer');
+ const requestedResources = getLabelValue(labels, 'requested-resources');
+ const numRequestedResources = requestedResources.length
+ ? requestedResources.split(',').length
+ : 0;
+
+ // Check if it is a resource request or a role request.
+ if (numRequestedResources) {
+ title = `${reviewer} approved your access request for ${numRequestedResources} resource${
+ numRequestedResources > 1 ? 's' : ''
+ }.`;
+ } else {
+ const requestedRole = getLabelValue(labels, 'requested-role');
+ title = `${reviewer} approved your access request for the '${requestedRole}' role.`;
+ }
+
+ notificationContent = {
+ kind: 'redirect',
+ title,
+ type: 'success',
+ icon: Icons.Users,
+ redirectRoute: '/', //TODO: rudream - handle enterprise routes
+ quickAction: {
+ onClick: () => null, //TODO: rudream - handle assuming roles from quick action button
+ buttonText: 'Assume Roles',
+ },
+ };
+ break;
+ }
+
+ case NotificationSubKind.AccessRequestDenied: {
+ let title;
+
+ const reviewer = getLabelValue(labels, 'reviewer');
+ const requestedResources = getLabelValue(labels, 'requested-resources');
+ const numRequestedResources = requestedResources.length
+ ? requestedResources.split(',').length
+ : 0;
+
+ // Check if it is a resource request or a role request.
+ if (numRequestedResources) {
+ title = `${reviewer} denied your access request for ${numRequestedResources} resource${
+ numRequestedResources > 1 ? 's' : ''
+ }.`;
+ } else {
+ const requestedRole = getLabelValue(labels, 'requested-role');
+ title = `${reviewer} denied your access request for the '${requestedRole}' role.`;
+ }
+
+ notificationContent = {
+ kind: 'redirect',
+ title,
+ type: 'failure',
+ icon: Icons.Users,
+ redirectRoute: '/', //TODO: rudream - handle enterprise routes
+ };
+ break;
+ }
+
+ case NotificationSubKind.AccessRequestPending: {
+ let title;
+
+ const requester = getLabelValue(labels, 'requester');
+ const requestedResources = getLabelValue(labels, 'requested-resources');
+ const numRequestedResources = requestedResources.length
+ ? requestedResources.split(',').length
+ : 0;
+
+ // Check if it is a resource request or a role request.
+ if (numRequestedResources) {
+ title = `${requester} requested access to ${numRequestedResources} resource${
+ numRequestedResources > 1 ? 's' : ''
+ }.`;
+ } else {
+ const requestedRole = getLabelValue(labels, 'requested-role');
+ title = `${requester} requested access to the '${requestedRole}' role.`;
+ }
+
+ notificationContent = {
+ kind: 'redirect',
+ title,
+ type: 'informational',
+ icon: Icons.UserList,
+ redirectRoute: '/', //TODO: rudream - handle enterprise routes
+ };
+ break;
+ }
+
+ case NotificationSubKind.AccessRequestNowAssumable: {
+ let title;
+ let buttonText;
+
+ const requestedResources = getLabelValue(labels, 'requested-resources');
+ const numRequestedResources = requestedResources.length
+ ? requestedResources.split(',').length
+ : 0;
+
+ // Check if it is a resource request or a role request.
+ if (numRequestedResources) {
+ if (numRequestedResources === 1) {
+ title = `"${requestedResources}" is now available to access.`;
+ } else {
+ title = `${numRequestedResources} resources are now available to access.`;
+ }
+ buttonText = 'Access Now';
+ } else {
+ const requestedRole = getLabelValue(labels, 'requested-role');
+ title = `"${requestedRole}" is now ready to assume.`;
+ buttonText = 'Assume Role';
+ }
+
+ notificationContent = {
+ kind: 'redirect',
+ title,
+ type: 'success-alt',
+ icon: Icons.Users,
+ redirectRoute: '/', //TODO: rudream - handle enterprise routes
+ quickAction: {
+ onClick: () => null, //TODO: rudream - handle assuming roles from quick action button
+ buttonText: buttonText,
+ },
+ };
+ break;
+ }
+ }
+
+ return notificationContent;
+}
+
+type NotificationContentBase = {
+ /** title is the title of the notification. */
+ title: string;
+ /** type is the type of notification this is, this will determine the style of this notification (color and sub-icon). */
+ type: 'success' | 'success-alt' | 'informational' | 'warning' | 'failure';
+ /** icon is the icon to render for this notification. This should be an icon from `design/Icon`. */
+ icon: React.FC;
+};
+
+/** For notifications that are clickable and redirect you to a page, and may also optionally include a quick action. */
+type NotificationContentRedirect = NotificationContentBase & {
+ kind: 'redirect';
+ /** redirectRoute is the route the user should be redirected to when clicking the notification, if any. */
+ redirectRoute: string;
+ quickAction?: {
+ /** onClick is what should be run when the user clicks on the quick action button */
+ onClick: () => void;
+ /** buttonText is the text that should be shown on the quick action button */
+ buttonText: string;
+ };
+};
+
+/** For notifications that only contain text and are not interactive in any other way. This is used for user-created notifications. */
+type NotificationContentText = NotificationContentBase & {
+ kind: 'text';
+ /** textContent is the text content of the notification, this is used for user-created notifications and will contain the text that will be shown in a modal upon clicking the notification. */
+ textContent: string;
+};
+
+export type NotificationContent =
+ | NotificationContentRedirect
+ | NotificationContentText;
+
+// getLabelValue returns the value of a label for a given key.
+function getLabelValue(labels: Label[], key: string): string {
+ const label = labels.find(label => {
+ return label.name === key;
+ });
+ return label?.value || '';
+}
diff --git a/web/packages/teleport/src/services/notifications/index.ts b/web/packages/teleport/src/services/notifications/index.ts
new file mode 100644
index 0000000000000..ed965b13f72eb
--- /dev/null
+++ b/web/packages/teleport/src/services/notifications/index.ts
@@ -0,0 +1,19 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+export * from './types';
diff --git a/web/packages/teleport/src/services/notifications/types.ts b/web/packages/teleport/src/services/notifications/types.ts
new file mode 100644
index 0000000000000..dafceead11c09
--- /dev/null
+++ b/web/packages/teleport/src/services/notifications/types.ts
@@ -0,0 +1,49 @@
+/**
+ * Teleport
+ * Copyright (C) 2023 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import { Label } from 'teleport/types';
+
+export type Notification = {
+ /** id is the uuid of this notification */
+ id: string;
+ /* subKind is a string which represents which type of notification this is, ie. "access-request-approved" */
+ subKind: NotificationSubKind;
+ /** createdDate is when the notification was created. */
+ createdDate: Date;
+ /** clicked is whether this notification has been clicked on by this user. */
+ clicked: boolean;
+ /** labels are this notification's labels, this is where the notification's metadata is stored.*/
+ labels: Label[];
+ /** title is the title of this notification. It is preferred to not use this and instead construct a title dynamically using metadata from the labels. */
+ title: string;
+ /** description is the description of this notification. It is preferred to not use this and instead construct a description dynamically using metadata from the labels. */
+ description: string;
+};
+
+/** NotificationSubKind is the subkind of notifications, these should be kept in sync with TBD (TODO: rudream - add backend counterpart location here) */
+export enum NotificationSubKind {
+ DefaultInformational = 'default-informational',
+ DefaultWarning = 'default-warning',
+ UserCreatedInformational = 'user-created-informational',
+ UserCreatedWarning = 'user-created-warning',
+ AccessRequestPending = 'access-request-pending',
+ AccessRequestApproved = 'access-request-approved',
+ AccessRequestDenied = 'access-request-denied',
+ /** AccessRequestNowAssumable is the notification for when an approved access request that was scheduled for a later date is now assumable. */
+ AccessRequestNowAssumable = 'access-request-now-assumable',
+}