mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08:00
Add plugin metadata panel to management and settings pages (#37331)
* Add PluginMetadataPanel to plugin management and settings pages Surface plugin ID, version, website, and release notes links on the Plugin Management list and individual plugin settings pages so admins do not need to switch between pages to find plugin identity information. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Simplify plugin metadata to a single inline line Show plugin identity as 'Name (id - version - website)' on Plugin Management and in the plugin settings page header instead of a multi-row metadata block. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Refine plugin metadata line with code ID, name link, and v prefix Style the plugin ID as inline code with a copy button, link the display name to the website URL, and prefix versions with v when missing. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Move plugin metadata into settings page content area Render the metadata line in the plugin settings content body instead of the oversized admin header so it matches Plugin Management styling. Use the installed plugin status version when available. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Fix lint issues in plugin metadata panel files Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Fix stylelint property order in plugin metadata scss Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Add i18n string for plugin metadata release notes link Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Harden plugin metadata edge cases and link version to release notes Fall back to the plugin ID when the display name is empty, omit the version segment when no version is installed, and link the version text to release notes instead of showing a separate release notes label. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Simplify plugin ID styling and shrink copy icon Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Style plugin metadata parenthetical as muted technical text Wrap the ID and version in a smaller, muted segment and use monospace for the plugin ID without code-block highlighting. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Update plugin management snapshots for muted metadata styling Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Add visually hidden page heading for plugin settings pages Plugin settings pages hide the AdminHeader title in favor of the inline metadata panel, which left no accessible page heading. Render an sr-only h1 with the schema title so assistive tech retains page semantics without changing the visual layout. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Make plugin ID text clickable to copy Replace the separate copy icon with click-to-copy on the monospace plugin ID. The ID shows a pointer cursor and link color on hover, with the same Copy text / Copied tooltip feedback as before. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Use shared useCopyText hook for plugin ID copy Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Fix stylelint property order in plugin metadata panel styles Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> * Address review: add name/id test and memoize copy handlers Add an integration test where plugin display name differs from plugin ID. Use useCallback and useMemo in PluginMetadataId to avoid recreating handlers and tooltip content on every render. Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Ben Schumacher <hanzei@users.noreply.github.com>
This commit is contained in:
co-authored by
Ben Schumacher
Cursor Agent
parent
cce485f605
commit
b530799aeb
@@ -82,6 +82,13 @@ describe('custom plugin sections and settings', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const expectPluginPageTitle = (pluginName: string, pluginId: string) => {
|
||||
const panel = screen.getByTestId('plugin-metadata-panel');
|
||||
expect(panel).toHaveTextContent(`${pluginName} (${pluginId}`);
|
||||
expect(document.querySelector('.PluginMetadataPanel__settingsWrapper')).toContainElement(panel);
|
||||
expect(screen.getByRole('heading', {level: 1, hidden: true})).toHaveTextContent(pluginName);
|
||||
};
|
||||
|
||||
it('empty sections and settings', () => {
|
||||
renderWithContext(
|
||||
<CustomPluginSettings
|
||||
@@ -90,12 +97,48 @@ describe('custom plugin sections and settings', () => {
|
||||
/>,
|
||||
{...baseState});
|
||||
|
||||
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||
expectPluginPageTitle('testplugin', 'testplugin');
|
||||
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('renders plugin metadata with distinct display name and id', () => {
|
||||
const pluginId = 'com.mattermost.fl3xx';
|
||||
const pluginName = 'FL3XX';
|
||||
const namedPlugin = {
|
||||
...plugin,
|
||||
id: pluginId,
|
||||
name: pluginName,
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<CustomPluginSettings
|
||||
{...baseProps}
|
||||
match={{params: {plugin_id: pluginId}} as match<{plugin_id: string}>}
|
||||
config={{
|
||||
PluginSettings: {
|
||||
Plugins: {
|
||||
[pluginId]: {},
|
||||
},
|
||||
} as unknown as PluginSettings,
|
||||
}}
|
||||
patchConfig={jest.fn()}
|
||||
/>,
|
||||
{
|
||||
entities: {
|
||||
admin: {
|
||||
plugins: {
|
||||
[pluginId]: namedPlugin,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expectPluginPageTitle(pluginName, pluginId);
|
||||
});
|
||||
|
||||
it('all custom sections with plugin disabled should show single warning', () => {
|
||||
const state = {
|
||||
...baseState,
|
||||
@@ -160,7 +203,7 @@ describe('custom plugin sections and settings', () => {
|
||||
/>,
|
||||
{...state});
|
||||
|
||||
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||
expectPluginPageTitle('testplugin', 'testplugin');
|
||||
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();
|
||||
@@ -239,7 +282,7 @@ describe('custom plugin sections and settings', () => {
|
||||
/>,
|
||||
{...state});
|
||||
|
||||
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||
expectPluginPageTitle('testplugin', 'testplugin');
|
||||
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.queryByText('Custom Section 1')).toBeInTheDocument();
|
||||
@@ -342,7 +385,7 @@ describe('custom plugin sections and settings', () => {
|
||||
/>,
|
||||
{...state});
|
||||
|
||||
expect(screen.getByText('testplugin')).toBeInTheDocument();
|
||||
expectPluginPageTitle('testplugin', 'testplugin');
|
||||
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();
|
||||
|
||||
@@ -203,6 +203,8 @@ function makeMapStateToProps() {
|
||||
return {
|
||||
schema: getPluginSchema(state, pluginId),
|
||||
roles: getRoles(state),
|
||||
plugin: state.entities.admin.plugins?.[pluginId],
|
||||
pluginVersion: state.entities.admin.pluginStatuses?.[pluginId]?.version,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
+182
-49
@@ -4207,16 +4207,35 @@ exports[`components/PluginManagement should match snapshot, with installed plugi
|
||||
<div
|
||||
data-testid="plugin_0"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="PluginMetadataPanel"
|
||||
data-testid="plugin-metadata-panel"
|
||||
>
|
||||
<strong>
|
||||
Plugin 0
|
||||
</strong>
|
||||
(
|
||||
plugin_0
|
||||
-
|
||||
0.1.0
|
||||
)
|
||||
</div>
|
||||
<span
|
||||
class="PluginMetadataPanel__metadata"
|
||||
>
|
||||
(
|
||||
<span
|
||||
aria-label="Copy"
|
||||
class="PluginMetadataPanel__id"
|
||||
data-testid="plugin-metadata-id"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
plugin_0
|
||||
</span>
|
||||
-
|
||||
<span
|
||||
data-testid="plugin-metadata-version"
|
||||
>
|
||||
v0.1.0
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="pt-2"
|
||||
>
|
||||
@@ -4314,16 +4333,35 @@ exports[`components/PluginManagement should match snapshot, with installed plugi
|
||||
<div
|
||||
data-testid="plugin_1"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="PluginMetadataPanel"
|
||||
data-testid="plugin-metadata-panel"
|
||||
>
|
||||
<strong>
|
||||
Plugin 1
|
||||
</strong>
|
||||
(
|
||||
plugin_1
|
||||
-
|
||||
0.0.1
|
||||
)
|
||||
</div>
|
||||
<span
|
||||
class="PluginMetadataPanel__metadata"
|
||||
>
|
||||
(
|
||||
<span
|
||||
aria-label="Copy"
|
||||
class="PluginMetadataPanel__id"
|
||||
data-testid="plugin-metadata-id"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
plugin_1
|
||||
</span>
|
||||
-
|
||||
<span
|
||||
data-testid="plugin-metadata-version"
|
||||
>
|
||||
v0.0.1
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="pt-2"
|
||||
>
|
||||
@@ -4866,16 +4904,35 @@ exports[`components/PluginManagement should match snapshot, with installed plugi
|
||||
<div
|
||||
data-testid="plugin_0"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="PluginMetadataPanel"
|
||||
data-testid="plugin-metadata-panel"
|
||||
>
|
||||
<strong>
|
||||
Plugin 0
|
||||
</strong>
|
||||
(
|
||||
plugin_0
|
||||
-
|
||||
0.1.0
|
||||
)
|
||||
</div>
|
||||
<span
|
||||
class="PluginMetadataPanel__metadata"
|
||||
>
|
||||
(
|
||||
<span
|
||||
aria-label="Copy"
|
||||
class="PluginMetadataPanel__id"
|
||||
data-testid="plugin-metadata-id"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
plugin_0
|
||||
</span>
|
||||
-
|
||||
<span
|
||||
data-testid="plugin-metadata-version"
|
||||
>
|
||||
v0.1.0
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="pt-2"
|
||||
>
|
||||
@@ -5342,16 +5399,35 @@ exports[`components/PluginManagement should match snapshot, with installed plugi
|
||||
<div
|
||||
data-testid="plugin_0"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="PluginMetadataPanel"
|
||||
data-testid="plugin-metadata-panel"
|
||||
>
|
||||
<strong>
|
||||
Plugin 0
|
||||
</strong>
|
||||
(
|
||||
plugin_0
|
||||
-
|
||||
0.1.0
|
||||
)
|
||||
</div>
|
||||
<span
|
||||
class="PluginMetadataPanel__metadata"
|
||||
>
|
||||
(
|
||||
<span
|
||||
aria-label="Copy"
|
||||
class="PluginMetadataPanel__id"
|
||||
data-testid="plugin-metadata-id"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
plugin_0
|
||||
</span>
|
||||
-
|
||||
<span
|
||||
data-testid="plugin-metadata-version"
|
||||
>
|
||||
v0.1.0
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="pt-2"
|
||||
>
|
||||
@@ -5818,16 +5894,35 @@ exports[`components/PluginManagement should match snapshot, with installed plugi
|
||||
<div
|
||||
data-testid="plugin_0"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="PluginMetadataPanel"
|
||||
data-testid="plugin-metadata-panel"
|
||||
>
|
||||
<strong>
|
||||
Plugin 0
|
||||
</strong>
|
||||
(
|
||||
plugin_0
|
||||
-
|
||||
0.1.0
|
||||
)
|
||||
</div>
|
||||
<span
|
||||
class="PluginMetadataPanel__metadata"
|
||||
>
|
||||
(
|
||||
<span
|
||||
aria-label="Copy"
|
||||
class="PluginMetadataPanel__id"
|
||||
data-testid="plugin-metadata-id"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
plugin_0
|
||||
</span>
|
||||
-
|
||||
<span
|
||||
data-testid="plugin-metadata-version"
|
||||
>
|
||||
v0.1.0
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="pt-2"
|
||||
>
|
||||
@@ -6294,16 +6389,35 @@ exports[`components/PluginManagement should match snapshot, with installed plugi
|
||||
<div
|
||||
data-testid="plugin_0"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="PluginMetadataPanel"
|
||||
data-testid="plugin-metadata-panel"
|
||||
>
|
||||
<strong>
|
||||
Plugin 0
|
||||
</strong>
|
||||
(
|
||||
plugin_0
|
||||
-
|
||||
0.1.0
|
||||
)
|
||||
</div>
|
||||
<span
|
||||
class="PluginMetadataPanel__metadata"
|
||||
>
|
||||
(
|
||||
<span
|
||||
aria-label="Copy"
|
||||
class="PluginMetadataPanel__id"
|
||||
data-testid="plugin-metadata-id"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
plugin_0
|
||||
</span>
|
||||
-
|
||||
<span
|
||||
data-testid="plugin-metadata-version"
|
||||
>
|
||||
v0.1.0
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="pt-2"
|
||||
>
|
||||
@@ -6393,16 +6507,35 @@ exports[`components/PluginManagement should match snapshot, with installed plugi
|
||||
<div
|
||||
data-testid="plugin_1"
|
||||
>
|
||||
<div>
|
||||
<span
|
||||
class="PluginMetadataPanel"
|
||||
data-testid="plugin-metadata-panel"
|
||||
>
|
||||
<strong>
|
||||
Plugin 1
|
||||
</strong>
|
||||
(
|
||||
plugin_1
|
||||
-
|
||||
0.0.1
|
||||
)
|
||||
</div>
|
||||
<span
|
||||
class="PluginMetadataPanel__metadata"
|
||||
>
|
||||
(
|
||||
<span
|
||||
aria-label="Copy"
|
||||
class="PluginMetadataPanel__id"
|
||||
data-testid="plugin-metadata-id"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
plugin_1
|
||||
</span>
|
||||
-
|
||||
<span
|
||||
data-testid="plugin-metadata-version"
|
||||
>
|
||||
v0.0.1
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
<div
|
||||
class="pt-2"
|
||||
>
|
||||
|
||||
+14
-8
@@ -24,6 +24,7 @@ import * as Utils from 'utils/utils';
|
||||
import BooleanSetting from '../boolean_setting';
|
||||
import OLDAdminSettings from '../old_admin_settings';
|
||||
import type {BaseProps, BaseState} from '../old_admin_settings';
|
||||
import PluginMetadataPanel from '../plugin_metadata_panel/plugin_metadata_panel';
|
||||
import SettingSet from '../setting_set';
|
||||
import SettingsGroup from '../settings_group';
|
||||
import TextSetting from '../text_setting';
|
||||
@@ -178,6 +179,10 @@ type PluginStatus = {
|
||||
|
||||
type PluginItemProps = {
|
||||
pluginStatus: PluginStatus;
|
||||
plugin?: {
|
||||
homepage_url?: string;
|
||||
release_notes_url?: string;
|
||||
};
|
||||
removing: boolean;
|
||||
handleEnable: (e: any) => any;
|
||||
handleDisable: (e: any) => any;
|
||||
@@ -228,6 +233,7 @@ export const searchableStrings = [
|
||||
|
||||
const PluginItem = ({
|
||||
pluginStatus,
|
||||
plugin,
|
||||
removing,
|
||||
handleEnable,
|
||||
handleDisable,
|
||||
@@ -430,14 +436,13 @@ const PluginItem = ({
|
||||
|
||||
return (
|
||||
<div data-testid={pluginStatus.id}>
|
||||
<div>
|
||||
<strong>{pluginStatus.name}</strong>
|
||||
{' ('}
|
||||
{pluginStatus.id}
|
||||
{' - '}
|
||||
{pluginStatus.version}
|
||||
{')'}
|
||||
</div>
|
||||
<PluginMetadataPanel
|
||||
name={pluginStatus.name}
|
||||
id={pluginStatus.id}
|
||||
version={pluginStatus.version}
|
||||
homepageUrl={plugin?.homepage_url}
|
||||
releaseNotesUrl={plugin?.release_notes_url}
|
||||
/>
|
||||
{description}
|
||||
<div className='pt-2'>
|
||||
{activateButton}
|
||||
@@ -997,6 +1002,7 @@ export class PluginManagement extends OLDAdminSettings<Props, State> {
|
||||
<PluginItem
|
||||
key={pluginStatus.id}
|
||||
pluginStatus={pluginStatus}
|
||||
plugin={p}
|
||||
removing={this.state.removing === pluginStatus.id}
|
||||
handleEnable={this.handleEnable}
|
||||
handleDisable={this.handleDisable}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
.PluginMetadataPanel {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
&__settingsWrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__metadata {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 0.9em;
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__id {
|
||||
cursor: pointer;
|
||||
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
|
||||
&:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import PluginMetadataPanel, {formatPluginVersion} from 'components/admin_console/plugin_metadata_panel/plugin_metadata_panel';
|
||||
|
||||
import {screen, renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
describe('formatPluginVersion', () => {
|
||||
test('should prefix version with v when missing', () => {
|
||||
expect(formatPluginVersion('0.7.4')).toBe('v0.7.4');
|
||||
expect(formatPluginVersion('1.2.3')).toBe('v1.2.3');
|
||||
});
|
||||
|
||||
test('should not duplicate v prefix', () => {
|
||||
expect(formatPluginVersion('v0.7.4')).toBe('v0.7.4');
|
||||
expect(formatPluginVersion('V1.0.0')).toBe('V1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PluginMetadataPanel', () => {
|
||||
test('should render plugin name, id, and version on one line', () => {
|
||||
renderWithContext(
|
||||
<PluginMetadataPanel
|
||||
name='FL3XX'
|
||||
id='com.mattermost.fl3xx'
|
||||
version='0.7.4'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('plugin-metadata-panel')).toHaveTextContent('FL3XX (com.mattermost.fl3xx - v0.7.4)');
|
||||
expect(screen.getByTestId('plugin-metadata-id')).toHaveTextContent('com.mattermost.fl3xx');
|
||||
expect(screen.getByTestId('plugin-metadata-version')).toHaveTextContent('v0.7.4');
|
||||
expect(screen.getByRole('button', {name: 'Copy'})).toHaveTextContent('com.mattermost.fl3xx');
|
||||
});
|
||||
|
||||
test('should fall back to plugin id when display name is missing', () => {
|
||||
renderWithContext(
|
||||
<PluginMetadataPanel
|
||||
name=' '
|
||||
id='com.mattermost.fl3xx'
|
||||
version='0.7.4'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('plugin-metadata-panel')).toHaveTextContent('com.mattermost.fl3xx (com.mattermost.fl3xx - v0.7.4)');
|
||||
expect(screen.getByText('com.mattermost.fl3xx', {selector: 'strong'})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should omit version segment when version is missing', () => {
|
||||
renderWithContext(
|
||||
<PluginMetadataPanel
|
||||
name='FL3XX'
|
||||
id='com.mattermost.fl3xx'
|
||||
version=''
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('plugin-metadata-panel')).toHaveTextContent('FL3XX (com.mattermost.fl3xx)');
|
||||
expect(screen.queryByTestId('plugin-metadata-version')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should link display name to website and version to release notes when provided', () => {
|
||||
renderWithContext(
|
||||
<PluginMetadataPanel
|
||||
name='Agents Plugin'
|
||||
id='com.mattermost.ai'
|
||||
version='1.2.3'
|
||||
homepageUrl='https://github.com/mattermost/mattermost-plugin-ai'
|
||||
releaseNotesUrl='https://github.com/mattermost/mattermost-plugin-ai/releases/tag/v1.2.3'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('link', {name: 'Agents Plugin'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-plugin-ai');
|
||||
expect(screen.getByRole('link', {name: 'v1.2.3'})).toHaveAttribute('href', 'https://github.com/mattermost/mattermost-plugin-ai/releases/tag/v1.2.3');
|
||||
expect(screen.queryByText('release notes')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not render links when urls are not provided', () => {
|
||||
renderWithContext(
|
||||
<PluginMetadataPanel
|
||||
name='Agents Plugin'
|
||||
id='com.mattermost.ai'
|
||||
version='1.2.3'
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {WithTooltip} from '@mattermost/shared/components/tooltip';
|
||||
|
||||
import useCopyText, {messages as copyMessages} from 'components/common/hooks/useCopyText';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import './plugin_metadata_panel.scss';
|
||||
|
||||
export function formatPluginVersion(version: string): string {
|
||||
if (!version) {
|
||||
return version;
|
||||
}
|
||||
|
||||
return (/^v/i).test(version) ? version : `v${version}`;
|
||||
}
|
||||
|
||||
export type PluginMetadataPanelProps = {
|
||||
name: string;
|
||||
id: string;
|
||||
version: string;
|
||||
homepageUrl?: string;
|
||||
releaseNotesUrl?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const PluginMetadataId = ({id}: {id: string}) => {
|
||||
const intl = useIntl();
|
||||
const {copiedRecently, onClick: copyId} = useCopyText({
|
||||
text: id,
|
||||
successCopyTimeout: 2000,
|
||||
});
|
||||
|
||||
const tooltipMessage = copiedRecently ? copyMessages.copied : copyMessages.copy;
|
||||
|
||||
const handleCopy = useCallback((e: React.MouseEvent | React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
copyId();
|
||||
}, [copyId]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleCopy(e);
|
||||
}
|
||||
}, [handleCopy]);
|
||||
|
||||
const tooltipTitle = useMemo(() => (
|
||||
<FormattedMessage {...tooltipMessage}/>
|
||||
), [tooltipMessage]);
|
||||
|
||||
return (
|
||||
<WithTooltip
|
||||
title={tooltipTitle}
|
||||
>
|
||||
<span
|
||||
className='PluginMetadataPanel__id'
|
||||
data-testid='plugin-metadata-id'
|
||||
onClick={handleCopy}
|
||||
onKeyDown={handleKeyDown}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-label={intl.formatMessage(tooltipMessage)}
|
||||
>
|
||||
{id}
|
||||
</span>
|
||||
</WithTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const PluginMetadataPanel = ({
|
||||
name,
|
||||
id,
|
||||
version,
|
||||
homepageUrl,
|
||||
releaseNotesUrl,
|
||||
className,
|
||||
}: PluginMetadataPanelProps) => {
|
||||
const displayName = name.trim() || id;
|
||||
const formattedVersion = formatPluginVersion(version);
|
||||
|
||||
let nameElement: React.ReactNode = <strong>{displayName}</strong>;
|
||||
if (homepageUrl) {
|
||||
nameElement = (
|
||||
<ExternalLink
|
||||
href={homepageUrl}
|
||||
location='plugin_metadata_panel'
|
||||
>
|
||||
<strong>{displayName}</strong>
|
||||
</ExternalLink>
|
||||
);
|
||||
}
|
||||
|
||||
let versionElement: React.ReactNode = null;
|
||||
if (formattedVersion) {
|
||||
versionElement = (
|
||||
<>
|
||||
{' - '}
|
||||
{releaseNotesUrl ? (
|
||||
<ExternalLink
|
||||
href={releaseNotesUrl}
|
||||
location='plugin_metadata_panel'
|
||||
data-testid='plugin-metadata-version'
|
||||
>
|
||||
{formattedVersion}
|
||||
</ExternalLink>
|
||||
) : (
|
||||
<span data-testid='plugin-metadata-version'>
|
||||
{formattedVersion}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames('PluginMetadataPanel', className)}
|
||||
data-testid='plugin-metadata-panel'
|
||||
>
|
||||
{nameElement}
|
||||
<span className='PluginMetadataPanel__metadata'>
|
||||
{' ('}
|
||||
<PluginMetadataId id={id}/>
|
||||
{versionElement}
|
||||
{')'}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginMetadataPanel;
|
||||
@@ -9,6 +9,7 @@ import {Link} from 'react-router-dom';
|
||||
import {WithTooltip} from '@mattermost/shared/components/tooltip';
|
||||
import type {CloudState} from '@mattermost/types/cloud';
|
||||
import type {AdminConfig, ClientLicense, EnvironmentConfig} from '@mattermost/types/config';
|
||||
import type {PluginRedux} from '@mattermost/types/plugins';
|
||||
import type {Role} from '@mattermost/types/roles';
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
@@ -40,6 +41,7 @@ import * as I18n from 'i18n/i18n';
|
||||
import Constants from 'utils/constants';
|
||||
import {mappingValueFromRoles, rolesFromMapping} from 'utils/policy_roles_adapter';
|
||||
|
||||
import PluginMetadataPanel from './plugin_metadata_panel/plugin_metadata_panel';
|
||||
import Setting from './setting';
|
||||
import type {AdminDefinitionConfigSchemaSection, AdminDefinitionSetting, AdminDefinitionSettingBanner, AdminDefinitionSettingDropdownOption, AdminDefinitionSubSectionSchema, ConsoleAccess} from './types';
|
||||
|
||||
@@ -80,6 +82,8 @@ export type SchemaAdminSettingsProps = {
|
||||
cloud: CloudState;
|
||||
isCurrentUserSystemAdmin: boolean;
|
||||
enterpriseReady: boolean;
|
||||
plugin?: PluginRedux;
|
||||
pluginVersion?: string;
|
||||
} & WrappedComponentProps;
|
||||
|
||||
type State = {
|
||||
@@ -307,6 +311,22 @@ export class SchemaAdminSettings extends React.PureComponent<SchemaAdminSettings
|
||||
name = this.props.schema.name;
|
||||
}
|
||||
|
||||
if (this.props.plugin) {
|
||||
const title = typeof name === 'string' ? (
|
||||
name
|
||||
) : (
|
||||
<FormattedMessage
|
||||
{...name}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<h1 className='sr-only'>
|
||||
{title}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
|
||||
const betaBadge = this.props.schema.isBeta && (
|
||||
<BetaTag
|
||||
variant='default'
|
||||
@@ -334,6 +354,24 @@ export class SchemaAdminSettings extends React.PureComponent<SchemaAdminSettings
|
||||
);
|
||||
};
|
||||
|
||||
renderPluginMetadata = () => {
|
||||
if (!this.props.plugin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='PluginMetadataPanel__settingsWrapper'>
|
||||
<PluginMetadataPanel
|
||||
name={this.props.plugin.name}
|
||||
id={this.props.plugin.id}
|
||||
version={this.props.pluginVersion || this.props.plugin.version}
|
||||
homepageUrl={this.props.plugin.homepage_url}
|
||||
releaseNotesUrl={this.props.plugin.release_notes_url}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderBanner = (setting: AdminDefinitionSettingBanner) => {
|
||||
if (!this.props.schema || !('label' in setting)) {
|
||||
return <span>{''}</span>;
|
||||
@@ -1353,6 +1391,7 @@ export class SchemaAdminSettings extends React.PureComponent<SchemaAdminSettings
|
||||
{this.renderTitle()}
|
||||
<div className='admin-console__wrapper'>
|
||||
<div className='admin-console__content'>
|
||||
{this.renderPluginMetadata()}
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
|
||||
Reference in New Issue
Block a user