Skip to content

Commit

Permalink
[MM-53990] Support a global retention time of less than 1 day (matter…
Browse files Browse the repository at this point in the history
…most#25196)

* adding new MessageRetentionHours config

---------

Co-authored-by: Mattermost Build <[email protected]>
  • Loading branch information
BenCookie95 and mattermost-build authored Jan 9, 2024
1 parent 82b8d4d commit bb88b92
Show file tree
Hide file tree
Showing 18 changed files with 376 additions and 47 deletions.
2 changes: 2 additions & 0 deletions e2e-tests/playwright/support/server/default_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,9 @@ const defaultServerConfig: AdminConfig = {
EnableFileDeletion: false,
EnableBoardsDeletion: false,
MessageRetentionDays: 365,
MessageRetentionHours: 0,
FileRetentionDays: 365,
FileRetentionHours: 0,
BoardsRetentionDays: 365,
DeletionJobStartTime: '02:00',
BatchSize: 3000,
Expand Down
8 changes: 4 additions & 4 deletions server/config/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["AllowCustomThemes"] = "true"
props["AllowedThemes"] = ""
props["DataRetentionEnableMessageDeletion"] = "false"
props["DataRetentionMessageRetentionDays"] = "0"
props["DataRetentionMessageRetentionHours"] = "0"
props["DataRetentionEnableFileDeletion"] = "false"
props["DataRetentionFileRetentionDays"] = "0"
props["DataRetentionFileRetentionHours"] = "0"

props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomURLSchemes, ",")
props["MaxMarkdownNodes"] = strconv.FormatInt(int64(*c.DisplaySettings.MaxMarkdownNodes), 10)
Expand Down Expand Up @@ -204,9 +204,9 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li

if *license.Features.DataRetention {
props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion)
props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10)
props["DataRetentionMessageRetentionHours"] = strconv.FormatInt(int64(c.DataRetentionSettings.GetMessageRetentionHours()), 10)
props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion)
props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10)
props["DataRetentionFileRetentionHours"] = strconv.FormatInt(int64(c.DataRetentionSettings.GetFileRetentionHours()), 10)
}

