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: Allow different time units for retention policy #32425

Merged
merged 36 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7ee66fb
Settings and translations
gabriellsh May 13, 2024
1c7467c
Timespan input
gabriellsh May 13, 2024
8d55014
cron
gabriellsh May 13, 2024
183357e
add cs
gabriellsh May 13, 2024
62e38fc
Merge branch 'develop' into imp/retention
gabriellsh May 13, 2024
d7ae3bd
add unit tests
gabriellsh May 15, 2024
ee32132
clean migration
KevLehman May 16, 2024
430e960
Review and QA
gabriellsh May 16, 2024
6d13c8f
review and QA 2
gabriellsh May 17, 2024
d24ba1b
Fix other places that use the setting
gabriellsh May 20, 2024
ed03337
fix migration check
gabriellsh May 20, 2024
db22715
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into imp/…
gabriellsh May 28, 2024
01aeaa9
fix ts and unit
gabriellsh May 28, 2024
4e58642
fix typecheck
gabriellsh May 28, 2024
5d4908f
typecheck again
gabriellsh May 28, 2024
b7ae52c
undo setting
gabriellsh May 31, 2024
d78832a
unbreak-it
gabriellsh May 31, 2024
e9a586b
update only once
gabriellsh May 31, 2024
3bab15c
remove migration
gabriellsh May 31, 2024
44e84eb
fix tests
gabriellsh Jun 3, 2024
79bcef3
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into imp/…
gabriellsh Jun 3, 2024
9cb434a
fix edit room
gabriellsh Jun 3, 2024
971b318
fix test
gabriellsh Jun 3, 2024
dc5a9db
Update apps/meteor/server/startup/migrations/xrun.ts
gabriellsh Jun 7, 2024
078197a
fix xrun
gabriellsh Jun 7, 2024
53a3393
Merge branch 'develop' into imp/retention
gabriellsh Jun 10, 2024
48e4c0c
remove the balaca
gabriellsh Jun 11, 2024
b7039a5
update translation
gabriellsh Jun 11, 2024
3b01f2d
Review
gabriellsh Jun 11, 2024
c5f280e
fix lint
gabriellsh Jun 12, 2024
457e539
Merge branch 'develop' into imp/retention
gabriellsh Jun 13, 2024
0cdccd8
Merge branch 'develop' of github.com:RocketChat/Rocket.Chat into imp/…
gabriellsh Jun 18, 2024
bf4db11
Merge branch 'develop' into imp/retention
gabriellsh Jun 19, 2024
97b16db
Merge branch 'develop' into imp/retention
gabriellsh Jun 20, 2024
533f8bf
Merge branch 'develop' into imp/retention
ggazzo Jun 20, 2024
02b5ddf
Merge branch 'develop' into imp/retention
ggazzo Jun 21, 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
7 changes: 7 additions & 0 deletions .changeset/ten-stingrays-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
"@rocket.chat/i18n": minor
---

