[MM-57418] Improvements to custom plugin config sections (#27831)
* Improve rendering of plugin custom admin sections * Tests * UX review * Update translations * Review logic
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ee3558079a
Коммит
061ff0fad6
@@ -0,0 +1,268 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type {match} from 'react-router-dom';
|
||||||
|
|
||||||
|
import type {CloudState} from '@mattermost/types/cloud';
|
||||||
|
import type {PluginSettings} from '@mattermost/types/config';
|
||||||
|
|
||||||
|
import CustomPluginSettings from 'components/admin_console/custom_plugin_settings';
|
||||||
|
|
||||||
|
import {screen, renderWithContext} from 'tests/react_testing_utils';
|
||||||
|
|
||||||
|
describe('custom plugin sections and settings', () => {
|
||||||
|
const plugin = {
|
||||||
|
id: 'testplugin',
|
||||||
|
name: 'testplugin',
|
||||||
|
description: '',
|
||||||
|
version: '',
|
||||||
|
active: true,
|
||||||
|
webapp: {
|
||||||
|
bundle_path: '/static/testplugin_bundle.js',
|
||||||
|
},
|
||||||
|
settings_schema: {
|
||||||
|
header: 'This is the header',
|
||||||
|
footer: 'This is the footer',
|
||||||
|
settings: [],
|
||||||
|
sections: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
isDisabled: false,
|
||||||
|
environmentConfig: {},
|
||||||
|
setNavigationBlocked: jest.fn(),
|
||||||
|
roles: {},
|
||||||
|
cloud: {} as CloudState,
|
||||||
|
license: {},
|
||||||
|
editRole: jest.fn(),
|
||||||
|
isCurrentUserSystemAdmin: false,
|
||||||
|
enterpriseReady: false,
|
||||||
|
match: {params: {plugin_id: 'testplugin'}} as match<{ plugin_id: string }>,
|
||||||
|
config: {
|
||||||
|
PluginSettings: {
|
||||||
|
Plugins: {
|
||||||
|
testplugin: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as PluginSettings,
|
||||||
|
},
|
||||||
|
consoleAccess: {
|
||||||
|
read: {
|
||||||
|
about: true,
|
||||||
|
reporting: true,
|
||||||
|
environment: true,
|
||||||
|
site_configuration: true,
|
||||||
|
authentication: true,
|
||||||
|
plugins: true,
|
||||||
|
integrations: true,
|
||||||
|
compliance: true,
|
||||||
|
},
|
||||||
|
write: {
|
||||||
|
about: true,
|
||||||
|
reporting: true,
|
||||||
|
environment: true,
|
||||||
|
site_configuration: true,
|
||||||
|
authentication: true,
|
||||||
|
plugins: true,
|
||||||
|
integrations: true,
|
||||||
|
compliance: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseState = {
|
||||||
|
entities: {
|
||||||
|
admin: {
|
||||||
|
plugins: {
|
||||||
|
testplugin: plugin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('empty sections and settings', () => {
|
||||||
|
renderWithContext(
|
||||||
|
<CustomPluginSettings
|
||||||
|
{...baseProps}
|
||||||
|
patchConfig={jest.fn()}
|
||||||
|
/>
|
||||||
|
, {...baseState});
|
||||||
|
|
||||||
|
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('PluginSettings.PluginStates.testplugin.Enable')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('This is the header')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('This is the footer')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all custom sections with plugin disabled should show single warning', () => {
|
||||||
|
const state = {
|
||||||
|
...baseState,
|
||||||
|
entities: {
|
||||||
|
admin: {
|
||||||
|
plugins: {
|
||||||
|
testplugin: {
|
||||||
|
...plugin,
|
||||||
|
settings_schema: {
|
||||||
|
...plugin.settings_schema,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
key: 'section1',
|
||||||
|
title: 'Custom Section 1',
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: 'customsection1numbersetting',
|
||||||
|
label: 'Custom Section Number Setting',
|
||||||
|
type: 'number' as const,
|
||||||
|
help_text: 'Custom Section Number Setting Help Text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'section2',
|
||||||
|
title: 'Custom Section 2',
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: 'customsection2numbersetting',
|
||||||
|
label: 'Custom Section Number Setting',
|
||||||
|
type: 'number' as const,
|
||||||
|
help_text: 'Custom Section Number Setting Help Text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
config: {
|
||||||
|
...baseProps.config,
|
||||||
|
PluginStates: {
|
||||||
|
testplugin: {
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<CustomPluginSettings
|
||||||
|
{...props}
|
||||||
|
patchConfig={jest.fn()}
|
||||||
|
/>
|
||||||
|
, {...state});
|
||||||
|
|
||||||
|
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('PluginSettings.PluginStates.testplugin.Enable')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('In order to view and configure plugin settings, enable the plugin and click Save.')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Custom Section 1')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Custom Section 2')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('custom sections with plugin enabled should render as expected', () => {
|
||||||
|
const CustomSection1 = () => {
|
||||||
|
return (
|
||||||
|
<div>{'Custom Component Section 1'}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomSection2 = () => {
|
||||||
|
return (
|
||||||
|
<div>{'Custom Component Section 2'}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
...baseState,
|
||||||
|
entities: {
|
||||||
|
admin: {
|
||||||
|
plugins: {
|
||||||
|
testplugin: {
|
||||||
|
...plugin,
|
||||||
|
settings_schema: {
|
||||||
|
...plugin.settings_schema,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
key: 'section1',
|
||||||
|
title: 'Custom Section 1',
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: 'customsection1numbersetting',
|
||||||
|
label: 'Custom Section Number Setting',
|
||||||
|
type: 'number' as const,
|
||||||
|
help_text: 'Custom Section Number Setting Help Text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'section2',
|
||||||
|
title: 'Custom Section 2',
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: 'customsection2numbersetting',
|
||||||
|
label: 'Custom Section Number Setting',
|
||||||
|
type: 'number' as const,
|
||||||
|
help_text: 'Custom Section Number Setting Help Text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
custom: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
adminConsoleCustomSections: {
|
||||||
|
testplugin: {
|
||||||
|
section1: {
|
||||||
|
pluginId: 'testplugin',
|
||||||
|
key: 'section1',
|
||||||
|
component: CustomSection1 as unknown as React.Component,
|
||||||
|
},
|
||||||
|
section2: {
|
||||||
|
pluginId: 'testplugin',
|
||||||
|
key: 'section2',
|
||||||
|
component: CustomSection2 as unknown as React.Component,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
...baseProps,
|
||||||
|
config: {
|
||||||
|
...baseProps.config,
|
||||||
|
PluginStates: {
|
||||||
|
testplugin: {
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithContext(
|
||||||
|
<CustomPluginSettings
|
||||||
|
{...props}
|
||||||
|
patchConfig={jest.fn()}
|
||||||
|
/>
|
||||||
|
, {...state});
|
||||||
|
|
||||||
|
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('PluginSettings.PluginStates.testplugin.Enable')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('In order to view and configure plugin settings, enable the plugin and click Save.')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Custom Component Section 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Custom Component Section 2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -94,9 +94,11 @@ function makeGetPluginSchema() {
|
|||||||
if (section.custom) {
|
if (section.custom) {
|
||||||
if (customSections[key]) {
|
if (customSections[key]) {
|
||||||
component = customSections[key]?.component;
|
component = customSections[key]?.component;
|
||||||
|
settings = parsePluginSettings(section.settings);
|
||||||
} else {
|
} else {
|
||||||
// Show warning banner for custom sections when the plugin is disabled.
|
// Show warning banner for custom sections when the plugin is disabled.
|
||||||
settings = [{
|
settings = [{
|
||||||
|
key: key + 'disabledWarning',
|
||||||
type: Constants.SettingsTypes.TYPE_BANNER,
|
type: Constants.SettingsTypes.TYPE_BANNER,
|
||||||
label: defineMessage({
|
label: defineMessage({
|
||||||
id: 'admin.plugin.customSection.pluginDisabledWarning',
|
id: 'admin.plugin.customSection.pluginDisabledWarning',
|
||||||
@@ -105,9 +107,7 @@ function makeGetPluginSchema() {
|
|||||||
banner_type: 'warning',
|
banner_type: 'warning',
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if (settings.length === 0) {
|
|
||||||
settings = parsePluginSettings(section.settings);
|
settings = parsePluginSettings(section.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,16 +132,33 @@ function makeGetPluginSchema() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.id !== appsPluginID || appsFeatureFlagIsEnabled) {
|
if (plugin.id !== appsPluginID || appsFeatureFlagIsEnabled) {
|
||||||
const pluginEnableSetting = getEnablePluginSetting(plugin);
|
const pluginEnableSetting = getEnablePluginSetting(plugin) as AdminDefinitionSetting;
|
||||||
if (pluginEnableSetting.isDisabled) {
|
|
||||||
pluginEnableSetting.isDisabled = it.any(pluginEnableSetting.isDisabled, it.not(it.userHasWritePermissionOnResource('plugins')));
|
|
||||||
} else {
|
|
||||||
pluginEnableSetting.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sections.length > 0) {
|
if (plugin.settings_schema && plugin.settings_schema.sections?.every((s) => s.custom && !customSections[s.key.toLowerCase()])) {
|
||||||
sections[0].settings.unshift(pluginEnableSetting as AdminDefinitionSetting);
|
// If the plugin is composed of purely custom sections (e.g. Calls) and it's disabled (custom components are not found), we show a single warning.
|
||||||
|
const warningBanner = {
|
||||||
|
key: 'admin.plugin.customSections.pluginDisabledWarning',
|
||||||
|
type: Constants.SettingsTypes.TYPE_BANNER,
|
||||||
|
label: defineMessage({id: 'admin.plugin.customSections.pluginDisabledWarning', defaultMessage: 'In order to view and configure plugin settings, enable the plugin and click Save.'}),
|
||||||
|
banner_type: 'warning' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
sections = [{
|
||||||
|
key: pluginEnabledConfigKey + '.Section',
|
||||||
|
header: plugin.settings_schema?.header,
|
||||||
|
footer: plugin.settings_schema?.footer,
|
||||||
|
settings: [pluginEnableSetting, warningBanner],
|
||||||
|
}];
|
||||||
|
} else if (sections.length > 0) {
|
||||||
|
// Have a separate section on top with the plugin enable/disable setting.
|
||||||
|
sections.unshift({
|
||||||
|
key: pluginEnabledConfigKey + '.Section',
|
||||||
|
header: plugin.settings_schema?.header,
|
||||||
|
footer: plugin.settings_schema?.footer,
|
||||||
|
settings: [pluginEnableSetting],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// Otherwise we retain existing behaviour and add the setting in front.
|
||||||
settings.unshift(pluginEnableSetting);
|
settings.unshift(pluginEnableSetting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1108,6 +1108,23 @@ export class SchemaAdminSettings extends React.PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a bit of special case since designs for plugin config expect the Enable/Disable setting
|
||||||
|
// to be on top and out of the sections.
|
||||||
|
if (section.key.startsWith('PluginSettings.PluginStates') && section.key.endsWith('Enable.Section')) {
|
||||||
|
sections.push(
|
||||||
|
<SettingsGroup
|
||||||
|
container={false}
|
||||||
|
key={section.key}
|
||||||
|
>
|
||||||
|
{header}
|
||||||
|
{settingsList}
|
||||||
|
{footer}
|
||||||
|
</SettingsGroup>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sections.push(
|
sections.push(
|
||||||
<div
|
<div
|
||||||
className={'config-section'}
|
className={'config-section'}
|
||||||
|
|||||||
@@ -1918,6 +1918,7 @@
|
|||||||
"admin.plugin.choose": "Choose File",
|
"admin.plugin.choose": "Choose File",
|
||||||
"admin.plugin.cluster_instance": "Cluster Instance",
|
"admin.plugin.cluster_instance": "Cluster Instance",
|
||||||
"admin.plugin.customSection.pluginDisabledWarning": "In order to view this section, enable the plugin and click Save.",
|
"admin.plugin.customSection.pluginDisabledWarning": "In order to view this section, enable the plugin and click Save.",
|
||||||
|
"admin.plugin.customSections.pluginDisabledWarning": "In order to view and configure plugin settings, enable the plugin and click Save.",
|
||||||
"admin.plugin.customSetting.pluginDisabledWarning": "In order to view this setting, enable the plugin and click Save.",
|
"admin.plugin.customSetting.pluginDisabledWarning": "In order to view this setting, enable the plugin and click Save.",
|
||||||
"admin.plugin.disable": "Disable",
|
"admin.plugin.disable": "Disable",
|
||||||
"admin.plugin.disabling": "Disabling...",
|
"admin.plugin.disabling": "Disabling...",
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user