if license.HasSharedChannels() {
Expand Down
28 changes: 26 additions & 2 deletions server/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8742,13 +8742,37 @@
"id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error",
"translation": "Data retention job start time must be a 24-hour time stamp in the form HH:MM."
},
{
"id": "model.config.is_valid.data_retention.file_retention_both_zero.app_error",
"translation": "File retention days and file retention hours cannot both be 0."
},
{
"id": "model.config.is_valid.data_retention.file_retention_days_too_low.app_error",
"translation": "File retention must be one day or longer."
"translation": "File retention days cannot be less than 0."
},
{
"id": "model.config.is_valid.data_retention.file_retention_hours_too_low.app_error",
"translation": "File retention hours cannot be less than 0"
},
{
"id": "model.config.is_valid.data_retention.file_retention_misconfiguration.app_error",
"translation": "File retention days and file retention hours cannot both be greater than 0."
},
{
"id": "model.config.is_valid.data_retention.message_retention_both_zero.app_error",
"translation": "Message retention days and message retention hours cannot both be 0."
},
{
"id": "model.config.is_valid.data_retention.message_retention_days_too_low.app_error",
"translation": "Message retention must be one day or longer."
"translation": "Message retention days cannot be less than 0."
},
{
"id": "model.config.is_valid.data_retention.message_retention_hours_too_low.app_error",
"translation": "Message retention hours cannot be less than 0"
},
{
"id": "model.config.is_valid.data_retention.message_retention_misconfiguration.app_error",
"translation": "Message retention days and message retention hours cannot both be greater than 0."
},
{
"id": "model.config.is_valid.directory.app_error",
Expand Down
2 changes: 2 additions & 0 deletions server/platform/services/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,9 @@ func (ts *TelemetryService) trackConfig() {
"enable_message_deletion": *cfg.DataRetentionSettings.EnableMessageDeletion,
"enable_file_deletion": *cfg.DataRetentionSettings.EnableFileDeletion,
"message_retention_days": *cfg.DataRetentionSettings.MessageRetentionDays,
"message_retention_hours": *cfg.DataRetentionSettings.MessageRetentionHours,
"file_retention_days": *cfg.DataRetentionSettings.FileRetentionDays,
"file_retention_hours": *cfg.DataRetentionSettings.FileRetentionHours,
"deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime,
"batch_size": *cfg.DataRetentionSettings.BatchSize,
"time_between_batches": *cfg.DataRetentionSettings.TimeBetweenBatchesMilliseconds,
Expand Down
68 changes: 64 additions & 4 deletions server/public/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ const (
BleveSettingsDefaultBatchSize = 10000

DataRetentionSettingsDefaultMessageRetentionDays = 365
DataRetentionSettingsDefaultMessageRetentionHours = 0
DataRetentionSettingsDefaultFileRetentionDays = 365
DataRetentionSettingsDefaultFileRetentionHours = 0
DataRetentionSettingsDefaultBoardsRetentionDays = 365
DataRetentionSettingsDefaultDeletionJobStartTime = "02:00"
DataRetentionSettingsDefaultBatchSize = 3000
Expand Down Expand Up @@ -2911,8 +2913,10 @@ type DataRetentionSettings struct {
EnableMessageDeletion *bool `access:"compliance_data_retention_policy"`
EnableFileDeletion *bool `access:"compliance_data_retention_policy"`
EnableBoardsDeletion *bool `access:"compliance_data_retention_policy"`
MessageRetentionDays *int `access:"compliance_data_retention_policy"`
FileRetentionDays *int `access:"compliance_data_retention_policy"`
MessageRetentionDays *int `access:"compliance_data_retention_policy"` // Deprecated: use `MessageRetentionHours`
MessageRetentionHours *int `access:"compliance_data_retention_policy"`
FileRetentionDays *int `access:"compliance_data_retention_policy"` // Deprecated: use `FileRetentionHours`
FileRetentionHours *int `access:"compliance_data_retention_policy"`
BoardsRetentionDays *int `access:"compliance_data_retention_policy"`
DeletionJobStartTime *string `access:"compliance_data_retention_policy"`
BatchSize *int `access:"compliance_data_retention_policy"`
Expand All @@ -2937,10 +2941,18 @@ func (s *DataRetentionSettings) SetDefaults() {
s.MessageRetentionDays = NewInt(DataRetentionSettingsDefaultMessageRetentionDays)
}

if s.MessageRetentionHours == nil {
s.MessageRetentionHours = NewInt(DataRetentionSettingsDefaultMessageRetentionHours)
}

if s.FileRetentionDays == nil {
s.FileRetentionDays = NewInt(DataRetentionSettingsDefaultFileRetentionDays)
}

if s.FileRetentionHours == nil {
s.FileRetentionHours = NewInt(DataRetentionSettingsDefaultFileRetentionHours)
}

if s.BoardsRetentionDays == nil {
s.BoardsRetentionDays = NewInt(DataRetentionSettingsDefaultBoardsRetentionDays)
}
Expand All @@ -2961,6 +2973,30 @@ func (s *DataRetentionSettings) SetDefaults() {
}
}

// GetMessageRetentionHours returns the message retention time as an int.
// MessageRetentionHours takes precedence over the deprecated MessageRetentionDays.
func (s *DataRetentionSettings) GetMessageRetentionHours() int {
if s.MessageRetentionHours != nil && *s.MessageRetentionHours > 0 {
return *s.MessageRetentionHours
}
if s.MessageRetentionDays != nil && *s.MessageRetentionDays > 0 {
return *s.MessageRetentionDays * 24
}
return DataRetentionSettingsDefaultMessageRetentionDays * 24
}

// GetFileRetentionHours returns the message retention time as an int.
// FileRetentionHours takes precedence over the deprecated FileRetentionDays.
func (s *DataRetentionSettings) GetFileRetentionHours() int {
if s.FileRetentionHours != nil && *s.FileRetentionHours > 0 {
return *s.FileRetentionHours
}
if s.FileRetentionDays != nil && *s.FileRetentionDays > 0 {
return *s.FileRetentionDays * 24
}
return DataRetentionSettingsDefaultFileRetentionDays * 24
}

type JobSettings struct {
RunJobs *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
RunScheduler *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
Expand Down Expand Up @@ -4141,14 +4177,38 @@ func (bs *BleveSettings) isValid() *AppError {
}

func (s *DataRetentionSettings) isValid() *AppError {
if *s.MessageRetentionDays <= 0 {
if s.MessageRetentionDays == nil || *s.MessageRetentionDays < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_days_too_low.app_error", nil, "", http.StatusBadRequest)
}

if *s.FileRetentionDays <= 0 {
if s.MessageRetentionHours == nil || *s.MessageRetentionHours < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_hours_too_low.app_error", nil, "", http.StatusBadRequest)
}

if s.FileRetentionDays == nil || *s.FileRetentionDays < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_days_too_low.app_error", nil, "", http.StatusBadRequest)
}

if s.FileRetentionHours == nil || *s.FileRetentionHours < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_hours_too_low.app_error", nil, "", http.StatusBadRequest)
}

if *s.MessageRetentionDays > 0 && *s.MessageRetentionHours > 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_misconfiguration.app_error", nil, "", http.StatusBadRequest)
}

if *s.FileRetentionDays > 0 && *s.FileRetentionHours > 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_misconfiguration.app_error", nil, "", http.StatusBadRequest)
}

if *s.MessageRetentionDays == 0 && *s.MessageRetentionHours == 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.message_retention_both_zero.app_error", nil, "", http.StatusBadRequest)
}