Added the possibility to choose the time unit (days, hours, minutes) to the global retention policy settings
1 change: 1 addition & 0 deletions apps/meteor/app/lib/server/methods/saveSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Meteor.methods<ServerMethods>({
case 'boolean':
check(value, Boolean);
break;
case 'timespan':
case 'int':
check(value, Number);
break;
Expand Down
14 changes: 7 additions & 7 deletions apps/meteor/app/retention-policy/server/cronPruneMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async function job(): Promise<void> {
// get all rooms with default values
for await (const type of types) {
const maxAge = maxTimes[type] || 0;
const latest = new Date(now.getTime() - toDays(maxAge));
const latest = new Date(now.getTime() - maxAge);

const rooms = await Rooms.find(
{
Expand Down Expand Up @@ -107,9 +107,9 @@ settings.watchMultiple(
'RetentionPolicy_AppliesToChannels',
'RetentionPolicy_AppliesToGroups',
'RetentionPolicy_AppliesToDMs',
'RetentionPolicy_MaxAge_Channels',
'RetentionPolicy_MaxAge_Groups',
'RetentionPolicy_MaxAge_DMs',
'RetentionPolicy_TTL_Channels',
'RetentionPolicy_TTL_Groups',
'RetentionPolicy_TTL_DMs',
'RetentionPolicy_Advanced_Precision',
'RetentionPolicy_Advanced_Precision_Cron',
'RetentionPolicy_Precision',
Expand All @@ -132,9 +132,9 @@ settings.watchMultiple(
types.push('d');
}

maxTimes.c = settings.get('RetentionPolicy_MaxAge_Channels');
maxTimes.p = settings.get('RetentionPolicy_MaxAge_Groups');
maxTimes.d = settings.get('RetentionPolicy_MaxAge_DMs');
maxTimes.c = settings.get<number>('RetentionPolicy_TTL_Channels');
maxTimes.p = settings.get<number>('RetentionPolicy_TTL_Groups');
maxTimes.d = settings.get<number>('RetentionPolicy_TTL_DMs');

const precision =
(settings.get<boolean>('RetentionPolicy_Advanced_Precision') && settings.get<string>('RetentionPolicy_Advanced_Precision_Cron')) ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { FC } from 'react';
import React from 'react';

import { useFormattedRelativeTime } from '../../hooks/useFormattedRelativeTime';
import { getMaxAgeInMS } from '../../views/room/hooks/useRetentionPolicy';

type RetentionPolicyCalloutProps = {
filesOnly: boolean;
Expand All @@ -14,7 +13,7 @@ type RetentionPolicyCalloutProps = {

const RetentionPolicyCallout: FC<RetentionPolicyCalloutProps> = ({ filesOnly, excludePinned, maxAge }) => {
const t = useTranslation();
const time = useFormattedRelativeTime(getMaxAgeInMS(maxAge));
const time = useFormattedRelativeTime(maxAge);

return (
<Callout arial-label={t('Retention_policy_warning_callout')} role='alert' aria-live='polite' type='warning'>
Expand Down
65 changes: 65 additions & 0 deletions apps/meteor/client/lib/convertTimeUnit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { TIMEUNIT, timeUnitToMs, msToTimeUnit } from './convertTimeUnit';

describe('timeUnitToMs function', () => {
it('should correctly convert days to milliseconds', () => {
gabriellsh marked this conversation as resolved.
Show resolved Hide resolved
expect(timeUnitToMs(TIMEUNIT.days, 1)).toBe(86400000);
expect(timeUnitToMs(TIMEUNIT.days, 2)).toBe(172800000);
expect(timeUnitToMs(TIMEUNIT.days, 0.5)).toBe(43200000);
});

it('should correctly convert hours to milliseconds', () => {
expect(timeUnitToMs(TIMEUNIT.hours, 1)).toBe(3600000);
expect(timeUnitToMs(TIMEUNIT.hours, 2)).toBe(7200000);
expect(timeUnitToMs(TIMEUNIT.hours, 0.5)).toBe(1800000);
});

it('should correctly convert minutes to milliseconds', () => {
expect(timeUnitToMs(TIMEUNIT.minutes, 1)).toBe(60000);
expect(timeUnitToMs(TIMEUNIT.minutes, 2)).toBe(120000);
expect(timeUnitToMs(TIMEUNIT.minutes, 0.5)).toBe(30000);
});

it('should throw an error for invalid time units', () => {
expect(() => timeUnitToMs('invalidUnit' as TIMEUNIT, 1)).toThrow('timeUnitToMs - invalid time unit');
});

it('should throw an error for invalid timespan', () => {
const errorMessage = 'timeUnitToMs - invalid timespan';
expect(() => timeUnitToMs(TIMEUNIT.days, NaN)).toThrow(errorMessage);
expect(() => timeUnitToMs(TIMEUNIT.days, Infinity)).toThrow(errorMessage);
expect(() => timeUnitToMs(TIMEUNIT.days, -Infinity)).toThrow(errorMessage);
expect(() => timeUnitToMs(TIMEUNIT.days, -1)).toThrow(errorMessage);
});
});

describe('msToTimeUnit function', () => {
it('should correctly convert milliseconds to days', () => {
gabriellsh marked this conversation as resolved.
Show resolved Hide resolved
expect(msToTimeUnit(TIMEUNIT.days, 86400000)).toBe(1); // 1 day
expect(msToTimeUnit(TIMEUNIT.days, 172800000)).toBe(2); // 2 days
expect(msToTimeUnit(TIMEUNIT.days, 43200000)).toBe(0.5); // .5 days
});

it('should correctly convert milliseconds to hours', () => {
expect(msToTimeUnit(TIMEUNIT.hours, 3600000)).toBe(1); // 1 hour
expect(msToTimeUnit(TIMEUNIT.hours, 7200000)).toBe(2); // 2 hours
expect(msToTimeUnit(TIMEUNIT.hours, 1800000)).toBe(0.5); // .5 hours
});

it('should correctly convert milliseconds to minutes', () => {
expect(msToTimeUnit(TIMEUNIT.minutes, 60000)).toBe(1); // 1 min
expect(msToTimeUnit(TIMEUNIT.minutes, 120000)).toBe(2); // 2 min
expect(msToTimeUnit(TIMEUNIT.minutes, 30000)).toBe(0.5); // .5 min
});

it('should throw an error for invalid time units', () => {
expect(() => msToTimeUnit('invalidUnit' as TIMEUNIT, 1)).toThrow('msToTimeUnit - invalid time unit');
});

it('should throw an error for invalid timespan', () => {
const errorMessage = 'msToTimeUnit - invalid timespan';
expect(() => msToTimeUnit(TIMEUNIT.days, NaN)).toThrow(errorMessage);
expect(() => msToTimeUnit(TIMEUNIT.days, Infinity)).toThrow(errorMessage);
expect(() => msToTimeUnit(TIMEUNIT.days, -Infinity)).toThrow(errorMessage);
expect(() => msToTimeUnit(TIMEUNIT.days, -1)).toThrow(errorMessage);
});
});
58 changes: 58 additions & 0 deletions apps/meteor/client/lib/convertTimeUnit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export enum TIMEUNIT {
days = 'days',
hours = 'hours',
minutes = 'minutes',
}

const isValidTimespan = (timespan: number): boolean => {
if (Number.isNaN(timespan)) {
return false;
}

if (!Number.isFinite(timespan)) {
return false;
}

if (timespan < 0) {
MarcosSpessatto marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

return true;
};

export const timeUnitToMs = (unit: TIMEUNIT, timespan: number) => {
if (!isValidTimespan(timespan)) {
throw new Error('timeUnitToMs - invalid timespan');
}

switch (unit) {
case TIMEUNIT.days:
return timespan * 24 * 60 * 60 * 1000;

case TIMEUNIT.hours:
return timespan * 60 * 60 * 1000;

case TIMEUNIT.minutes:
return timespan * 60 * 1000;

default:
throw new Error('timeUnitToMs - invalid time unit');
}
};

export const msToTimeUnit = (unit: TIMEUNIT, timespan: number) => {
if (!isValidTimespan(timespan)) {
throw new Error('msToTimeUnit - invalid timespan');
}

switch (unit) {
case TIMEUNIT.days:
return timespan / 24 / 60 / 60 / 1000;
case TIMEUNIT.hours:
return timespan / 60 / 60 / 1000;
case TIMEUNIT.minutes:
return timespan / 60 / 1000;
default:
throw new Error('msToTimeUnit - invalid time unit');
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const PriorityEditForm = ({ data, onSave, onCancel }: PriorityEditFormProps): Re
render={({ field: { value, onChange } }): ReactElement => (
<StringSettingInput
_id=''
packageValue={defaultName}
disabled={isSaving}
error={errors.name?.message}
label={`${t('Name')}*`}
Expand Down
3 changes: 3 additions & 0 deletions apps/meteor/client/views/admin/settings/MemoizedSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import RoomPickSettingInput from './inputs/RoomPickSettingInput';
import SelectSettingInput from './inputs/SelectSettingInput';
import SelectTimezoneSettingInput from './inputs/SelectTimezoneSettingInput';
import StringSettingInput from './inputs/StringSettingInput';
import TimespanSettingInput from './inputs/TimespanSettingInput';

// @todo: the props are loosely typed because `Setting` needs to typecheck them.
const inputsByType: Record<ISettingBase['type'], ElementType<any>> = {
Expand All @@ -39,13 +40,15 @@ const inputsByType: Record<ISettingBase['type'], ElementType<any>> = {
roomPick: RoomPickSettingInput,
timezone: SelectTimezoneSettingInput,
lookup: LookupSettingInput,
timespan: TimespanSettingInput,
date: GenericSettingInput, // @todo: implement
group: GenericSettingInput, // @todo: implement
};

type MemoizedSettingProps = {
_id?: string;
type: ISettingBase['type'];
packageValue: ISettingBase['packageValue'];
hint?: ReactNode;
callout?: ReactNode;
value?: SettingValue;
Expand Down
22 changes: 11 additions & 11 deletions apps/meteor/client/views/admin/settings/Setting.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ WithCallout.args = {

export const types = () => (
<FieldGroup>
<Setting.Memoized _id='setting-id-1' label='Label' type='action' actionText='Action text' />
<Setting.Memoized _id='setting-id-2' label='Label' type='asset' />
<Setting.Memoized _id='setting-id-3' label='Label' type='boolean' />
<Setting.Memoized _id='setting-id-4' label='Label' type='code' />
<Setting.Memoized _id='setting-id-5' label='Label' type='font' />
<Setting.Memoized _id='setting-id-6' label='Label' type='int' />
<Setting.Memoized _id='setting-id-7' label='Label' type='language' />
<Setting.Memoized _id='setting-id-8' label='Label' type='password' />
<Setting.Memoized _id='setting-id-9' label='Label' type='relativeUrl' />
<Setting.Memoized _id='setting-id-10' label='Label' type='select' />
<Setting.Memoized _id='setting-id-11' label='Label' type='string' />
<Setting.Memoized packageValue _id='setting-id-1' label='Label' type='action' actionText='Action text' />
<Setting.Memoized packageValue='' _id='setting-id-2' label='Label' type='asset' />
<Setting.Memoized packageValue _id='setting-id-3' label='Label' type='boolean' />
<Setting.Memoized packageValue='' _id='setting-id-4' label='Label' type='code' />
<Setting.Memoized packageValue='' _id='setting-id-5' label='Label' type='font' />
<Setting.Memoized packageValue={1} _id='setting-id-6' label='Label' type='int' />
<Setting.Memoized packageValue='' _id='setting-id-7' label='Label' type='language' />
<Setting.Memoized packageValue='' _id='setting-id-8' label='Label' type='password' />
<Setting.Memoized packageValue='' _id='setting-id-9' label='Label' type='relativeUrl' />
<Setting.Memoized packageValue='' _id='setting-id-10' label='Label' type='select' />
<Setting.Memoized packageValue='' _id='setting-id-11' label='Label' type='string' />
</FieldGroup>
);

Expand Down
Loading
Loading