From 383e522127da2df0ad6e61a13b2460634ed42650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Fri, 14 Jul 2023 09:49:34 +0100 Subject: [PATCH] feat: Slack App addon (#4238) https://linear.app/unleash/issue/2-1232/implement-first-iteration-of-the-new-slack-app-addon This PR implements the first iteration of the new Slack App addon. Unlike the old Slack addon, this one uses a Slack App (bot) that is installed to Slack workspaces in order to post messages. This uses `@slack/web-api`, which internally uses the latest Slack API endpoints like `postMessage`. This is currently behind a flag: `slackAppAddon`. The current flow is that the Unleash Slack App is installed from whatever source: - Unleash addons page; - Direct link; - https://unleash-slack-app.vercel.app/ (temporary URL); - Slack App Directory (in the future); - Etc; After installed, we resolve the authorization to an `access_token` that the user can paste into the Unleash Slack App addon configuration form. https://github.com/Unleash/unleash/assets/14320932/6a6621b9-5b8a-4921-a279-30668be6d46c Co-authored by: @daveleek --------- Co-authored-by: David Leek --- .../addons/AddonForm/AddonForm.styles.tsx | 6 + .../component/addons/AddonForm/AddonForm.tsx | 30 ++++- .../AddonForm/AddonInstall/AddonInstall.tsx | 39 +++++++ .../addons/AddonList/AddonIcon/AddonIcon.tsx | 1 + .../AddonList/AddonNameCell/AddonNameCell.tsx | 35 ++++++ .../AvailableAddons/AvailableAddons.tsx | 37 +++--- .../ConfigureAddonButton.tsx | 11 +- .../ConfiguredAddons/ConfiguredAddons.tsx | 25 ++-- frontend/src/interfaces/addons.ts | 15 +++ package.json | 1 + src/lib/addons/addon-schema.ts | 12 ++ src/lib/addons/index.ts | 24 +++- .../addons/installation-definition-schema.ts | 7 ++ src/lib/addons/slack-app-definition.ts | 77 +++++++++++++ src/lib/addons/slack-app.ts | 91 +++++++++++++++ src/lib/openapi/spec/addon-type-schema.ts | 60 ++++++++++ src/lib/services/addon-service.ts | 7 +- src/lib/types/model.ts | 14 +++ src/server-dev.ts | 1 + src/test/e2e/api/admin/addon.e2e.test.ts | 3 +- yarn.lock | 108 ++++++++++++++++++ 21 files changed, 562 insertions(+), 42 deletions(-) create mode 100644 frontend/src/component/addons/AddonForm/AddonInstall/AddonInstall.tsx create mode 100644 frontend/src/component/addons/AddonList/AddonNameCell/AddonNameCell.tsx create mode 100644 src/lib/addons/installation-definition-schema.ts create mode 100644 src/lib/addons/slack-app-definition.ts create mode 100644 src/lib/addons/slack-app.ts diff --git a/frontend/src/component/addons/AddonForm/AddonForm.styles.tsx b/frontend/src/component/addons/AddonForm/AddonForm.styles.tsx index 03dd7f5d76..c55f022500 100644 --- a/frontend/src/component/addons/AddonForm/AddonForm.styles.tsx +++ b/frontend/src/component/addons/AddonForm/AddonForm.styles.tsx @@ -12,6 +12,12 @@ export const StyledFormSection = styled('section')({ marginBottom: '36px', }); +export const StyledAlerts = styled(StyledFormSection)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), +})); + export const StyledHelpText = styled('p')({ marginBottom: '0.5rem', }); diff --git a/frontend/src/component/addons/AddonForm/AddonForm.tsx b/frontend/src/component/addons/AddonForm/AddonForm.tsx index 2baaf1a8e2..adedfae8a5 100644 --- a/frontend/src/component/addons/AddonForm/AddonForm.tsx +++ b/frontend/src/component/addons/AddonForm/AddonForm.tsx @@ -1,4 +1,4 @@ -import React, { +import { ChangeEventHandler, FormEventHandler, MouseEventHandler, @@ -6,11 +6,18 @@ import React, { useState, VFC, } from 'react'; -import { Button, Divider, FormControlLabel, Switch } from '@mui/material'; +import { + Alert, + Button, + Divider, + FormControlLabel, + Switch, +} from '@mui/material'; import produce from 'immer'; import { trim } from 'component/common/util'; import { IAddon, IAddonProvider } from 'interfaces/addons'; import { AddonParameters } from './AddonParameters/AddonParameters'; +import { AddonInstall } from './AddonInstall/AddonInstall'; import cloneDeep from 'lodash.clonedeep'; import { useNavigate } from 'react-router-dom'; import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi'; @@ -29,6 +36,7 @@ import { import { StyledForm, StyledFormSection, + StyledAlerts, StyledHelpText, StyledTextField, StyledContainer, @@ -37,6 +45,7 @@ import { } from './AddonForm.styles'; import { useTheme } from '@mui/system'; import { GO_BACK } from 'constants/navigate'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; interface IAddonFormProps { provider?: IAddonProvider; @@ -241,6 +250,8 @@ export const AddonForm: VFC = ({ name, description, documentationUrl = 'https://unleash.github.io/docs/addons', + installation, + alerts, } = provider ? provider : ({} as Partial); return ( @@ -253,6 +264,21 @@ export const AddonForm: VFC = ({ > + + {alerts?.map(({ type, text }) => ( + {text} + ))} + + ( + + )} + /> { + return ( + + + {title} + {helpText} + + + + ); +}; diff --git a/frontend/src/component/addons/AddonList/AddonIcon/AddonIcon.tsx b/frontend/src/component/addons/AddonList/AddonIcon/AddonIcon.tsx index 2fb1ccfd5e..427a6ca7ba 100644 --- a/frontend/src/component/addons/AddonList/AddonIcon/AddonIcon.tsx +++ b/frontend/src/component/addons/AddonList/AddonIcon/AddonIcon.tsx @@ -21,6 +21,7 @@ interface IAddonIconProps { export const AddonIcon = ({ name }: IAddonIconProps) => { switch (name) { case 'slack': + case 'slack-app': return ( ({ + cursor: 'pointer', + marginLeft: theme.spacing(1), +})); + +interface IAddonNameCellProps { + provider: Pick< + IAddonProvider, + 'displayName' | 'description' | 'deprecated' + >; +} + +export const AddonNameCell = ({ provider }: IAddonNameCellProps) => ( + + Deprecated + + } + /> + } + /> +); diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.tsx b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.tsx index b069130cf3..83239de237 100644 --- a/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.tsx +++ b/frontend/src/component/addons/AddonList/AvailableAddons/AvailableAddons.tsx @@ -11,7 +11,6 @@ import { } from 'component/common/Table'; import { useTable, useSortBy } from 'react-table'; -import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { sortTypes } from 'utils/sortTypes'; @@ -19,6 +18,8 @@ import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; import { ConfigureAddonButton } from './ConfigureAddonButton/ConfigureAddonButton'; import { AddonIcon } from '../AddonIcon/AddonIcon'; +import { AddonNameCell } from '../AddonNameCell/AddonNameCell'; +import { IAddonInstallation } from 'interfaces/addons'; interface IProvider { name: string; @@ -27,6 +28,8 @@ interface IProvider { documentationUrl: string; parameters: object[]; events: string[]; + installation?: IAddonInstallation; + deprecated?: string; } interface IAvailableAddonsProps { @@ -46,11 +49,15 @@ export const AvailableAddons = ({ }); } - return providers.map(({ name, displayName, description }) => ({ - name, - displayName, - description, - })); + return providers.map( + ({ name, displayName, description, deprecated, installation }) => ({ + name, + displayName, + description, + deprecated, + installation, + }) + ); }, [providers, loading]); const columns = useMemo( @@ -71,19 +78,9 @@ export const AvailableAddons = ({ Header: 'Name', accessor: 'name', width: '90%', - Cell: ({ - row: { - original: { name, description }, - }, - }: any) => { - return ( - - ); - }, + Cell: ({ row: { original } }: any) => ( + + ), sortType: 'alphanumeric', }, { @@ -91,7 +88,7 @@ export const AvailableAddons = ({ align: 'center', Cell: ({ row: { original } }: any) => ( - + ), width: 150, diff --git a/frontend/src/component/addons/AddonList/AvailableAddons/ConfigureAddonButton/ConfigureAddonButton.tsx b/frontend/src/component/addons/AddonList/AvailableAddons/ConfigureAddonButton/ConfigureAddonButton.tsx index 10de7b0a56..dfe10193c3 100644 --- a/frontend/src/component/addons/AddonList/AvailableAddons/ConfigureAddonButton/ConfigureAddonButton.tsx +++ b/frontend/src/component/addons/AddonList/AvailableAddons/ConfigureAddonButton/ConfigureAddonButton.tsx @@ -1,19 +1,24 @@ import PermissionButton from 'component/common/PermissionButton/PermissionButton'; import { CREATE_ADDON } from 'component/providers/AccessProvider/permissions'; +import { IAddonProvider } from 'interfaces/addons'; import { useNavigate } from 'react-router-dom'; interface IConfigureAddonButtonProps { - name: string; + provider: IAddonProvider; } -export const ConfigureAddonButton = ({ name }: IConfigureAddonButtonProps) => { +export const ConfigureAddonButton = ({ + provider, +}: IConfigureAddonButtonProps) => { const navigate = useNavigate(); return ( navigate(`/addons/create/${name}`)} + onClick={() => { + navigate(`/addons/create/${provider.name}`); + }} > Configure diff --git a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx index 8d4e69466d..c845bb98ab 100644 --- a/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx +++ b/frontend/src/component/addons/AddonList/ConfiguredAddons/ConfiguredAddons.tsx @@ -8,7 +8,6 @@ import useAddonsApi from 'hooks/api/actions/useAddonsApi/useAddonsApi'; import { IAddon } from 'interfaces/addons'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { formatUnknownError } from 'utils/formatUnknownError'; -import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; import { sortTypes } from 'utils/sortTypes'; import { useTable, useSortBy } from 'react-table'; import { PageHeader } from 'component/common/PageHeader/PageHeader'; @@ -16,9 +15,10 @@ import { SortableTableHeader, TablePlaceholder } from 'component/common/Table'; import { IconCell } from 'component/common/Table/cells/IconCell/IconCell'; import { AddonIcon } from '../AddonIcon/AddonIcon'; import { ConfiguredAddonsActionsCell } from './ConfiguredAddonsActionCell/ConfiguredAddonsActionsCell'; +import { AddonNameCell } from '../AddonNameCell/AddonNameCell'; export const ConfiguredAddons = () => { - const { refetchAddons, addons, loading } = useAddons(); + const { refetchAddons, addons, providers, loading } = useAddons(); const { updateAddon, removeAddon } = useAddonsApi(); const { setToastData, setToastApiError } = useToast(); const [showDelete, setShowDelete] = useState(false); @@ -84,15 +84,18 @@ export const ConfiguredAddons = () => { row: { original: { provider, description }, }, - }: any) => { - return ( - - ); - }, + }: any) => ( + name === provider + ) || { + displayName: provider, + }), + description, + }} + /> + ), sortType: 'alphanumeric', }, { diff --git a/frontend/src/interfaces/addons.ts b/frontend/src/interfaces/addons.ts index b0c1151f85..596980790a 100644 --- a/frontend/src/interfaces/addons.ts +++ b/frontend/src/interfaces/addons.ts @@ -19,6 +19,21 @@ export interface IAddonProvider { name: string; parameters: IAddonProviderParams[]; tagTypes: ITagType[]; + installation?: IAddonInstallation; + alerts?: IAddonAlert[]; + deprecated?: string; +} + +export interface IAddonInstallation { + url: string; + warning?: string; + title?: string; + helpText?: string; +} + +export interface IAddonAlert { + type: 'success' | 'info' | 'warning' | 'error'; + text: string; } export interface IAddonProviderParams { diff --git a/package.json b/package.json index dc69f8ce10..3b2e34e721 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ ] }, "dependencies": { + "@slack/web-api": "^6.8.1", "@unleash/express-openapi": "^0.3.0", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", diff --git a/src/lib/addons/addon-schema.ts b/src/lib/addons/addon-schema.ts index 540060e6a5..bc928b38b2 100644 --- a/src/lib/addons/addon-schema.ts +++ b/src/lib/addons/addon-schema.ts @@ -1,12 +1,14 @@ import joi from 'joi'; import { nameType } from '../routes/util'; import { tagTypeSchema } from '../services/tag-type-schema'; +import { installationDefinitionSchema } from './installation-definition-schema'; export const addonDefinitionSchema = joi.object().keys({ name: nameType, displayName: joi.string(), documentationUrl: joi.string().uri({ scheme: [/https?/] }), description: joi.string().allow(''), + deprecated: joi.boolean().optional().default(false), parameters: joi .array() .optional() @@ -23,4 +25,14 @@ export const addonDefinitionSchema = joi.object().keys({ ), events: joi.array().optional().items(joi.string()), tagTypes: joi.array().optional().items(tagTypeSchema), + installation: installationDefinitionSchema.optional(), + alerts: joi + .array() + .optional() + .items( + joi.object().keys({ + type: joi.string().valid('success', 'info', 'warning', 'error'), + text: joi.string().required(), + }), + ), }); diff --git a/src/lib/addons/index.ts b/src/lib/addons/index.ts index 81a1423d10..5d9dac21d3 100644 --- a/src/lib/addons/index.ts +++ b/src/lib/addons/index.ts @@ -4,6 +4,8 @@ import TeamsAddon from './teams'; import DatadogAddon from './datadog'; import Addon from './addon'; import { LogProvider } from '../logger'; +import SlackAppAddon from './slack-app'; +import { IFlagResolver } from '../types'; export interface IAddonProviders { [key: string]: Addon; @@ -12,14 +14,28 @@ export interface IAddonProviders { export const getAddons: (args: { getLogger: LogProvider; unleashUrl: string; - newFeatureLink?: boolean; -}) => IAddonProviders = ({ getLogger, unleashUrl }) => { - const addons = [ + flagResolver: IFlagResolver; +}) => IAddonProviders = ({ getLogger, unleashUrl, flagResolver }) => { + const slackAppAddonEnabled = flagResolver.isEnabled('slackAppAddon'); + + const slackAddon = new SlackAddon({ getLogger, unleashUrl }); + + if (slackAppAddonEnabled) { + slackAddon.definition.deprecated = + 'This addon is deprecated. Please try the new Slack App addon instead.'; + } + + const addons: Addon[] = [ new Webhook({ getLogger }), - new SlackAddon({ getLogger, unleashUrl }), + slackAddon, new TeamsAddon({ getLogger, unleashUrl }), new DatadogAddon({ getLogger, unleashUrl }), ]; + + if (slackAppAddonEnabled) { + addons.push(new SlackAppAddon({ getLogger, unleashUrl })); + } + return addons.reduce((map, addon) => { // eslint-disable-next-line no-param-reassign map[addon.name] = addon; diff --git a/src/lib/addons/installation-definition-schema.ts b/src/lib/addons/installation-definition-schema.ts new file mode 100644 index 0000000000..a7fedb8f13 --- /dev/null +++ b/src/lib/addons/installation-definition-schema.ts @@ -0,0 +1,7 @@ +import joi from 'joi'; + +export const installationDefinitionSchema = joi.object().keys({ + url: joi.string().uri({ scheme: [/https?/] }), + title: joi.string().optional(), + helpText: joi.string().optional(), +}); diff --git a/src/lib/addons/slack-app-definition.ts b/src/lib/addons/slack-app-definition.ts new file mode 100644 index 0000000000..569bba35fd --- /dev/null +++ b/src/lib/addons/slack-app-definition.ts @@ -0,0 +1,77 @@ +import { + FEATURE_CREATED, + FEATURE_UPDATED, + FEATURE_ARCHIVED, + FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_PROJECT_CHANGE, + FEATURE_VARIANTS_UPDATED, +} from '../types/events'; +import { IAddonDefinition } from '../types/model'; + +const slackAppDefinition: IAddonDefinition = { + name: 'slack-app', + displayName: 'Slack App', + description: + 'The Unleash Slack App posts messages to your Slack workspace. You can decide which channels to post to by configuring your feature toggles with "slack" tags. For example, if you\'d like the bot to post messages to the #general channel, you should configure your feature toggle with the "slack:general" tag.', + documentationUrl: 'https://docs.getunleash.io/docs/addons/slack-app', + alerts: [ + { + type: 'info', + text: `The Unleash Slack App bot has access to public channels by default. If you want the bot to post messages to private channels, you'll need to invite it to those channels.`, + }, + { + type: 'warning', + text: `Please ensure you have the Unleash Slack App installed in your Slack workspace if you haven't installed it already.`, + }, + ], + installation: { + url: 'https://unleash-slack-app.vercel.app/install', + title: 'Slack App installation', + helpText: + 'After installing the Unleash Slack app in your Slack workspace, paste the access token into the appropriate field below in order to configure this addon.', + }, + parameters: [ + { + name: 'accessToken', + displayName: 'Access token', + description: '(Required)', + type: 'text', + required: true, + sensitive: true, + }, + ], + events: [ + FEATURE_CREATED, + FEATURE_UPDATED, + FEATURE_ARCHIVED, + FEATURE_REVIVED, + FEATURE_STALE_ON, + FEATURE_STALE_OFF, + FEATURE_ENVIRONMENT_ENABLED, + FEATURE_ENVIRONMENT_DISABLED, + FEATURE_STRATEGY_REMOVE, + FEATURE_STRATEGY_UPDATE, + FEATURE_STRATEGY_ADD, + FEATURE_METADATA_UPDATED, + FEATURE_VARIANTS_UPDATED, + FEATURE_PROJECT_CHANGE, + ], + tagTypes: [ + { + name: 'slack', + description: + 'Slack tag used by the slack-addon to specify the slack channel.', + icon: 'S', + }, + ], +}; + +export default slackAppDefinition; diff --git a/src/lib/addons/slack-app.ts b/src/lib/addons/slack-app.ts new file mode 100644 index 0000000000..b44cfd1696 --- /dev/null +++ b/src/lib/addons/slack-app.ts @@ -0,0 +1,91 @@ +import { WebClient } from '@slack/web-api'; +import Addon from './addon'; + +import slackAppDefinition from './slack-app-definition'; +import { IAddonConfig } from '../types/model'; + +import { + FeatureEventFormatter, + FeatureEventFormatterMd, + LinkStyle, +} from './feature-event-formatter-md'; +import { IEvent } from '../types/events'; + +interface ISlackAppAddonParameters { + accessToken: string; +} +export default class SlackAppAddon extends Addon { + private msgFormatter: FeatureEventFormatter; + + private slackClient?: WebClient; + + constructor(args: IAddonConfig) { + super(slackAppDefinition, args); + this.msgFormatter = new FeatureEventFormatterMd( + args.unleashUrl, + LinkStyle.SLACK, + ); + } + + async handleEvent( + event: IEvent, + parameters: ISlackAppAddonParameters, + ): Promise { + const { accessToken } = parameters; + + if (!accessToken) return; + + if (!this.slackClient) { + this.slackClient = new WebClient(accessToken); + } + + const slackChannels = await this.slackClient.conversations.list({ + types: 'public_channel,private_channel', + }); + const taggedChannels = this.findTaggedChannels(event); + + if (slackChannels.channels?.length && taggedChannels.length) { + const slackChannelsToPostTo = slackChannels.channels.filter( + ({ id, name }) => id && name && taggedChannels.includes(name), + ); + + const text = this.msgFormatter.format(event); + const featureLink = this.msgFormatter.featureLink(event); + + const requests = slackChannelsToPostTo.map(({ id }) => + this.slackClient!.chat.postMessage({ + channel: id!, + text, + attachments: [ + { + actions: [ + { + name: 'featureToggle', + text: 'Open in Unleash', + type: 'button', + value: 'featureToggle', + style: 'primary', + url: featureLink, + }, + ], + }, + ], + }), + ); + + await Promise.all(requests); + this.logger.info(`Handled event ${event.type}.`); + } + } + + findTaggedChannels({ tags }: Pick): string[] { + if (tags) { + return tags + .filter((tag) => tag.type === 'slack') + .map((t) => t.value); + } + return []; + } +} + +module.exports = SlackAppAddon; diff --git a/src/lib/openapi/spec/addon-type-schema.ts b/src/lib/openapi/spec/addon-type-schema.ts index 5605f5b1ff..a97b7acd25 100644 --- a/src/lib/openapi/spec/addon-type-schema.ts +++ b/src/lib/openapi/spec/addon-type-schema.ts @@ -119,6 +119,66 @@ export const addonTypeSchema = { 'feature-project-change', ], }, + installation: { + type: 'object', + additionalProperties: false, + required: ['url'], + description: 'The installation configuration for this addon type.', + properties: { + url: { + type: 'string', + description: + 'A URL to where the addon configuration should redirect to install addons of this type.', + example: 'https://unleash-slack-app.vercel.app/install', + }, + title: { + type: 'string', + description: + 'The title of the installation configuration. This will be displayed to the user when installing addons of this type.', + example: 'Slack App installation', + }, + helpText: { + type: 'string', + description: + 'The help text of the installation configuration. This will be displayed to the user when installing addons of this type.', + example: + 'Clicking the Install button will send you to Slack to initiate the installation procedure for the Unleash Slack app for your workspace', + }, + }, + }, + alerts: { + type: 'array', + description: + 'A list of alerts to display to the user when installing addons of this type.', + items: { + type: 'object', + additionalProperties: false, + required: ['type', 'text'], + properties: { + type: { + type: 'string', + enum: ['success', 'info', 'warning', 'error'], + description: + 'The type of alert. This determines the color of the alert.', + example: 'info', + }, + text: { + type: 'string', + description: + 'The text of the alert. This is what will be displayed to the user.', + example: + "Please ensure you have the Unleash Slack App installed in your Slack workspace if you haven't installed it already. If you want the Unleash Slack App bot to post messages to private channels, you'll need to invite it to those channels.", + }, + }, + }, + }, + deprecated: { + type: 'string', + description: + 'This should be used to inform the user that this addon type is deprecated and should not be used. Deprecated addons will show a badge with this information on the UI.', + example: + 'This addon is deprecated. Please try the new addon instead.', + }, }, components: { schemas: { diff --git a/src/lib/services/addon-service.ts b/src/lib/services/addon-service.ts index 8251bda3b4..97bd695dac 100644 --- a/src/lib/services/addon-service.ts +++ b/src/lib/services/addon-service.ts @@ -49,7 +49,11 @@ export default class AddonService { IUnleashStores, 'addonStore' | 'eventStore' | 'featureToggleStore' >, - { getLogger, server }: Pick, + { + getLogger, + server, + flagResolver, + }: Pick, tagTypeService: TagTypeService, addons?: IAddonProviders, ) { @@ -64,6 +68,7 @@ export default class AddonService { getAddons({ getLogger, unleashUrl: server.unleashUrl, + flagResolver, }); this.sensitiveParams = this.loadSensitiveParams(this.addonProviders); if (addonStore) { diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 3b819c113d..3a8a5c942b 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -249,9 +249,23 @@ export interface IAddonDefinition { displayName: string; documentationUrl: string; description: string; + deprecated?: string; parameters?: IAddonParameterDefinition[]; events?: string[]; tagTypes?: ITagType[]; + installation?: IAddonInstallation; + alerts?: IAddonAlert[]; +} + +export interface IAddonInstallation { + url: string; + title?: string; + helpText?: string; +} + +export interface IAddonAlert { + type: 'success' | 'info' | 'warning' | 'error'; + text: string; } export interface IAddonConfig { diff --git a/src/server-dev.ts b/src/server-dev.ts index 6895fa3067..aa3139c656 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -43,6 +43,7 @@ process.nextTick(async () => { strategyVariant: true, newProjectLayout: true, emitPotentiallyStaleEvents: true, + slackAppAddon: true, }, }, authentication: { diff --git a/src/test/e2e/api/admin/addon.e2e.test.ts b/src/test/e2e/api/admin/addon.e2e.test.ts index 027bfc039c..5ead1b23dd 100644 --- a/src/test/e2e/api/admin/addon.e2e.test.ts +++ b/src/test/e2e/api/admin/addon.e2e.test.ts @@ -13,6 +13,7 @@ beforeAll(async () => { experimental: { flags: { strictSchemaValidation: true, + slackAppAddon: true, }, }, }); @@ -32,7 +33,7 @@ test('gets all addons', async () => { .expect(200) .expect((res) => { expect(res.body.addons.length).toBe(0); - expect(res.body.providers.length).toBe(4); + expect(res.body.providers.length).toBe(5); expect(res.body.providers[0].name).toBe('webhook'); }); }); diff --git a/yarn.lock b/yarn.lock index f5957ae33e..8d44f9c1b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1041,6 +1041,35 @@ dependencies: "@sinonjs/commons" "^2.0.0" +"@slack/logger@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@slack/logger/-/logger-3.0.0.tgz#b736d4e1c112c22a10ffab0c2d364620aedcb714" + integrity sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA== + dependencies: + "@types/node" ">=12.0.0" + +"@slack/types@^2.0.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@slack/types/-/types-2.8.0.tgz#11ea10872262a7e6f86f54e5bcd4f91e3a41fe91" + integrity sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ== + +"@slack/web-api@^6.8.1": + version "6.8.1" + resolved "https://registry.yarnpkg.com/@slack/web-api/-/web-api-6.8.1.tgz#c6c1e7405c884c4d9048f8b1d3901bd138d00610" + integrity sha512-eMPk2S99S613gcu7odSw/LV+Qxr8A+RXvBD0GYW510wJuTERiTjP5TgCsH8X09+lxSumbDE88wvWbuFuvGa74g== + dependencies: + "@slack/logger" "^3.0.0" + "@slack/types" "^2.0.0" + "@types/is-stream" "^1.1.0" + "@types/node" ">=12.0.0" + axios "^0.27.2" + eventemitter3 "^3.1.0" + form-data "^2.5.0" + is-electron "2.2.0" + is-stream "^1.1.0" + p-queue "^6.6.1" + p-retry "^4.0.0" + "@swc/core-darwin-arm64@1.3.67": version "1.3.67" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.67.tgz#8076dcd75862b92a7987a8da5a24986ab559d793" @@ -1267,6 +1296,13 @@ resolved "https://registry.yarnpkg.com/@types/hash-sum/-/hash-sum-1.0.0.tgz#838f4e8627887d42b162d05f3d96ca636c2bc504" integrity sha512-FdLBT93h3kcZ586Aee66HPCVJ6qvxVjBlDWNmxSGSbCZe9hTsjRKdSsl4y1T+3zfujxo9auykQMnFsfyHWD7wg== +"@types/is-stream@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" + integrity sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg== + dependencies: + "@types/node" "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -1363,6 +1399,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.37.tgz#a1f8728e4dc30163deb41e9b7aba65d0c2d4eda1" integrity sha512-ql+4dw4PlPFBP495k8JzUX/oMNRI2Ei4PrMHgj8oT4VhGlYUzF4EYr0qk2fW+XBVGIrq8Zzk13m4cvyXZuv4pA== +"@types/node@>=12.0.0": + version "20.4.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.1.tgz#a6033a8718653c50ac4962977e14d0f984d9527d" + integrity sha512-JIzsAvJeA/5iY6Y/OxZbv1lUcc8dNSE77lb2gnBH+/PJ3lFR1Ccvgwl5JWnHAkNHcRsT0TbpVOsiMKZ1F/yyJg== + "@types/nodemailer@6.4.8": version "6.4.8" resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.8.tgz#f06c661e9b201fc2acc3a00a0fded42ba7eaca9d" @@ -1409,6 +1450,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + "@types/semver@7.5.0": version "7.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" @@ -3174,6 +3220,16 @@ event-stream@=3.3.4: stream-combiner "~0.0.4" through "~2.3.1" +eventemitter3@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + +eventemitter3@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -3504,6 +3560,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -4110,6 +4175,11 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" +is-electron@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-electron/-/is-electron-2.2.0.tgz#8943084f09e8b731b3a7a0298a7b5d56f6b7eef0" + integrity sha512-SpMppC2XR3YdxSzczXReBjqs2zGscWQpBIKqwXYBFic0ERaxNVgwLCHwOLZeESfdJQjX0RDvrJ1lBXX2ij+G1Q== + is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -4221,6 +4291,11 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -5724,6 +5799,11 @@ owasp-password-strength-test@^1.3.0: resolved "https://registry.yarnpkg.com/owasp-password-strength-test/-/owasp-password-strength-test-1.3.0.tgz#4f629e42903e8f6d279b230d657ab61e58e44b12" integrity sha512-33/Z+vyjlFaVZsT7aAFe3SkQZdU6su59XNkYdU5o2Fssz0D9dt6uiFaMm62M7dFQSKogULq8UYvdKnHkeqNB2w== +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -5766,6 +5846,29 @@ p-map@^5.5.0: dependencies: aggregate-error "^4.0.0" +p-queue@^6.6.1: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-retry@^4.0.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -6425,6 +6528,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"