[MM-53990] Support a global retention time of less than 1 day (#25196)

* adding new MessageRetentionHours config

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Ben Cooke
2024-01-09 14:10:22 -05:00
коммит произвёл GitHub
родитель 82b8d4dc07
Коммит bb88b92b4c
18 изменённых файлов: 376 добавлений и 47 удалений

Просмотреть файл

@@ -611,7 +611,9 @@ const defaultServerConfig: AdminConfig = {
EnableFileDeletion: false, EnableFileDeletion: false,
EnableBoardsDeletion: false, EnableBoardsDeletion: false,
MessageRetentionDays: 365, MessageRetentionDays: 365,
MessageRetentionHours: 0,
FileRetentionDays: 365, FileRetentionDays: 365,
FileRetentionHours: 0,
BoardsRetentionDays: 365, BoardsRetentionDays: 365,
DeletionJobStartTime: '02:00', DeletionJobStartTime: '02:00',
BatchSize: 3000, BatchSize: 3000,

Просмотреть файл

@@ -122,9 +122,9 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
props["AllowCustomThemes"] = "true" props["AllowCustomThemes"] = "true"
props["AllowedThemes"] = "" props["AllowedThemes"] = ""
props["DataRetentionEnableMessageDeletion"] = "false" props["DataRetentionEnableMessageDeletion"] = "false"
props["DataRetentionMessageRetentionDays"] = "0" props["DataRetentionMessageRetentionHours"] = "0"
props["DataRetentionEnableFileDeletion"] = "false" props["DataRetentionEnableFileDeletion"] = "false"
props["DataRetentionFileRetentionDays"] = "0" props["DataRetentionFileRetentionHours"] = "0"
props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomURLSchemes, ",") props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomURLSchemes, ",")
props["MaxMarkdownNodes"] = strconv.FormatInt(int64(*c.DisplaySettings.MaxMarkdownNodes), 10) props["MaxMarkdownNodes"] = strconv.FormatInt(int64(*c.DisplaySettings.MaxMarkdownNodes), 10)
@@ -204,9 +204,9 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
if *license.Features.DataRetention { if *license.Features.DataRetention {
props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion) 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["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() { if license.HasSharedChannels() {

Просмотреть файл

@@ -8742,13 +8742,37 @@
"id": "model.config.is_valid.data_retention.deletion_job_start_time.app_error", "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." "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", "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", "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", "id": "model.config.is_valid.directory.app_error",

Просмотреть файл

@@ -829,7 +829,9 @@ func (ts *TelemetryService) trackConfig() {
"enable_message_deletion": *cfg.DataRetentionSettings.EnableMessageDeletion, "enable_message_deletion": *cfg.DataRetentionSettings.EnableMessageDeletion,
"enable_file_deletion": *cfg.DataRetentionSettings.EnableFileDeletion, "enable_file_deletion": *cfg.DataRetentionSettings.EnableFileDeletion,
"message_retention_days": *cfg.DataRetentionSettings.MessageRetentionDays, "message_retention_days": *cfg.DataRetentionSettings.MessageRetentionDays,
"message_retention_hours": *cfg.DataRetentionSettings.MessageRetentionHours,
"file_retention_days": *cfg.DataRetentionSettings.FileRetentionDays, "file_retention_days": *cfg.DataRetentionSettings.FileRetentionDays,
"file_retention_hours": *cfg.DataRetentionSettings.FileRetentionHours,
"deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime, "deletion_job_start_time": *cfg.DataRetentionSettings.DeletionJobStartTime,
"batch_size": *cfg.DataRetentionSettings.BatchSize, "batch_size": *cfg.DataRetentionSettings.BatchSize,
"time_between_batches": *cfg.DataRetentionSettings.TimeBetweenBatchesMilliseconds, "time_between_batches": *cfg.DataRetentionSettings.TimeBetweenBatchesMilliseconds,

Просмотреть файл

@@ -207,7 +207,9 @@ const (
BleveSettingsDefaultBatchSize = 10000 BleveSettingsDefaultBatchSize = 10000
DataRetentionSettingsDefaultMessageRetentionDays = 365 DataRetentionSettingsDefaultMessageRetentionDays = 365
DataRetentionSettingsDefaultMessageRetentionHours = 0
DataRetentionSettingsDefaultFileRetentionDays = 365 DataRetentionSettingsDefaultFileRetentionDays = 365
DataRetentionSettingsDefaultFileRetentionHours = 0
DataRetentionSettingsDefaultBoardsRetentionDays = 365 DataRetentionSettingsDefaultBoardsRetentionDays = 365
DataRetentionSettingsDefaultDeletionJobStartTime = "02:00" DataRetentionSettingsDefaultDeletionJobStartTime = "02:00"
DataRetentionSettingsDefaultBatchSize = 3000 DataRetentionSettingsDefaultBatchSize = 3000
@@ -2911,8 +2913,10 @@ type DataRetentionSettings struct {
EnableMessageDeletion *bool `access:"compliance_data_retention_policy"` EnableMessageDeletion *bool `access:"compliance_data_retention_policy"`
EnableFileDeletion *bool `access:"compliance_data_retention_policy"` EnableFileDeletion *bool `access:"compliance_data_retention_policy"`
EnableBoardsDeletion *bool `access:"compliance_data_retention_policy"` EnableBoardsDeletion *bool `access:"compliance_data_retention_policy"`
MessageRetentionDays *int `access:"compliance_data_retention_policy"` MessageRetentionDays *int `access:"compliance_data_retention_policy"` // Deprecated: use `MessageRetentionHours`
FileRetentionDays *int `access:"compliance_data_retention_policy"` 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"` BoardsRetentionDays *int `access:"compliance_data_retention_policy"`
DeletionJobStartTime *string `access:"compliance_data_retention_policy"` DeletionJobStartTime *string `access:"compliance_data_retention_policy"`
BatchSize *int `access:"compliance_data_retention_policy"` BatchSize *int `access:"compliance_data_retention_policy"`
@@ -2937,10 +2941,18 @@ func (s *DataRetentionSettings) SetDefaults() {
s.MessageRetentionDays = NewInt(DataRetentionSettingsDefaultMessageRetentionDays) s.MessageRetentionDays = NewInt(DataRetentionSettingsDefaultMessageRetentionDays)
} }
if s.MessageRetentionHours == nil {
s.MessageRetentionHours = NewInt(DataRetentionSettingsDefaultMessageRetentionHours)
}
if s.FileRetentionDays == nil { if s.FileRetentionDays == nil {
s.FileRetentionDays = NewInt(DataRetentionSettingsDefaultFileRetentionDays) s.FileRetentionDays = NewInt(DataRetentionSettingsDefaultFileRetentionDays)
} }
if s.FileRetentionHours == nil {
s.FileRetentionHours = NewInt(DataRetentionSettingsDefaultFileRetentionHours)
}
if s.BoardsRetentionDays == nil { if s.BoardsRetentionDays == nil {
s.BoardsRetentionDays = NewInt(DataRetentionSettingsDefaultBoardsRetentionDays) s.BoardsRetentionDays = NewInt(DataRetentionSettingsDefaultBoardsRetentionDays)
} }
@@ -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 { type JobSettings struct {
RunJobs *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none RunJobs *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
RunScheduler *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none RunScheduler *bool `access:"write_restrictable,cloud_restrictable"` // telemetry: none
@@ -4141,14 +4177,38 @@ func (bs *BleveSettings) isValid() *AppError {
} }
func (s *DataRetentionSettings) 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) 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) 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 { 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) return NewAppError("Config.IsValid", "model.config.is_valid.data_retention.deletion_job_start_time.app_error", nil, "", http.StatusBadRequest).Wrap(err)
} }

Просмотреть файл

@@ -1647,3 +1647,105 @@ func TestConfigDefaultCallsPluginState(t *testing.T) {
assert.False(t, c1.PluginSettings.PluginStates["com.mattermost.calls"].Enable) 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())
})
}
}