if *s.FileRetentionDays == 0 && *s.FileRetentionHours == 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.file_retention_both_zero.app_error", nil, "", http.StatusBadRequest)
}

if _, err := time.Parse("15:04", *s.DeletionJobStartTime); err != nil {
return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.deletion_job_start_time.app_error", nil, "", http.StatusBadRequest).Wrap(err)
}
Expand Down
102 changes: 102 additions & 0 deletions server/public/model/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1647,3 +1647,105 @@ func TestConfigDefaultCallsPluginState(t *testing.T) {
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.calls"].Enable)
})
}

func TestConfigGetMessageRetentionHours(t *testing.T) {
tests := []struct {
name string
config Config
value int
}{
{
name: "should return MessageRetentionDays config value in hours by default",
config: Config{},
value: 8760,
},
{
name: "should return MessageRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionHours: NewInt(48),
},
},
value: 48,
},
{
name: "should return MessageRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionDays: NewInt(50),
MessageRetentionHours: NewInt(48),
},
},
value: 48,
},
{
name: "should return MessageRetentionDays config value in hours",
config: Config{
DataRetentionSettings: DataRetentionSettings{
MessageRetentionDays: NewInt(50),
MessageRetentionHours: NewInt(0),
},
},
value: 1200,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.config.SetDefaults()

require.Equal(t, test.value, test.config.DataRetentionSettings.GetMessageRetentionHours())
})
}
}

