mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
feat: add timestamps to project events (#8389)
This PR adds timestamps to project events and displays them in the "latest events" box in the project details view. It also changes the font weight of events to be only normal. ![image](https://github.com/user-attachments/assets/69ee4052-fe96-4fc9-ae45-0818acb0570a)
This commit is contained in:
parent
5d61b3903b
commit
f23ba70bb4
@ -2,7 +2,9 @@ import type { FC } from 'react';
|
||||
import { Markdown } from '../common/Markdown/Markdown';
|
||||
import type { PersonalDashboardProjectDetailsSchema } from '../../openapi';
|
||||
import { UserAvatar } from '../common/UserAvatar/UserAvatar';
|
||||
import { styled } from '@mui/material';
|
||||
import { Typography, styled } from '@mui/material';
|
||||
import { formatDateYMDHM } from 'utils/formatDate';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
|
||||
const Events = styled('ul')(({ theme }) => ({
|
||||
padding: 0,
|
||||
@ -16,27 +18,72 @@ const Event = styled('li')(({ theme }) => ({
|
||||
gap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing(4),
|
||||
|
||||
'*': {
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
}));
|
||||
|
||||
const TitleContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const ActionBox = styled('article')(({ theme }) => ({
|
||||
padding: theme.spacing(0, 2),
|
||||
display: 'flex',
|
||||
gap: theme.spacing(3),
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const Timestamp = styled('time')(({ theme }) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.typography.fontSize,
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export const LatestProjectEvents: FC<{
|
||||
latestEvents: PersonalDashboardProjectDetailsSchema['latestEvents'];
|
||||
}> = ({ latestEvents }) => {
|
||||
const { locationSettings } = useLocationSettings();
|
||||
return (
|
||||
<Events>
|
||||
{latestEvents.map((event) => {
|
||||
return (
|
||||
<Event key={event.id}>
|
||||
<UserAvatar
|
||||
src={event.createdByImageUrl}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
<Markdown>
|
||||
{event.summary ||
|
||||
'No preview available for this event'}
|
||||
</Markdown>
|
||||
</Event>
|
||||
);
|
||||
})}
|
||||
</Events>
|
||||
<ActionBox>
|
||||
<TitleContainer>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
component='h4'
|
||||
>
|
||||
Latest Events
|
||||
</Typography>
|
||||
</TitleContainer>
|
||||
<Events>
|
||||
{latestEvents.map((event) => {
|
||||
return (
|
||||
<Event key={event.id}>
|
||||
<UserAvatar
|
||||
src={event.createdByImageUrl}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
<div>
|
||||
<Timestamp dateTime={event.createdAt}>
|
||||
{formatDateYMDHM(
|
||||
event.createdAt,
|
||||
locationSettings.locale,
|
||||
)}
|
||||
</Timestamp>
|
||||
<Markdown>
|
||||
{event.summary ||
|
||||
'No preview available for this event'}
|
||||
</Markdown>
|
||||
</div>
|
||||
</Event>
|
||||
);
|
||||
})}
|
||||
</Events>
|
||||
</ActionBox>
|
||||
);
|
||||
};
|
||||
|
@ -22,4 +22,5 @@ export type PersonalDashboardProjectDetailsSchemaLatestEventsItem = {
|
||||
* @nullable
|
||||
*/
|
||||
summary: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
@ -237,6 +237,8 @@ test('should return personal dashboard project details', async () => {
|
||||
`/api/admin/personal-dashboard/${project.id}`,
|
||||
);
|
||||
|
||||
const timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
||||
|
||||
expect(body).toMatchObject({
|
||||
owners: [
|
||||
{
|
||||
@ -268,24 +270,28 @@ test('should return personal dashboard project details', async () => {
|
||||
},
|
||||
latestEvents: [
|
||||
{
|
||||
createdAt: expect.stringMatching(timestampPattern),
|
||||
createdBy: 'new_user@test.com',
|
||||
summary: expect.stringContaining(
|
||||
'**new_user@test.com** created **[log_feature_c]',
|
||||
),
|
||||
},
|
||||
{
|
||||
createdAt: expect.stringMatching(timestampPattern),
|
||||
createdBy: 'new_user@test.com',
|
||||
summary: expect.stringContaining(
|
||||
'**new_user@test.com** created **[log_feature_b]',
|
||||
),
|
||||
},
|
||||
{
|
||||
createdAt: expect.stringMatching(timestampPattern),
|
||||
createdBy: 'new_user@test.com',
|
||||
summary: expect.stringContaining(
|
||||
'**new_user@test.com** created **[log_feature_a]',
|
||||
),
|
||||
},
|
||||
{
|
||||
createdAt: expect.stringMatching(timestampPattern),
|
||||
createdBy: 'unknown',
|
||||
summary: expect.stringContaining(
|
||||
'triggered **project-access-added**',
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { type IUnleashConfig, type IUnleashServices, NONE } from '../../types';
|
||||
import {
|
||||
type IUnleashConfig,
|
||||
type IUnleashServices,
|
||||
NONE,
|
||||
serializeDates,
|
||||
} from '../../types';
|
||||
import type { OpenApiService } from '../../services';
|
||||
import {
|
||||
createResponseSchema,
|
||||
@ -114,9 +119,9 @@ export default class PersonalDashboardController extends Controller {
|
||||
200,
|
||||
res,
|
||||
personalDashboardProjectDetailsSchema.$id,
|
||||
{
|
||||
serializeDates({
|
||||
...projectDetails,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,19 @@ import type { PersonalDashboardProjectDetailsSchema } from '../../openapi';
|
||||
import type { IRoleWithProject } from '../../types/stores/access-store';
|
||||
import { NotFoundError } from '../../error';
|
||||
|
||||
type PersonalDashboardProjectDetailsUnserialized = Omit<
|
||||
PersonalDashboardProjectDetailsSchema,
|
||||
'latestEvents'
|
||||
> & {
|
||||
latestEvents: {
|
||||
createdBy: string;
|
||||
summary: string;
|
||||
createdByImageUrl: string;
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
}[];
|
||||
};
|
||||
|
||||
export class PersonalDashboardService {
|
||||
private personalDashboardReadModel: IPersonalDashboardReadModel;
|
||||
|
||||
@ -105,7 +118,7 @@ export class PersonalDashboardService {
|
||||
async getPersonalProjectDetails(
|
||||
userId: number,
|
||||
projectId: string,
|
||||
): Promise<PersonalDashboardProjectDetailsSchema> {
|
||||
): Promise<PersonalDashboardProjectDetailsUnserialized> {
|
||||
const onboardingStatus =
|
||||
await this.onboardingReadModel.getOnboardingStatusForProject(
|
||||
projectId,
|
||||
@ -119,6 +132,7 @@ export class PersonalDashboardService {
|
||||
|
||||
const formatEvents = (recentEvents: IEvent[]) =>
|
||||
recentEvents.map((event) => ({
|
||||
createdAt: event.createdAt,
|
||||
summary: this.featureEventFormatter.format(event).text,
|
||||
createdBy: event.createdBy,
|
||||
id: event.id,
|
||||
|
@ -89,7 +89,13 @@ export const personalDashboardProjectDetailsSchema = {
|
||||
type: 'object',
|
||||
description: 'An event summary',
|
||||
additionalProperties: false,
|
||||
required: ['summary', 'createdBy', 'createdByImageUrl', 'id'],
|
||||
required: [
|
||||
'summary',
|
||||
'createdBy',
|
||||
'createdByImageUrl',
|
||||
'id',
|
||||
'createdAt',
|
||||
],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
@ -112,6 +118,12 @@ export const personalDashboardProjectDetailsSchema = {
|
||||
description: `URL used for the user profile image of the event author`,
|
||||
example: 'https://example.com/242x200.png',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: 'When the event was recorded',
|
||||
example: '2021-09-01T12:00:00Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user