diff --git a/.github/workflows/mmctl-test-template.yml b/.github/workflows/mmctl-test-template.yml index 3713ffee3f..d2098001f8 100644 --- a/.github/workflows/mmctl-test-template.yml +++ b/.github/workflows/mmctl-test-template.yml @@ -34,6 +34,10 @@ jobs: run: | cd server make setup-go-work + - name: Setup needed prepackaged plugins + run: | + cd server + make prepackaged-plugins PLUGIN_PACKAGES=mattermost-plugin-jira-v3.2.5 - name: Run docker compose run: | cd server/build diff --git a/e2e-tests/cypress/tests/integration/channels/plugins/marketplace/ui_spec.js b/e2e-tests/cypress/tests/integration/channels/plugins/marketplace/ui_spec.js index 9b36d0e96b..f5cc983ba8 100644 --- a/e2e-tests/cypress/tests/integration/channels/plugins/marketplace/ui_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/plugins/marketplace/ui_spec.js @@ -17,6 +17,7 @@ describe('Plugin Marketplace', () => { before(() => { cy.shouldNotRunOnCloudEdition(); + cy.shouldHaveFeatureFlag('StreamlinedMarketplace', 'false'); // https://mattermost.atlassian.net/browse/MM-54230 cy.shouldHavePluginUploadEnabled(); cy.apiInitSetup().then(({team}) => { @@ -72,6 +73,7 @@ describe('Plugin Marketplace', () => { cy.get('#marketplaceTabs-pane-allListing').should('be.visible'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('MM-T2001 autofocus on search plugin input box', () => { cy.uiClose(); @@ -82,6 +84,7 @@ describe('Plugin Marketplace', () => { cy.findByPlaceholderText('Search Marketplace').should('be.focused'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('render the list of all plugins by default', () => { // * Verify all plugins tab should be active cy.get('#marketplaceTabs-tab-allListing').should('be.visible').parent().should('have.class', 'active'); @@ -92,6 +95,7 @@ describe('Plugin Marketplace', () => { cy.get('#marketplaceTabs-pane-installed').should('not.exist'); }); + // https://mattermost.atlassian.net/browse/MM-54230 // this test uses exist, not visible, due to issues with Cypress it('render the list of installed plugins on demand', () => { // # Click on installed plugins tab @@ -117,6 +121,7 @@ describe('Plugin Marketplace', () => { cy.get('#modal_marketplace').should('not.exist'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('should filter all on search', () => { // # Load all plugins before searching cy.get('.more-modal__row').should('have.length', 15); @@ -136,6 +141,7 @@ describe('Plugin Marketplace', () => { should('have.length', 1); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('should show an error bar on failing to filter', () => { // # Enable Plugin Marketplace cy.apiUpdateConfig({ @@ -168,6 +174,7 @@ describe('Plugin Marketplace', () => { cy.get('#marketplace-plugin-com\\.mattermost\\.webex').find('.btn.btn-outline', {timeout: TIMEOUTS.ONE_MIN}).scrollIntoView().should('be.visible').and('have.text', 'Configure'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('should install a plugin from search results on demand', () => { // # Uninstall any existing webex plugin cy.apiRemovePluginById('com.mattermost.webex'); @@ -226,6 +233,7 @@ describe('Plugin Marketplace', () => { cy.get('#marketplace-plugin-github').should('be.visible'); }); + // https://mattermost.atlassian.net/browse/MM-54230 it('MM-T1986 change tab to "All Plugins" when "Install Plugins" link is clicked', () => { cy.get('#marketplaceTabs').scrollIntoView().should('be.visible').within(() => { // # Switch tab to installed plugin diff --git a/e2e-tests/playwright/support/server/default_config.ts b/e2e-tests/playwright/support/server/default_config.ts index 4260123605..be7f8514dc 100644 --- a/e2e-tests/playwright/support/server/default_config.ts +++ b/e2e-tests/playwright/support/server/default_config.ts @@ -679,6 +679,7 @@ const defaultServerConfig: AdminConfig = { OnboardingTourTips: true, DeprecateCloudFree: false, CloudReverseTrial: false, + StreamlinedMarketplace: true }, ImportSettings: { Directory: './import', diff --git a/server/Makefile b/server/Makefile index 0979467203..71ea089941 100644 --- a/server/Makefile +++ b/server/Makefile @@ -157,7 +157,8 @@ MMCTL_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/cmd/mmctl') TEMPLATES_DIR=templates # Plugins Packages -PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v1.0.0 +PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:) +PLUGIN_PACKAGES += mattermost-plugin-antivirus-v1.0.0 PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.4.0 PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0 PLUGIN_PACKAGES += mattermost-plugin-calls-v0.18.0 @@ -300,7 +301,7 @@ plugin-checker: $(GO) run $(GOFLAGS) ./public/plugin/checker prepackaged-plugins: ## Populate the prepackaged-plugins directory - @echo Downloading prepackaged plugins + @echo Downloading prepackaged plugins: $(PLUGIN_PACKAGES) mkdir -p prepackaged_plugins @cd prepackaged_plugins && for plugin_package in $(PLUGIN_PACKAGES) ; do \ curl -f -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz; \ diff --git a/server/channels/api4/plugin_test.go b/server/channels/api4/plugin_test.go index b515917ca9..7f5067a2a8 100644 --- a/server/channels/api4/plugin_test.go +++ b/server/channels/api4/plugin_test.go @@ -462,6 +462,9 @@ func TestDisableOnRemove(t *testing.T) { } func TestGetMarketplacePlugins(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + th := Setup(t) defer th.TearDown() @@ -682,6 +685,9 @@ func TestGetMarketplacePlugins(t *testing.T) { } func TestGetInstalledMarketplacePlugins(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + samplePlugins := []*model.MarketplacePlugin{ { BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ @@ -825,6 +831,9 @@ func TestGetInstalledMarketplacePlugins(t *testing.T) { } func TestSearchGetMarketplacePlugins(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + samplePlugins := []*model.MarketplacePlugin{ { BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ @@ -950,6 +959,9 @@ func TestSearchGetMarketplacePlugins(t *testing.T) { } func TestGetLocalPluginInMarketplace(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + th := Setup(t) defer th.TearDown() @@ -1111,6 +1123,9 @@ func TestGetLocalPluginInMarketplace(t *testing.T) { } func TestGetRemotePluginInMarketplace(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + th := Setup(t) defer th.TearDown() @@ -1166,7 +1181,70 @@ func TestGetRemotePluginInMarketplace(t *testing.T) { require.NoError(t, err) } +func TestRemoteMarketplaceDisabledByStreamlinedMarketplaceFlag(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + + th := Setup(t) + defer th.TearDown() + + marketplacePlugins := []*model.MarketplacePlugin{ + { + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps", + IconData: "https://example.com/icon.svg", + DownloadURL: "www.github.com/example", + Manifest: &model.Manifest{ + Id: "marketplace.test", + Name: "marketplacetest", + Description: "a marketplace plugin", + Version: "0.1.2", + MinServerVersion: "", + }, + }, + InstalledVersion: "", + }, + } + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(http.StatusOK) + json, err := json.Marshal([]*model.MarketplacePlugin{marketplacePlugins[0]}) + require.NoError(t, err) + res.Write(json) + })) + defer testServer.Close() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + *cfg.PluginSettings.EnableMarketplace = true + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.EnableUploads = true + *cfg.PluginSettings.MarketplaceURL = testServer.URL + }) + + prepackagePlugin := &plugin.PrepackagedPlugin{ + Manifest: &model.Manifest{ + Version: "0.0.1", + Id: "prepackaged.test", + }, + } + + env := th.App.GetPluginsEnvironment() + env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}, nil) + + // No marketplace plugins returned + plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{}) + require.NoError(t, err) + + // Only returns the prepackaged plugins + require.Len(t, plugins, 1) + require.Equal(t, prepackagePlugin.Manifest, plugins[0].Manifest) +} + func TestGetPrepackagedPluginInMarketplace(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + th := Setup(t) defer th.TearDown() @@ -1212,6 +1290,30 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) { env := th.App.GetPluginsEnvironment() env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}, nil) + t.Run("prepackaged plugins are shown in Cloud", func(t *testing.T) { + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.EnableRemoteMarketplace = true + *cfg.PluginSettings.EnableUploads = true + }) + + lic := th.App.Srv().License() + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + defer th.App.Srv().SetLicense(lic) + + plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{}) + require.NoError(t, err) + + expectedPlugins := marketplacePlugins + expectedPlugins = append(expectedPlugins, &model.MarketplacePlugin{ + BaseMarketplacePlugin: &model.BaseMarketplacePlugin{ + Manifest: prepackagePlugin.Manifest, + }, + }) + + require.ElementsMatch(t, expectedPlugins, plugins) + require.Len(t, plugins, 2) + }) + t.Run("get remote and prepackaged plugins", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.PluginSettings.EnableRemoteMarketplace = true @@ -1271,24 +1373,12 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) { require.Len(t, plugins, 1) require.Equal(t, newerPrepackagePlugin.Manifest, plugins[0].Manifest) }) - - t.Run("prepackaged plugins are not shown in Cloud", func(t *testing.T) { - th.App.UpdateConfig(func(cfg *model.Config) { - *cfg.PluginSettings.EnableRemoteMarketplace = true - *cfg.PluginSettings.EnableUploads = true - }) - - th.App.Srv().SetLicense(model.NewTestLicense("cloud")) - - plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{}) - require.NoError(t, err) - - require.ElementsMatch(t, marketplacePlugins, plugins) - require.Len(t, plugins, 1) - }) } func TestInstallMarketplacePlugin(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + th := Setup(t).InitBasic() defer th.TearDown() @@ -1639,6 +1729,9 @@ func TestInstallMarketplacePlugin(t *testing.T) { } func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") + path, _ := fileutils.FindDir("tests") signatureFilename := "testplugin2.tar.gz.sig" diff --git a/server/channels/api4/system_test.go b/server/channels/api4/system_test.go index 6b3f1911f1..cff0e28c71 100644 --- a/server/channels/api4/system_test.go +++ b/server/channels/api4/system_test.go @@ -828,6 +828,8 @@ func TestPushNotificationAck(t *testing.T) { } func TestCompleteOnboarding(t *testing.T) { + os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false") + defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE") th := Setup(t) defer th.TearDown() diff --git a/server/channels/app/plugin.go b/server/channels/app/plugin.go index 0ff6335141..9bbbb4381a 100644 --- a/server/channels/app/plugin.go +++ b/server/channels/app/plugin.go @@ -552,7 +552,7 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) { func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) { plugins := map[string]*model.MarketplacePlugin{} - if *a.Config().PluginSettings.EnableRemoteMarketplace && !filter.LocalOnly { + if *a.Config().PluginSettings.EnableRemoteMarketplace && !a.Config().FeatureFlags.StreamlinedMarketplace && !filter.LocalOnly { p, appErr := a.getRemotePlugins() if appErr != nil { return nil, appErr @@ -561,20 +561,12 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m } if !filter.RemoteOnly { - // Some plugin don't work on cloud. The remote Marketplace is aware of this fact, - // but prepackaged plugins are not. Hence, on a cloud installation prepackaged plugins - // shouldn't be shown in the Marketplace modal. - // This is a short term fix. The long term solution is to have a separate set of - // prepacked plugins for cloud: https://mattermost.atlassian.net/browse/MM-31331. - license := a.Srv().License() - if license == nil || !license.IsCloud() { - appErr := a.mergePrepackagedPlugins(plugins) - if appErr != nil { - return nil, appErr - } + appErr := a.mergePrepackagedPlugins(plugins) + if appErr != nil { + return nil, appErr } - appErr := a.mergeLocalPlugins(plugins) + appErr = a.mergeLocalPlugins(plugins) if appErr != nil { return nil, appErr } diff --git a/server/channels/app/plugin_install.go b/server/channels/app/plugin_install.go index f27e109e1e..74e3bee21b 100644 --- a/server/channels/app/plugin_install.go +++ b/server/channels/app/plugin_install.go @@ -289,7 +289,7 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl signatureFile = bytes.NewReader(prepackagedPlugin.Signature) } - if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace { + if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace && !ch.cfgSvc.Config().FeatureFlags.StreamlinedMarketplace { var plugin *model.BaseMarketplacePlugin plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version) // The plugin might only be prepackaged and not on the Marketplace. diff --git a/server/public/model/feature_flags.go b/server/public/model/feature_flags.go index 73b7ebf8d7..20a07e70cf 100644 --- a/server/public/model/feature_flags.go +++ b/server/public/model/feature_flags.go @@ -48,6 +48,8 @@ type FeatureFlags struct { EnableExportDirectDownload bool DataRetentionConcurrencyEnabled bool + + StreamlinedMarketplace bool } func (f *FeatureFlags) SetDefaults() { @@ -65,6 +67,7 @@ func (f *FeatureFlags) SetDefaults() { f.CloudReverseTrial = false f.EnableExportDirectDownload = false f.DataRetentionConcurrencyEnabled = true + f.StreamlinedMarketplace = true } // ToMap returns the feature flags as a map[string]string diff --git a/webapp/channels/src/components/admin_console/plugin_management/index.ts b/webapp/channels/src/components/admin_console/plugin_management/index.ts index 642d88e14c..77e0cbb347 100644 --- a/webapp/channels/src/components/admin_console/plugin_management/index.ts +++ b/webapp/channels/src/components/admin_console/plugin_management/index.ts @@ -18,12 +18,14 @@ import {GenericAction} from 'mattermost-redux/types/actions'; import {appsFeatureFlagEnabled} from 'mattermost-redux/selectors/entities/apps'; import PluginManagement from './plugin_management'; +import {streamlinedMarketplaceEnabled} from 'mattermost-redux/selectors/entities/preferences'; function mapStateToProps(state: any) { return { plugins: state.entities.admin.plugins, pluginStatuses: state.entities.admin.pluginStatuses, appsFeatureFlagEnabled: appsFeatureFlagEnabled(state), + streamlinedMarketplaceFlagEnabled: streamlinedMarketplaceEnabled(state), }; } diff --git a/webapp/channels/src/components/admin_console/plugin_management/plugin_management.test.tsx b/webapp/channels/src/components/admin_console/plugin_management/plugin_management.test.tsx index 7fdf19df6d..673594b208 100644 --- a/webapp/channels/src/components/admin_console/plugin_management/plugin_management.test.tsx +++ b/webapp/channels/src/components/admin_console/plugin_management/plugin_management.test.tsx @@ -91,6 +91,7 @@ describe('components/PluginManagement', () => { }, }, appsFeatureFlagEnabled: false, + streamlinedMarketplaceFlagEnabled: false, actions: { uploadPlugin: jest.fn(), installPluginFromUrl: jest.fn(), @@ -234,6 +235,7 @@ describe('components/PluginManagement', () => { pluginStatuses: {}, plugins: {}, appsFeatureFlagEnabled: false, + streamlinedMarketplaceFlagEnabled: false, actions: { uploadPlugin: jest.fn(), installPluginFromUrl: jest.fn(), @@ -324,6 +326,7 @@ describe('components/PluginManagement', () => { }, }, appsFeatureFlagEnabled: false, + streamlinedMarketplaceFlagEnabled: false, actions: { uploadPlugin: jest.fn(), installPluginFromUrl: jest.fn(), @@ -381,6 +384,7 @@ describe('components/PluginManagement', () => { }, }, appsFeatureFlagEnabled: false, + streamlinedMarketplaceFlagEnabled: false, actions: { uploadPlugin: jest.fn(), installPluginFromUrl: jest.fn(), @@ -438,6 +442,7 @@ describe('components/PluginManagement', () => { }, }, appsFeatureFlagEnabled: false, + streamlinedMarketplaceFlagEnabled: false, actions: { uploadPlugin: jest.fn(), installPluginFromUrl: jest.fn(), @@ -497,6 +502,7 @@ describe('components/PluginManagement', () => { }, }, appsFeatureFlagEnabled: false, + streamlinedMarketplaceFlagEnabled: false, actions: { uploadPlugin: jest.fn(), installPluginFromUrl: jest.fn(), diff --git a/webapp/channels/src/components/admin_console/plugin_management/plugin_management.tsx b/webapp/channels/src/components/admin_console/plugin_management/plugin_management.tsx index db442c65e5..a0e7340f8a 100644 --- a/webapp/channels/src/components/admin_console/plugin_management/plugin_management.tsx +++ b/webapp/channels/src/components/admin_console/plugin_management/plugin_management.tsx @@ -205,16 +205,17 @@ const PluginItem = ({ className={deactivating || isDisabled ? 'disabled' : ''} onClick={handleDisable} > - {deactivating ? + {deactivating ? ( : + /> + ) : ( - } + )} ); } else { @@ -224,16 +225,17 @@ const PluginItem = ({ className={activating || isDisabled ? 'disabled' : ''} onClick={handleEnable} > - {activating ? + {activating ? ( : + /> + ) : ( - } + )} ); } @@ -415,6 +417,7 @@ type Props = BaseProps & { pluginStatuses: Record; plugins: any; appsFeatureFlagEnabled: boolean; + streamlinedMarketplaceFlagEnabled: boolean; actions: { uploadPlugin: (fileData: File, force: boolean) => any; removePlugin: (pluginId: string) => any; @@ -1214,39 +1217,43 @@ export default class PluginManagement extends AdminSettings { onChange={this.handleChange} setByEnv={this.isSetByEnv('PluginSettings.EnableMarketplace')} /> - + + } + helpText={ + + } + value={this.state.enableRemoteMarketplace} + disabled={this.props.isDisabled || !this.state.enable || !this.state.enableUploads || !this.state.enableMarketplace} + onChange={this.handleChange} + setByEnv={this.isSetByEnv('PluginSettings.EnableRemoteMarketplace')} /> - } - helpText={ - + } + helpText={this.getMarketplaceURLHelpText(this.state.marketplaceUrl, this.state.enableUploads)} + value={this.state.marketplaceUrl} + disabled={this.props.isDisabled || !this.state.enable || !this.state.enableUploads || !this.state.enableMarketplace || !this.state.enableRemoteMarketplace} + onChange={this.handleChange} + setByEnv={this.isSetByEnv('PluginSettings.MarketplaceURL')} /> - } - value={this.state.enableRemoteMarketplace} - disabled={this.props.isDisabled || !this.state.enable || !this.state.enableUploads || !this.state.enableMarketplace} - onChange={this.handleChange} - setByEnv={this.isSetByEnv('PluginSettings.EnableRemoteMarketplace')} - /> - - } - helpText={this.getMarketplaceURLHelpText(this.state.marketplaceUrl, this.state.enableUploads)} - value={this.state.marketplaceUrl} - disabled={this.props.isDisabled || !this.state.enable || !this.state.enableUploads || !this.state.enableMarketplace || !this.state.enableRemoteMarketplace} - onChange={this.handleChange} - setByEnv={this.isSetByEnv('PluginSettings.MarketplaceURL')} - /> + + )} )} {pluginsContainer} diff --git a/webapp/channels/src/components/plugin_marketplace/__snapshots__/marketplace_modal.test.tsx.snap b/webapp/channels/src/components/plugin_marketplace/__snapshots__/marketplace_modal.test.tsx.snap index 0321f32193..a52754423a 100644 --- a/webapp/channels/src/components/plugin_marketplace/__snapshots__/marketplace_modal.test.tsx.snap +++ b/webapp/channels/src/components/plugin_marketplace/__snapshots__/marketplace_modal.test.tsx.snap @@ -1,5 +1,225 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`components/marketplace/ doesn't show web marketplace banner in FeatureFlags.StreamlinedMarketplace for Cloud 1`] = ` + +
+ +
+

+ App Marketplace +

+
+
+ +
+ + + Error connecting to the marketplace server. Please check your settings in the + + System Console + + . + +
+
+ +
+
+
+
+`; + +exports[`components/marketplace/ hides search, shows web marketplace banner in FeatureFlags.StreamlinedMarketplace 1`] = ` + +
+ +
+

+ App Marketplace +

+
+
+ +
+ + + Error connecting to the marketplace server. Please check your settings in the + + System Console + + . + +
+
+ +
+
+ +
+
+`; + exports[`components/marketplace/ should render default 1`] = `
{ entities: { general: { firstAdminCompleteSetup: false, + config: { + FeatureFlagStreamlinedMarketplace: 'false', + }, + license: { + Cloud: 'false', + }, }, admin: { pluginStatuses: {}, @@ -156,4 +163,49 @@ describe('components/marketplace/', () => { expect(wrapper.shallow()).toMatchSnapshot(); }); + + test('hides search, shows web marketplace banner in FeatureFlags.StreamlinedMarketplace', () => { + const setState = jest.fn(); + const useStateSpy = jest.spyOn(React, 'useState'); + useStateSpy.mockImplementation(() => [true, setState]); + + mockState.views.marketplace.plugins = [ + samplePlugin, + sampleInstalledPlugin, + ]; + + (mockState.entities.general.config as any).FeatureFlagStreamlinedMarketplace = 'true'; + + const wrapper = shallow( + , + ); + + wrapper.update(); + const content = wrapper.shallow(); + + expect(content.exists('#searchMarketplaceTextbox')).toBe(false); + expect(content.exists(WebMarketplaceBanner)).toBe(true); + + expect(content).toMatchSnapshot(); + }); + + test("doesn't show web marketplace banner in FeatureFlags.StreamlinedMarketplace for Cloud", () => { + const setState = jest.fn(); + const useStateSpy = jest.spyOn(React, 'useState'); + useStateSpy.mockImplementation(() => [true, setState]); + + (mockState.entities.general.config as any).FeatureFlagStreamlinedMarketplace = 'true'; + mockState.entities.general.license.Cloud = 'true'; + + const wrapper = shallow( + , + ); + + wrapper.update(); + const content = wrapper.shallow(); + + expect(content.exists(WebMarketplaceBanner)).toBe(false); + + expect(content).toMatchSnapshot(); + }); }); diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx index 23fd7a69f0..03445ea59d 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx @@ -13,7 +13,8 @@ import {MagnifyIcon} from '@mattermost/compass-icons/components'; import {FooterPagination, GenericModal} from '@mattermost/components'; import {getPluginStatuses} from 'mattermost-redux/actions/admin'; import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general'; -import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general'; +import {getFirstAdminVisitMarketplaceStatus, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {streamlinedMarketplaceEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {ActionResult} from 'mattermost-redux/types/actions'; import {fetchListing, filterListing} from 'actions/marketplace'; @@ -27,10 +28,13 @@ import {getListing, getInstalledListing} from 'selectors/views/marketplace'; import {isModalOpen} from 'selectors/views/modals'; import {GlobalState} from 'types/store'; import {ModalIdentifiers} from 'utils/constants'; +import WebMarketplaceBanner from './web_marketplace_banner'; +import {isCloudLicense} from 'utils/license_utils'; import './marketplace_modal.scss'; import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list/marketplace_list'; +import classNames from 'classnames'; const MarketplaceTabs = { ALL_LISTING: 'all', @@ -63,6 +67,9 @@ const MarketplaceModal = ({ const installedListing = useSelector(getInstalledListing); const pluginStatuses = useSelector((state: GlobalState) => state.entities.admin.pluginStatuses); const hasFirstAdminVisitedMarketplace = useSelector(getFirstAdminVisitMarketplaceStatus); + const isStreamlinedMarketplaceEnabled = useSelector(streamlinedMarketplaceEnabled); + const license = useSelector(getLicense); + const isCloud = isCloudLicense(license); const [tabKey, setTabKey] = useState(MarketplaceTabs.ALL_LISTING); const [filter, setFilter] = useState(''); @@ -162,39 +169,62 @@ const MarketplaceModal = ({ handleChangeTab(MarketplaceTabs.ALL_LISTING); }, [handleChangeTab]); - const getHeaderInput = useCallback(() => ( - } - placeholder={formatMessage({id: 'marketplace_modal.search', defaultMessage: 'Search marketplace'})} - useLegend={false} - autoFocus={true} - clearable={true} - value={filter} - onChange={handleOnChange} - onClear={handleOnClear} - /> - ), [filter, handleOnChange, handleOnClear]); + const getHeaderInput = useCallback(() => { + if (isStreamlinedMarketplaceEnabled) { + return null; + } - const getFooterContent = useCallback(() => ( - - ), [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey]); + return ( + } + placeholder={formatMessage({id: 'marketplace_modal.search', defaultMessage: 'Search marketplace'})} + useLegend={false} + autoFocus={true} + clearable={true} + value={filter} + onChange={handleOnChange} + onClear={handleOnClear} + /> + ); + }, [filter, handleOnChange, handleOnClear]); + + const getFooterContent = useCallback(() => { + if (isStreamlinedMarketplaceEnabled && listing.length <= ITEMS_PER_PAGE) { + return null; + } + + return ( + + ); + }, [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey, isStreamlinedMarketplaceEnabled]); + + const getAppendedContent = useCallback(() => { + if (!isStreamlinedMarketplaceEnabled || isCloud) { + return null; + } + + return ; + }, [isStreamlinedMarketplaceEnabled, isCloud]); return ( - - - {loading ? ( - - ) : ( - - )} - - + {isStreamlinedMarketplaceEnabled ? ( + <> - - + + ) : ( + + + {loading ? ( + + ) : ( + + )} + + + + + + )} ); }; diff --git a/webapp/channels/src/components/plugin_marketplace/web_marketplace_banner.tsx b/webapp/channels/src/components/plugin_marketplace/web_marketplace_banner.tsx new file mode 100644 index 0000000000..a3868fee15 --- /dev/null +++ b/webapp/channels/src/components/plugin_marketplace/web_marketplace_banner.tsx @@ -0,0 +1,109 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {useIntl} from 'react-intl'; +import styled from 'styled-components'; +import ExternalLink from '../external_link'; + +import webMarketplaceBannerBackground from 'images/marketplace-notice-background.jpg'; +import pluginIconConfluence from 'images/icons/confluence.svg'; +import pluginIconGiphy from 'images/icons/giphy.svg'; +import pluginIconPagerDuty from 'images/icons/pager-duty.svg'; + +import {ArrowRightIcon} from '@mattermost/compass-icons/components'; + +const WEB_MARKETPLACE_LINK = 'https://mattermost.com/marketplace'; + +const WebMarketplaceBanner = () => { + const {formatMessage} = useIntl(); + + return ( + + + + {formatMessage({id: 'marketplace_modal.web_marketplace_link.title', defaultMessage: 'Explore Community Integrations'})} + <ArrowRightIcon size={24}/> + + + {formatMessage({id: 'marketplace_modal.web_marketplace_link.desc', defaultMessage: 'We have dozens of community integrations available. So definitely do check them out!'})} + + + + + + + + + ); +}; + +const ExternalBannerLink = styled(ExternalLink)` + &&, + &&:hover, + &&:focus { + color: var(--denim-center-channel-bg, #FFF); + text-decoration: none; + } + && { + display: grid; + grid-template-columns: auto auto; + justify-content: space-between; + text-align: left; + padding: 24px 32px; + } +`; + +const WebMarketplaceBannerRoot = styled.section` + background-image: url(${webMarketplaceBannerBackground}); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + border-radius: 0 0 12px 12px !important; + margin: -1px; +`; + +const Title = styled.div` + font-family: Metropolis; + font-size: 16px; + font-style: normal; + font-weight: 600; + line-height: 24px; + margin: 4px 0; + grid-column: 1; + + svg { + vertical-align: middle; + display: inline-block; + margin-left: 4px; + } +`; + +const Description = styled.p` + font-family: Open Sans; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + grid-column: 1; + margin-bottom: 4px; +`; + +const PluginIcon = styled.img` + width: 50px; + height: 50px; + border-radius: 50%; +`; + +const IconsContainer = styled.div` + grid-column: 2; + grid-row: span 2/2; + ${PluginIcon}:nth-child(n+2) { + margin-left: calc(-54px / 1/4); + } +`; + +export default WebMarketplaceBanner; diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 787d8db7cf..14f46c88a3 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -3949,6 +3949,8 @@ "marketplace_modal.tabs.all_listing": "All", "marketplace_modal.tabs.installed_listing": "Installed ({count})", "marketplace_modal.title": "App Marketplace", + "marketplace_modal.web_marketplace_link.desc": "We have dozens of community integrations available. So definitely do check them out!", + "marketplace_modal.web_marketplace_link.title": "Explore Community Integrations", "menu.cloudFree.enterpriseTrialDescription": "Your trial is active until {trialEndDay}. Discover our top Enterprise features. Learn more", "menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial", "menu.cloudFree.postTrial.tryEnterprise": "Interested in a limitless plan with high-security features? See plans", diff --git a/webapp/channels/src/images/icons/confluence.svg b/webapp/channels/src/images/icons/confluence.svg new file mode 100644 index 0000000000..934c65b374 --- /dev/null +++ b/webapp/channels/src/images/icons/confluence.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/channels/src/images/icons/giphy.svg b/webapp/channels/src/images/icons/giphy.svg new file mode 100644 index 0000000000..78669eeaa5 --- /dev/null +++ b/webapp/channels/src/images/icons/giphy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/channels/src/images/icons/pager-duty.svg b/webapp/channels/src/images/icons/pager-duty.svg new file mode 100644 index 0000000000..d0d0f42aab --- /dev/null +++ b/webapp/channels/src/images/icons/pager-duty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/channels/src/images/marketplace-notice-background.jpg b/webapp/channels/src/images/marketplace-notice-background.jpg new file mode 100644 index 0000000000..8f6169fcef Binary files /dev/null and b/webapp/channels/src/images/marketplace-notice-background.jpg differ diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts index 1dc6b8f9f2..7ac235685e 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts @@ -281,3 +281,7 @@ export function deprecateCloudFree(state: GlobalState): boolean { export function cloudReverseTrial(state: GlobalState): boolean { return getFeatureFlagValue(state, 'CloudReverseTrial') === 'true'; } + +export function streamlinedMarketplaceEnabled(state: GlobalState): boolean { + return getFeatureFlagValue(state, 'StreamlinedMarketplace') === 'true'; +} diff --git a/webapp/platform/components/src/generic_modal/generic_modal.scss b/webapp/platform/components/src/generic_modal/generic_modal.scss index 9e70d63799..7a9ae57afa 100644 --- a/webapp/platform/components/src/generic_modal/generic_modal.scss +++ b/webapp/platform/components/src/generic_modal/generic_modal.scss @@ -22,6 +22,10 @@ max-height: 100%; padding: 0; + &.divider { + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08); + } + .form-control { height: 40px; box-sizing: border-box; @@ -106,7 +110,7 @@ border-radius: 4px; &.divider { - border-top: 1px solid rgba(63, 67, 80, 0.08); + border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08); } } diff --git a/webapp/platform/components/src/generic_modal/generic_modal.tsx b/webapp/platform/components/src/generic_modal/generic_modal.tsx index 3bca20efd0..18421b9ebf 100644 --- a/webapp/platform/components/src/generic_modal/generic_modal.tsx +++ b/webapp/platform/components/src/generic_modal/generic_modal.tsx @@ -39,8 +39,10 @@ export type Props = { keyboardEscape?: boolean; headerInput?: React.ReactNode; bodyPadding?: boolean; + bodyDivider?: boolean; footerContent?: React.ReactNode; footerDivider?: boolean; + appendedContent?: React.ReactNode; headerButton?: React.ReactNode; }; @@ -199,7 +201,7 @@ export class GenericModal extends React.PureComponent { )} - + {this.props.compassDesign ? ( this.props.errorText && (
@@ -226,6 +228,7 @@ export class GenericModal extends React.PureComponent { )} )} + {Boolean(this.props.appendedContent) && this.props.appendedContent}
);