func TestConfigGetFileRetentionHours(t *testing.T) {
tests := []struct {
name string
config Config
value int
}{
{
name: "should return FileRetentionDays config value in hours by default",
config: Config{},
value: 8760,
},
{
name: "should return FileRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionHours: NewInt(48),
},
},
value: 48,
},
{
name: "should return FileRetentionHours config value",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionDays: NewInt(50),
FileRetentionHours: NewInt(48),
},
},
value: 48,
},
{
name: "should return FileRetentionDays config value in hours",
config: Config{
DataRetentionSettings: DataRetentionSettings{
FileRetentionDays: NewInt(50),
FileRetentionHours: NewInt(0),
},
},
value: 1200,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
test.config.SetDefaults()

require.Equal(t, test.value, test.config.DataRetentionSettings.GetFileRetentionHours())
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ describe('components/admin_console/data_retention_settings/data_retention_settin
EnableMessageDeletion: true,
EnableFileDeletion: true,
MessageRetentionDays: 100,
MessageRetentionHours: 2400,
FileRetentionDays: 100,
FileRetentionHours: 2400,
DeletionJobStartTime: '00:15',
},
},
customPolicies: {},
customPoliciesCount: 0,
globalMessageRetentionHours: '2400',
globalFileRetentionHours: '2400',
actions: {
getDataRetentionCustomPolicies: jest.fn().mockResolvedValue([]),
createJob: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type Props = {
config: DeepPartial<AdminConfig>;
customPolicies: DataRetentionCustomPolicies;
customPoliciesCount: number;
globalMessageRetentionHours: string | undefined;
globalFileRetentionHours: string | undefined;
actions: {
getDataRetentionCustomPolicies: (page: number) => Promise<{ data: DataRetentionCustomPolicies }>;
createJob: (job: JobTypeBase) => Promise<{ data: any }>;
Expand Down Expand Up @@ -147,6 +149,52 @@ export default class DataRetentionSettings extends React.PureComponent<Props, St
];
return columns;
};

getGlobalRetentionSetting = (enabled: boolean | undefined, hours: string | undefined): JSX.Element => {
if (!enabled) {
return (
<FormattedMessage
id='admin.data_retention.form.keepForever'
defaultMessage='Keep forever'
/>
);
}
const hoursInt = parseInt(hours || '', 10);
if (hoursInt && hoursInt % 8760 === 0) {
const years = hoursInt / 8760;
return (
<FormattedMessage
id='admin.data_retention.retention_years'
defaultMessage='{count} {count, plural, one {year} other {years}}'
values={{
count: `${years}`,
}}
/>
);
}
if (hoursInt && hoursInt % 24 === 0) {
const days = hoursInt / 24;
return (
<FormattedMessage
id='admin.data_retention.retention_days'
defaultMessage='{count} {count, plural, one {day} other {days}}'
values={{
count: `${days}`,
}}
/>
);
}

return (
<FormattedMessage
id='admin.data_retention.retention_hours'
defaultMessage='{count} {count, plural, one {hour} other {hours}}'
values={{
count: `${hours}`,
}}
/>
);
};
getMessageRetentionSetting = (enabled: boolean | undefined, days: number | undefined): JSX.Element => {
if (!enabled) {
return (
Expand Down Expand Up @@ -185,12 +233,12 @@ export default class DataRetentionSettings extends React.PureComponent<Props, St
description: Utils.localizeMessage('admin.data_retention.form.text', 'Applies to all teams and channels, but does not apply to custom retention policies.'),
channel_messages: (
<div data-testid='global_message_retention_cell'>
{this.getMessageRetentionSetting(DataRetentionSettings?.EnableMessageDeletion, DataRetentionSettings?.MessageRetentionDays)}
{this.getGlobalRetentionSetting(DataRetentionSettings?.EnableMessageDeletion, this.props.globalMessageRetentionHours)}
</div>
),
files: (
<div data-testid='global_file_retention_cell'>
{this.getMessageRetentionSetting(DataRetentionSettings?.EnableFileDeletion, DataRetentionSettings?.FileRetentionDays)}
{this.getGlobalRetentionSetting(DataRetentionSettings?.EnableFileDeletion, this.props.globalFileRetentionHours)}
</div>
),
actions: (
Expand Down
Loading

0 comments on commit bb88b92

Please sign in to comment.