Просмотреть файл

@@ -13,12 +13,16 @@ describe('components/admin_console/data_retention_settings/data_retention_settin
EnableMessageDeletion: true, EnableMessageDeletion: true,
EnableFileDeletion: true, EnableFileDeletion: true,
MessageRetentionDays: 100, MessageRetentionDays: 100,
MessageRetentionHours: 2400,
FileRetentionDays: 100, FileRetentionDays: 100,
FileRetentionHours: 2400,
DeletionJobStartTime: '00:15', DeletionJobStartTime: '00:15',
}, },
}, },
customPolicies: {}, customPolicies: {},
customPoliciesCount: 0, customPoliciesCount: 0,
globalMessageRetentionHours: '2400',
globalFileRetentionHours: '2400',
actions: { actions: {
getDataRetentionCustomPolicies: jest.fn().mockResolvedValue([]), getDataRetentionCustomPolicies: jest.fn().mockResolvedValue([]),
createJob: jest.fn(), createJob: jest.fn(),

Просмотреть файл

@@ -37,6 +37,8 @@ type Props = {
config: DeepPartial<AdminConfig>; config: DeepPartial<AdminConfig>;
customPolicies: DataRetentionCustomPolicies; customPolicies: DataRetentionCustomPolicies;
customPoliciesCount: number; customPoliciesCount: number;
globalMessageRetentionHours: string | undefined;
globalFileRetentionHours: string | undefined;
actions: { actions: {
getDataRetentionCustomPolicies: (page: number) => Promise<{ data: DataRetentionCustomPolicies }>; getDataRetentionCustomPolicies: (page: number) => Promise<{ data: DataRetentionCustomPolicies }>;
createJob: (job: JobTypeBase) => Promise<{ data: any }>; createJob: (job: JobTypeBase) => Promise<{ data: any }>;
@@ -147,6 +149,52 @@ export default class DataRetentionSettings extends React.PureComponent<Props, St
]; ];
return columns; 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 => { getMessageRetentionSetting = (enabled: boolean | undefined, days: number | undefined): JSX.Element => {
if (!enabled) { if (!enabled) {
return ( return (
@@ -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.'), description: Utils.localizeMessage('admin.data_retention.form.text', 'Applies to all teams and channels, but does not apply to custom retention policies.'),
channel_messages: ( channel_messages: (
<div data-testid='global_message_retention_cell'> <div data-testid='global_message_retention_cell'>
{this.getMessageRetentionSetting(DataRetentionSettings?.EnableMessageDeletion, DataRetentionSettings?.MessageRetentionDays)} {this.getGlobalRetentionSetting(DataRetentionSettings?.EnableMessageDeletion, this.props.globalMessageRetentionHours)}
</div> </div>
), ),
files: ( files: (
<div data-testid='global_file_retention_cell'> <div data-testid='global_file_retention_cell'>
{this.getMessageRetentionSetting(DataRetentionSettings?.EnableFileDeletion, DataRetentionSettings?.FileRetentionDays)} {this.getGlobalRetentionSetting(DataRetentionSettings?.EnableFileDeletion, this.props.globalFileRetentionHours)}
</div> </div>
), ),
actions: ( actions: (

Просмотреть файл

@@ -8,6 +8,8 @@ import * as Utils from 'utils/utils';
export const FOREVER = 'FOREVER'; export const FOREVER = 'FOREVER';
export const YEARS = 'YEARS'; export const YEARS = 'YEARS';
export const DAYS = 'DAYS'; export const DAYS = 'DAYS';
export const HOURS = 'HOURS';
export const keepForeverOption = () => ({value: FOREVER, label: <div><i className='icon icon-infinity option-icon'/><span className='option_forever'>{Utils.localizeMessage('admin.data_retention.form.keepForever', 'Keep forever')}</span></div>}); export const keepForeverOption = () => ({value: FOREVER, label: <div><i className='icon icon-infinity option-icon'/><span className='option_forever'>{Utils.localizeMessage('admin.data_retention.form.keepForever', 'Keep forever')}</span></div>});
export const yearsOption = () => ({value: YEARS, label: <span className='option_years'>{Utils.localizeMessage('admin.data_retention.form.years', 'Years')}</span>}); export const yearsOption = () => ({value: YEARS, label: <span className='option_years'>{Utils.localizeMessage('admin.data_retention.form.years', 'Years')}</span>});
export const daysOption = () => ({value: DAYS, label: <span className='option_days'>{Utils.localizeMessage('admin.data_retention.form.days', 'Days')}</span>}); export const daysOption = () => ({value: DAYS, label: <span className='option_days'>{Utils.localizeMessage('admin.data_retention.form.days', 'Days')}</span>});
export const hoursOption = () => ({value: HOURS, label: <span className='option_hours'>{Utils.localizeMessage('admin.data_retention.form.hours', 'Hours')}</span>});

Просмотреть файл

@@ -62,13 +62,21 @@ exports[`components/PluginManagement should match snapshot 1`] = `
} }
inputId="channel_message_retention_input" inputId="channel_message_retention_input"
inputType="number" inputType="number"
inputValue="60" inputValue="100"
legend="Channel & direct message retention" legend="Channel & direct message retention"
name="channel_message_retention" name="channel_message_retention"
onDropdownChange={[Function]} onDropdownChange={[Function]}
onInputChange={[Function]} onInputChange={[Function]}
options={ options={
Array [ Array [
Object {
"label": <span
className="option_hours"
>
Hours
</span>,
"value": "HOURS",
},
Object { Object {
"label": <span "label": <span
className="option_days" className="option_days"
@@ -141,13 +149,21 @@ exports[`components/PluginManagement should match snapshot 1`] = `
} }
inputId="file_retention_input" inputId="file_retention_input"
inputType="number" inputType="number"
inputValue="40" inputValue="100"
legend="File retention" legend="File retention"
name="file_retention" name="file_retention"
onDropdownChange={[Function]} onDropdownChange={[Function]}
onInputChange={[Function]} onInputChange={[Function]}
options={ options={
Array [ Array [
Object {
"label": <span
className="option_hours"
>
Hours
</span>,
"value": "HOURS",
},
Object { Object {
"label": <span "label": <span
className="option_days" className="option_days"

Просмотреть файл

@@ -48,6 +48,12 @@
background-color: var(--sys-center-channel-bg); background-color: var(--sys-center-channel-bg);
color: rgba(var(--sys-center-channel-color-rgb), 0.64); color: rgba(var(--sys-center-channel-color-rgb), 0.64);
} }
.alert {
&.alert-warning {
margin-top: 10px;
}
}
} }
.CustomPolicy__error { .CustomPolicy__error {

Просмотреть файл

@@ -12,11 +12,14 @@ describe('components/PluginManagement', () => {
DataRetentionSettings: { DataRetentionSettings: {
EnableMessageDeletion: true, EnableMessageDeletion: true,
EnableFileDeletion: true, EnableFileDeletion: true,
MessageRetentionDays: 60, MessageRetentionHours: 1440,
FileRetentionDays: 40, FileRetentionHours: 960,
DeletionJobStartTime: '10:00', DeletionJobStartTime: '10:00',
}, },
}, },
messageRetentionHours: '2400',
fileRetentionHours: '2400',
environmentConfig: {},
actions: { actions: {
updateConfig: jest.fn(), updateConfig: jest.fn(),
setNavigationBlocked: jest.fn(), setNavigationBlocked: jest.fn(),

Просмотреть файл

@@ -4,12 +4,13 @@
import React from 'react'; import React from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import type {AdminConfig} from '@mattermost/types/config'; import type {AdminConfig, EnvironmentConfig} from '@mattermost/types/config';
import type {ServerError} from '@mattermost/types/errors'; import type {ServerError} from '@mattermost/types/errors';
import type {DeepPartial} from '@mattermost/types/utilities'; import type {DeepPartial} from '@mattermost/types/utilities';
import BlockableLink from 'components/admin_console/blockable_link'; import BlockableLink from 'components/admin_console/blockable_link';
import {keepForeverOption, yearsOption, daysOption, FOREVER, YEARS, DAYS} from 'components/admin_console/data_retention_settings/dropdown_options/dropdown_options'; import {keepForeverOption, yearsOption, daysOption, FOREVER, YEARS, DAYS, hoursOption} from 'components/admin_console/data_retention_settings/dropdown_options/dropdown_options';
import SetByEnv from 'components/admin_console/set_by_env';
import Card from 'components/card/card'; import Card from 'components/card/card';
import SaveButton from 'components/save_button'; import SaveButton from 'components/save_button';
import AdminHeader from 'components/widgets/admin_console/admin_header'; import AdminHeader from 'components/widgets/admin_console/admin_header';
@@ -26,6 +27,9 @@ type ValueType = {
} }
type Props = { type Props = {
config: DeepPartial<AdminConfig>; config: DeepPartial<AdminConfig>;
messageRetentionHours: string | undefined;
fileRetentionHours: string | undefined;
environmentConfig: Partial<EnvironmentConfig>;
actions: { actions: {
updateConfig: (config: Record<string, any>) => Promise<{ data?: AdminConfig; error?: ServerError }>; updateConfig: (config: Record<string, any>) => Promise<{ data?: AdminConfig; error?: ServerError }>;
setNavigationBlocked: (blocked: boolean) => void; setNavigationBlocked: (blocked: boolean) => void;
@@ -51,30 +55,44 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
saving: false, saving: false,
serverError: null, serverError: null,
formErrorText: '', formErrorText: '',
messageRetentionDropdownValue: this.getDefaultDropdownValue(DataRetentionSettings?.EnableMessageDeletion, DataRetentionSettings?.MessageRetentionDays), messageRetentionDropdownValue: this.getDefaultDropdownValue(DataRetentionSettings?.EnableMessageDeletion, props.messageRetentionHours),
messageRetentionInputValue: this.getDefaultInputValue(DataRetentionSettings?.EnableMessageDeletion, DataRetentionSettings?.MessageRetentionDays), messageRetentionInputValue: this.getDefaultInputValue(DataRetentionSettings?.EnableMessageDeletion, props.messageRetentionHours),
fileRetentionDropdownValue: this.getDefaultDropdownValue(DataRetentionSettings?.EnableFileDeletion, DataRetentionSettings?.FileRetentionDays), fileRetentionDropdownValue: this.getDefaultDropdownValue(DataRetentionSettings?.EnableFileDeletion, props.fileRetentionHours),
fileRetentionInputValue: this.getDefaultInputValue(DataRetentionSettings?.EnableFileDeletion, DataRetentionSettings?.FileRetentionDays), fileRetentionInputValue: this.getDefaultInputValue(DataRetentionSettings?.EnableFileDeletion, props.fileRetentionHours),
}; };
} }
getDefaultInputValue = (isEnabled: boolean | undefined, days: number | undefined): string => { getDefaultInputValue = (isEnabled: boolean | undefined, hours: string | undefined): string => {
if (!isEnabled || days === undefined) { if (!isEnabled || hours === undefined) {
return ''; return '';
} }
if (days % 365 === 0) { const hoursInt = parseInt(hours, 10);
return (days / 365).toString();
// 8760 hours in a year
if (hoursInt % 8760 === 0) {
return (hoursInt / 8760).toString();
} }
return days.toString(); if (hoursInt % 24 === 0) {
return (hoursInt / 24).toString();
}
return hours.toString();
}; };
getDefaultDropdownValue = (isEnabled: boolean | undefined, days: number | undefined) => { getDefaultDropdownValue = (isEnabled: boolean | undefined, hours: string | undefined) => {
if (!isEnabled || days === undefined) { if (!isEnabled || hours === undefined) {
return keepForeverOption(); return keepForeverOption();
} }
if (days % 365 === 0) { const hoursInt = parseInt(hours, 10);
// 8760 hours in a year
if (hoursInt % 8760 === 0) {
return yearsOption(); return yearsOption();
} }
return daysOption(); if (hoursInt % 24 === 0) {
return daysOption();
}
return hoursOption();
}; };
handleSubmit = async () => { handleSubmit = async () => {
@@ -90,16 +108,16 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
newConfig.DataRetentionSettings.EnableMessageDeletion = this.setDeletionEnabled(messageRetentionDropdownValue.value); newConfig.DataRetentionSettings.EnableMessageDeletion = this.setDeletionEnabled(messageRetentionDropdownValue.value);
const messageDays = this.setRetentionDays(messageRetentionDropdownValue.value, messageRetentionInputValue); if (!this.isMessageRetentionSetByEnv() && this.setDeletionEnabled(messageRetentionDropdownValue.value)) {
if (messageDays >= 1) { newConfig.DataRetentionSettings.MessageRetentionDays = 0;
newConfig.DataRetentionSettings.MessageRetentionDays = messageDays; newConfig.DataRetentionSettings.MessageRetentionHours = this.setRetentionHours(messageRetentionDropdownValue.value, messageRetentionInputValue);
} }
newConfig.DataRetentionSettings.EnableFileDeletion = this.setDeletionEnabled(fileRetentionDropdownValue.value); newConfig.DataRetentionSettings.EnableFileDeletion = this.setDeletionEnabled(fileRetentionDropdownValue.value);
const fileDays = this.setRetentionDays(fileRetentionDropdownValue.value, fileRetentionInputValue); if (!this.isFileRetentionSetByEnv() && this.setDeletionEnabled(fileRetentionDropdownValue.value)) {
if (fileDays >= 1) { newConfig.DataRetentionSettings.FileRetentionDays = 0;
newConfig.DataRetentionSettings.FileRetentionDays = fileDays; newConfig.DataRetentionSettings.FileRetentionHours = this.setRetentionHours(fileRetentionDropdownValue.value, fileRetentionInputValue);
} }
const {error} = await this.props.actions.updateConfig(newConfig); const {error} = await this.props.actions.updateConfig(newConfig);
@@ -119,16 +137,26 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
return true; return true;
}; };
setRetentionDays = (dropdownValue: string, value: string): number => { setRetentionHours = (dropdownValue: string, value: string): number => {
if (dropdownValue === YEARS) { if (dropdownValue === YEARS) {
return parseInt(value, 10) * 365; return parseInt(value, 10) * 24 * 365;
} }
if (dropdownValue === DAYS) { if (dropdownValue === DAYS) {
return parseInt(value, 10); return parseInt(value, 10) * 24;
} }
return parseInt(value, 10);
};
return 0; isMessageRetentionSetByEnv = () => {
return (this.props.environmentConfig?.DataRetentionSettings?.MessageRetentionDays && this.props.config.DataRetentionSettings?.MessageRetentionDays && this.props.config.DataRetentionSettings.MessageRetentionDays > 0) ||
(this.props.environmentConfig?.DataRetentionSettings?.MessageRetentionHours && this.props.config.DataRetentionSettings?.MessageRetentionHours && this.props.config.DataRetentionSettings.MessageRetentionHours > 0) ||
(this.props.environmentConfig?.DataRetentionSettings?.EnableMessageDeletion && !this.props.config.DataRetentionSettings?.EnableMessageDeletion);
};
isFileRetentionSetByEnv = () => {
return (this.props.environmentConfig?.DataRetentionSettings?.FileRetentionDays && this.props.config.DataRetentionSettings?.FileRetentionDays && this.props.config.DataRetentionSettings.FileRetentionDays > 0) ||
(this.props.environmentConfig?.DataRetentionSettings?.FileRetentionHours && this.props.config.DataRetentionSettings?.FileRetentionHours && this.props.config.DataRetentionSettings.FileRetentionHours > 0) ||
(this.props.environmentConfig?.DataRetentionSettings?.EnableFileDeletion && !this.props.config.DataRetentionSettings?.EnableFileDeletion);
}; };
render = () => { render = () => {
@@ -173,8 +201,9 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
inputValue={this.state.messageRetentionInputValue} inputValue={this.state.messageRetentionInputValue}
width={90} width={90}
exceptionToInput={[FOREVER]} exceptionToInput={[FOREVER]}
disabled={this.isMessageRetentionSetByEnv()}
defaultValue={keepForeverOption()} defaultValue={keepForeverOption()}
options={[daysOption(), yearsOption(), keepForeverOption()]} options={[hoursOption(), daysOption(), yearsOption(), keepForeverOption()]}
legend={Utils.localizeMessage('admin.data_retention.form.channelAndDirectMessageRetention', 'Channel & direct message retention')} legend={Utils.localizeMessage('admin.data_retention.form.channelAndDirectMessageRetention', 'Channel & direct message retention')}
placeholder={Utils.localizeMessage('admin.data_retention.form.channelAndDirectMessageRetention', 'Channel & direct message retention')} placeholder={Utils.localizeMessage('admin.data_retention.form.channelAndDirectMessageRetention', 'Channel & direct message retention')}
name={'channel_message_retention'} name={'channel_message_retention'}
@@ -182,6 +211,7 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
dropdownClassNamePrefix={'channel_message_retention_dropdown'} dropdownClassNamePrefix={'channel_message_retention_dropdown'}
inputId={'channel_message_retention_input'} inputId={'channel_message_retention_input'}
/> />
{this.isMessageRetentionSetByEnv() && <SetByEnv/>}
</div> </div>
<div id='global_file_dropdown'> <div id='global_file_dropdown'>
<DropdownInputHybrid <DropdownInputHybrid
@@ -199,8 +229,9 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
inputValue={this.state.fileRetentionInputValue} inputValue={this.state.fileRetentionInputValue}
width={90} width={90}
exceptionToInput={[FOREVER]} exceptionToInput={[FOREVER]}
disabled={this.isFileRetentionSetByEnv()}
defaultValue={keepForeverOption()} defaultValue={keepForeverOption()}
options={[daysOption(), yearsOption(), keepForeverOption()]} options={[hoursOption(), daysOption(), yearsOption(), keepForeverOption()]}
legend={Utils.localizeMessage('admin.data_retention.form.fileRetention', 'File retention')} legend={Utils.localizeMessage('admin.data_retention.form.fileRetention', 'File retention')}
placeholder={Utils.localizeMessage('admin.data_retention.form.fileRetention', 'File retention')} placeholder={Utils.localizeMessage('admin.data_retention.form.fileRetention', 'File retention')}
name={'file_retention'} name={'file_retention'}
@@ -208,6 +239,7 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
dropdownClassNamePrefix={'file_retention_dropdown'} dropdownClassNamePrefix={'file_retention_dropdown'}
inputId={'file_retention_input'} inputId={'file_retention_input'}
/> />
{this.isFileRetentionSetByEnv() && <SetByEnv/>}
</div> </div>
</div> </div>

Просмотреть файл

@@ -11,10 +11,14 @@ import type {ServerError} from '@mattermost/types/errors';
import { import {
updateConfig, updateConfig,
} from 'mattermost-redux/actions/admin'; } from 'mattermost-redux/actions/admin';
import {getEnvironmentConfig} from 'mattermost-redux/selectors/entities/admin';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import type {GenericAction, ActionFunc} from 'mattermost-redux/types/actions'; import type {GenericAction, ActionFunc} from 'mattermost-redux/types/actions';
import {setNavigationBlocked} from 'actions/admin_actions.jsx'; import {setNavigationBlocked} from 'actions/admin_actions.jsx';
import type {GlobalState} from 'types/store';
import GlobalPolicyForm from './global_policy_form'; import GlobalPolicyForm from './global_policy_form';
type Actions = { type Actions = {
@@ -22,6 +26,17 @@ type Actions = {
setNavigationBlocked: (blocked: boolean) => void; setNavigationBlocked: (blocked: boolean) => void;
}; };
function mapStateToProps(state: GlobalState) {
const messageRetentionHours = getConfig(state).DataRetentionMessageRetentionHours;
const fileRetentionHours = getConfig(state).DataRetentionFileRetentionHours;
return {
messageRetentionHours,
fileRetentionHours,
environmentConfig: getEnvironmentConfig(state),
};
}
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) { function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
return { return {
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({ actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
@@ -31,4 +46,4 @@ function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
}; };
} }
export default connect(null, mapDispatchToProps)(GlobalPolicyForm); export default connect(mapStateToProps, mapDispatchToProps)(GlobalPolicyForm);

Просмотреть файл

@@ -11,6 +11,7 @@ import type {JobTypeBase, JobType} from '@mattermost/types/jobs';
import {getDataRetentionCustomPolicies as fetchDataRetentionCustomPolicies, deleteDataRetentionCustomPolicy, updateConfig} from 'mattermost-redux/actions/admin'; import {getDataRetentionCustomPolicies as fetchDataRetentionCustomPolicies, deleteDataRetentionCustomPolicy, updateConfig} from 'mattermost-redux/actions/admin';
import {createJob, getJobsByType} from 'mattermost-redux/actions/jobs'; import {createJob, getJobsByType} from 'mattermost-redux/actions/jobs';
import {getDataRetentionCustomPolicies, getDataRetentionCustomPoliciesCount} from 'mattermost-redux/selectors/entities/admin'; import {getDataRetentionCustomPolicies, getDataRetentionCustomPoliciesCount} from 'mattermost-redux/selectors/entities/admin';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import type {GenericAction, ActionFunc, ActionResult} from 'mattermost-redux/types/actions'; import type {GenericAction, ActionFunc, ActionResult} from 'mattermost-redux/types/actions';
import type {GlobalState} from 'types/store'; import type {GlobalState} from 'types/store';
@@ -28,10 +29,14 @@ type Actions = {
function mapStateToProps(state: GlobalState) { function mapStateToProps(state: GlobalState) {
const customPolicies = getDataRetentionCustomPolicies(state); const customPolicies = getDataRetentionCustomPolicies(state);
const customPoliciesCount = getDataRetentionCustomPoliciesCount(state); const customPoliciesCount = getDataRetentionCustomPoliciesCount(state);
const globalMessageRetentionHours = getConfig(state).DataRetentionMessageRetentionHours;
const globalFileRetentionHours = getConfig(state).DataRetentionFileRetentionHours;
return { return {
customPolicies, customPolicies,
customPoliciesCount, customPoliciesCount,
globalMessageRetentionHours,
globalFileRetentionHours,
}; };
} }

Просмотреть файл

@@ -44,6 +44,8 @@ const baseStyles = {
boxShadow: 'none', boxShadow: 'none',
padding: '0 2px', padding: '0 2px',
cursor: 'pointer', cursor: 'pointer',
minHeight: '40px',
borderRadius: '0',
}), }),
indicatorSeparator: (provided: CSSProperties) => ({ indicatorSeparator: (provided: CSSProperties) => ({
...provided, ...provided,
@@ -206,6 +208,7 @@ const DropdownInputHybrid = <T extends OptionType = OptionType>(props: Props<T>)
className={classNames('Input form-control')} className={classNames('Input form-control')}
ref={inputRef} ref={inputRef}
id={inputId} id={inputId}
disabled={props.disabled}
/> />
</div> </div>
<div <div
@@ -232,6 +235,7 @@ const DropdownInputHybrid = <T extends OptionType = OptionType>(props: Props<T>)
hideSelectedOptions={true} hideSelectedOptions={true}
isSearchable={false} isSearchable={false}
menuPortalTarget={document.body} menuPortalTarget={document.body}
isDisabled={props.disabled}
{...otherProps} {...otherProps}
/> />
</div> </div>

Просмотреть файл

@@ -722,6 +722,7 @@
"admin.data_retention.form.channelAndDirectMessageRetention": "Channel & direct message retention", "admin.data_retention.form.channelAndDirectMessageRetention": "Channel & direct message retention",
"admin.data_retention.form.days": "Days", "admin.data_retention.form.days": "Days",
"admin.data_retention.form.fileRetention": "File retention", "admin.data_retention.form.fileRetention": "File retention",
"admin.data_retention.form.hours": "Hours",
"admin.data_retention.form.keepForever": "Keep forever", "admin.data_retention.form.keepForever": "Keep forever",
"admin.data_retention.form.text": "Applies to all teams and channels, but does not apply to custom retention policies.", "admin.data_retention.form.text": "Applies to all teams and channels, but does not apply to custom retention policies.",
"admin.data_retention.form.years": "Years", "admin.data_retention.form.years": "Years",
@@ -739,6 +740,7 @@
"admin.data_retention.jobTimeAM": "{time} AM (UTC)", "admin.data_retention.jobTimeAM": "{time} AM (UTC)",
"admin.data_retention.jobTimePM": "{time} PM (UTC)", "admin.data_retention.jobTimePM": "{time} PM (UTC)",
"admin.data_retention.retention_days": "{count} {count, plural, one {day} other {days}}", "admin.data_retention.retention_days": "{count} {count, plural, one {day} other {days}}",
"admin.data_retention.retention_hours": "{count} {count, plural, one {hour} other {hours}}",
"admin.data_retention.retention_years": "{count} {count, plural, one {year} other {years}}", "admin.data_retention.retention_years": "{count} {count, plural, one {year} other {years}}",
"admin.data_retention.settings.title": "Data Retention Policies", "admin.data_retention.settings.title": "Data Retention Policies",
"admin.data_retention.title": "Data Retention Policy", "admin.data_retention.title": "Data Retention Policy",

Просмотреть файл

@@ -33,8 +33,8 @@ export type ClientConfig = {
CWSMock: string; CWSMock: string;
DataRetentionEnableFileDeletion: string; DataRetentionEnableFileDeletion: string;
DataRetentionEnableMessageDeletion: string; DataRetentionEnableMessageDeletion: string;
DataRetentionFileRetentionDays: string; DataRetentionFileRetentionHours: string;
DataRetentionMessageRetentionDays: string; DataRetentionMessageRetentionHours: string;
DefaultClientLocale: string; DefaultClientLocale: string;
DefaultTheme: string; DefaultTheme: string;
DiagnosticId: string; DiagnosticId: string;
@@ -828,7 +828,9 @@ export type DataRetentionSettings = {
EnableMessageDeletion: boolean; EnableMessageDeletion: boolean;
EnableFileDeletion: boolean; EnableFileDeletion: boolean;
MessageRetentionDays: number; MessageRetentionDays: number;
MessageRetentionHours: number;
FileRetentionDays: number; FileRetentionDays: number;
FileRetentionHours: number;
DeletionJobStartTime: string; DeletionJobStartTime: string;
BatchSize: number; BatchSize: number;
EnableBoardsDeletion: boolean, EnableBoardsDeletion: boolean,