diff --git a/frontend/__snapshots__/scenes-app-apps--installed.png b/frontend/__snapshots__/scenes-app-apps--installed.png index 848a0e2dfdc2d..34ae3b5167bb3 100644 Binary files a/frontend/__snapshots__/scenes-app-apps--installed.png and b/frontend/__snapshots__/scenes-app-apps--installed.png differ diff --git a/frontend/src/scenes/plugins/AppsScene.tsx b/frontend/src/scenes/plugins/AppsScene.tsx index 664d7c643c42a..374681460c088 100644 --- a/frontend/src/scenes/plugins/AppsScene.tsx +++ b/frontend/src/scenes/plugins/AppsScene.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { useActions, useValues } from 'kea' import { pluginsLogic } from './pluginsLogic' import { PageHeader } from 'lib/components/PageHeader' -import { canViewPlugins } from './access' +import { canGloballyManagePlugins, canViewPlugins } from './access' import { userLogic } from 'scenes/userLogic' import { SceneExport } from 'scenes/sceneTypes' import { ActivityLog } from 'lib/components/ActivityLog/ActivityLog' @@ -15,6 +15,7 @@ import { LemonButton } from '@posthog/lemon-ui' import { urls } from 'scenes/urls' import './Plugins.scss' +import { AppsManagementTab } from './tabs/apps/AppsManagementTab' export const scene: SceneExport = { component: AppsScene, @@ -61,6 +62,11 @@ export function AppsScene(): JSX.Element | null { label: 'History', content: , }, + canGloballyManagePlugins(user?.organization) && { + key: PluginTab.AppsManagement, + label: 'Apps Management', + content: , + }, ]} /> diff --git a/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx b/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx new file mode 100644 index 0000000000000..6324af5a536b8 --- /dev/null +++ b/frontend/src/scenes/plugins/tabs/apps/AppManagementView.tsx @@ -0,0 +1,138 @@ +import { LemonButton, Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { IconCheckmark, IconCloudDownload } from 'lib/lemon-ui/icons' +import { PluginImage } from 'scenes/plugins/plugin/PluginImage' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { PluginTypeWithConfig, PluginRepositoryEntry, PluginInstallationType } from 'scenes/plugins/types' +import { PluginType } from '~/types' +import { PluginTags } from './components' +import { Tooltip } from 'lib/lemon-ui/Tooltip' +import { Popconfirm } from 'antd' +import { DeleteOutlined, GlobalOutlined, RollbackOutlined } from '@ant-design/icons' +import { canGloballyManagePlugins } from 'scenes/plugins/access' +import { userLogic } from 'scenes/userLogic' + +export function AppManagementView({ + plugin, +}: { + plugin: PluginTypeWithConfig | PluginType | PluginRepositoryEntry +}): JSX.Element { + const { user } = useValues(userLogic) + + if (!canGloballyManagePlugins(user?.organization)) { + return <> + } + const { installingPluginUrl, pluginsNeedingUpdates, pluginsUpdating, loading, unusedPlugins } = + useValues(pluginsLogic) + const { installPlugin, editPlugin, updatePlugin, uninstallPlugin, patchPlugin } = useActions(pluginsLogic) + + return ( +
+
+ +
+
+ + + {plugin.name} + + + +
+
{plugin.description}
+
+
+ +
+ {'id' in plugin ? ( + <> + {'updateStatus' in plugin && pluginsNeedingUpdates.find((x) => x.id === plugin.id) && ( + { + plugin.updateStatus?.updated ? editPlugin(plugin.id) : updatePlugin(plugin.id) + }} + loading={pluginsUpdating.includes(plugin.id)} + icon={plugin.updateStatus?.updated ? : } + > + {plugin.updateStatus?.updated ? 'Updated' : 'Update'} + + )} + uninstallPlugin(plugin.id)} + okText="Uninstall" + cancelText="Cancel" + className="Plugins__Popconfirm" + > + } + disabledReason={ + unusedPlugins.includes(plugin.id) ? undefined : 'This app is still in use.' + } + data-attr="plugin-uninstall" + > + Uninstall + + + {plugin.is_global ? ( + + This app can currently be used by other organizations in this instance of + PostHog. This action will disable and hide it for all organizations other + than yours. + + } + > + } + onClick={() => patchPlugin(plugin.id, { is_global: false })} + > + Make local + + + ) : ( + + This action will mark this app as installed for all organizations in this + instance of PostHog. + + } + > + } + onClick={() => patchPlugin(plugin.id, { is_global: true })} + > + Make global + + + )} + + ) : ( + } + size="small" + onClick={() => + plugin.url ? installPlugin(plugin.url, PluginInstallationType.Repository) : undefined + } + > + Install + + )} +
+
+ ) +} diff --git a/frontend/src/scenes/plugins/tabs/apps/AppView.tsx b/frontend/src/scenes/plugins/tabs/apps/AppView.tsx index 8a190a014c3af..f76dae1df1a38 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppView.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppView.tsx @@ -1,10 +1,8 @@ import { Link, LemonButton, LemonBadge } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { DeleteOutlined, GlobalOutlined, RollbackOutlined } from '@ant-design/icons' import { LemonMenuItem, LemonMenu } from 'lib/lemon-ui/LemonMenu' import { IconLink, - IconCheckmark, IconCloudDownload, IconSettings, IconEllipsis, @@ -19,27 +17,14 @@ import { urls } from 'scenes/urls' import { PluginType } from '~/types' import { PluginTags } from './components' import { Tooltip } from 'lib/lemon-ui/Tooltip' -import { userLogic } from 'scenes/userLogic' -import { canGloballyManagePlugins } from 'scenes/plugins/access' -import { Popconfirm } from 'antd' export function AppView({ plugin, }: { plugin: PluginTypeWithConfig | PluginType | PluginRepositoryEntry }): JSX.Element { - const { - installingPluginUrl, - pluginsNeedingUpdates, - pluginsUpdating, - showAppMetricsForPlugin, - loading, - sortableEnabledPlugins, - unusedPlugins, - } = useValues(pluginsLogic) - const { installPlugin, editPlugin, toggleEnabled, updatePlugin, openReorderModal, patchPlugin, uninstallPlugin } = - useActions(pluginsLogic) - const { user } = useValues(userLogic) + const { installingPluginUrl, showAppMetricsForPlugin, loading, sortableEnabledPlugins } = useValues(pluginsLogic) + const { installPlugin, editPlugin, toggleEnabled, openReorderModal } = useActions(pluginsLogic) const pluginConfig = 'pluginConfig' in plugin ? plugin.pluginConfig : null const isConfigured = !!pluginConfig?.id @@ -144,84 +129,6 @@ export function AppView({ )} - {'updateStatus' in plugin && pluginsNeedingUpdates.find((x) => x.id === plugin.id) && ( - { - plugin.updateStatus?.updated ? editPlugin(plugin.id) : updatePlugin(plugin.id) - }} - loading={pluginsUpdating.includes(plugin.id)} - icon={plugin.updateStatus?.updated ? : } - > - {plugin.updateStatus?.updated ? 'Updated' : 'Update'} - - )} - - {canGloballyManagePlugins(user?.organization) && ( - <> - uninstallPlugin(plugin.id)} - okText="Uninstall" - cancelText="Cancel" - className="Plugins__Popconfirm" - > - } - disabledReason={ - unusedPlugins.includes(plugin.id) ? undefined : 'This app is still in use.' - } - data-attr="plugin-uninstall" - > - Uninstall - - - {plugin.is_global ? ( - - This app can currently be used by other organizations in this instance - of PostHog. This action will disable and hide it for all - organizations other than yours. - - } - > - } - onClick={() => patchPlugin(plugin.id, { is_global: false })} - > - Make local - - - ) : ( - - This action will mark this app as installed for all organizations{' '} - in this instance of PostHog. - - } - > - } - onClick={() => patchPlugin(plugin.id, { is_global: true })} - > - Make global - - - )} - - )} - {pluginConfig.id && (pluginConfig.error ? ( + } + + const { checkForUpdates, openAdvancedInstallModal } = useActions(pluginsLogic) + + const { + installedPlugins, + installedPluginUrls, + filteredPluginsNeedingUpdates, + loading, + filteredUninstalledPlugins, + repositoryLoading, + pluginsNeedingUpdates, + hasUpdatablePlugins, + checkingForUpdates, + updateStatus, + } = useValues(pluginsLogic) + + const officialPlugins = useMemo( + () => filteredUninstalledPlugins.filter((plugin) => plugin.maintainer === 'official'), + [filteredUninstalledPlugins] + ) + const communityPlugins = useMemo( + () => filteredUninstalledPlugins.filter((plugin) => plugin.maintainer === 'community'), + [filteredUninstalledPlugins] + ) + + const renderfn: (plugin: PluginTypeWithConfig | PluginType | PluginRepositoryEntry) => JSX.Element = (plugin) => ( + + ) + + return ( + <> +
+
+ + +
+ {hasUpdatablePlugins && ( + 0 ? : } + onClick={(e) => { + e.stopPropagation() + checkForUpdates(true) + }} + loading={checkingForUpdates} + > + {checkingForUpdates + ? `Checking app ${Object.keys(updateStatus).length + 1} out of ${ + Object.keys(installedPluginUrls).length + }` + : pluginsNeedingUpdates.length > 0 + ? 'Check again for updates' + : 'Check for updates'} + + )} + + Install app (advanced) + +
+
+ + {filteredPluginsNeedingUpdates.length > 0 && ( + + )} + + + + {canGloballyManagePlugins(user?.organization) && ( + <> + + + + + + )} +
+ + + ) +} diff --git a/frontend/src/scenes/plugins/tabs/apps/AppsTab.tsx b/frontend/src/scenes/plugins/tabs/apps/AppsTab.tsx index db786812d56eb..a02ad02f27fef 100644 --- a/frontend/src/scenes/plugins/tabs/apps/AppsTab.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/AppsTab.tsx @@ -1,45 +1,20 @@ -import { LemonTable, LemonButton, LemonDivider } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { IconCloudDownload, IconRefresh, IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' -import { useMemo, useState } from 'react' +import { useValues } from 'kea' import { PluginsSearch } from 'scenes/plugins/PluginsSearch' -import { canGloballyManagePlugins, canInstallPlugins } from 'scenes/plugins/access' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' -import { PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' -import { userLogic } from 'scenes/userLogic' -import { PluginType } from '~/types' -import { AdvancedInstallModal } from './AdvancedInstallModal' -import { AppView } from './AppView' import { PluginDrawer } from 'scenes/plugins/edit/PluginDrawer' import { BatchExportsAlternativeWarning } from './components' import { InstalledAppsReorderModal } from './InstalledAppsReorderModal' +import { AppsTable } from './AppsTable' +import { AppView } from './AppView' +import { PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' +import { PluginType } from '~/types' export function AppsTab(): JSX.Element { - const { user } = useValues(userLogic) - const { checkForUpdates, openAdvancedInstallModal } = useActions(pluginsLogic) + const { sortableEnabledPlugins, unsortableEnabledPlugins, filteredDisabledPlugins, loading } = + useValues(pluginsLogic) - const { - sortableEnabledPlugins, - unsortableEnabledPlugins, - filteredDisabledPlugins, - installedPluginUrls, - filteredPluginsNeedingUpdates, - loading, - filteredUninstalledPlugins, - repositoryLoading, - pluginsNeedingUpdates, - hasUpdatablePlugins, - checkingForUpdates, - updateStatus, - } = useValues(pluginsLogic) - - const officialPlugins = useMemo( - () => filteredUninstalledPlugins.filter((plugin) => plugin.maintainer === 'official'), - [filteredUninstalledPlugins] - ) - const communityPlugins = useMemo( - () => filteredUninstalledPlugins.filter((plugin) => plugin.maintainer === 'community'), - [filteredUninstalledPlugins] + const renderfn: (plugin: PluginTypeWithConfig | PluginType | PluginRepositoryEntry) => JSX.Element = (plugin) => ( + ) return ( @@ -47,126 +22,25 @@ export function AppsTab(): JSX.Element {
- -
- {canInstallPlugins(user?.organization) && hasUpdatablePlugins && ( - 0 ? : } - onClick={(e) => { - e.stopPropagation() - checkForUpdates(true) - }} - loading={checkingForUpdates} - > - {checkingForUpdates - ? `Checking app ${Object.keys(updateStatus).length + 1} out of ${ - Object.keys(installedPluginUrls).length - }` - : pluginsNeedingUpdates.length > 0 - ? 'Check again for updates' - : 'Check for updates'} - - )} - - {canInstallPlugins(user?.organization) && ( - - Install app (advanced) - - )} -
- {filteredPluginsNeedingUpdates.length > 0 && ( - - )} - + - - - {canGloballyManagePlugins(user?.organization) && ( - <> - - - - - - )}
- ) } - -export function AppsTable({ - title = 'Apps', - plugins, - loading, -}: { - title?: string - plugins: (PluginTypeWithConfig | PluginType | PluginRepositoryEntry)[] - loading: boolean -}): JSX.Element { - const [expanded, setExpanded] = useState(true) - const { searchTerm } = useValues(pluginsLogic) - - return ( - - : } - onClick={() => setExpanded(!expanded)} - className="-ml-2 mr-2" - /> - {title} - - ), - key: 'app', - render: (_, plugin) => { - return - }, - }, - ]} - emptyState={ - !expanded ? ( - - setExpanded(true)}> - Show apps - - - ) : searchTerm ? ( - 'No apps matching your search criteria' - ) : ( - 'No apps found' - ) - } - /> - ) -} diff --git a/frontend/src/scenes/plugins/tabs/apps/AppsTable.tsx b/frontend/src/scenes/plugins/tabs/apps/AppsTable.tsx new file mode 100644 index 0000000000000..5fb4af0d52ac7 --- /dev/null +++ b/frontend/src/scenes/plugins/tabs/apps/AppsTable.tsx @@ -0,0 +1,61 @@ +import { LemonTable, LemonButton } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { IconUnfoldLess, IconUnfoldMore } from 'lib/lemon-ui/icons' +import { useState } from 'react' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { PluginRepositoryEntry, PluginTypeWithConfig } from 'scenes/plugins/types' +import { PluginType } from '~/types' + +export function AppsTable({ + title = 'Apps', + plugins, + loading, + renderfn, +}: { + title?: string + plugins: (PluginTypeWithConfig | PluginType | PluginRepositoryEntry)[] + loading: boolean + renderfn: (plugin: PluginTypeWithConfig | PluginType | PluginRepositoryEntry) => JSX.Element +}): JSX.Element { + const [expanded, setExpanded] = useState(true) + const { searchTerm } = useValues(pluginsLogic) + + return ( + + : } + onClick={() => setExpanded(!expanded)} + className="-ml-2 mr-2" + /> + {title} + + ), + key: 'app', + // Passing a function to render after loading + render: (_, plugin) => renderfn(plugin), + }, + ]} + emptyState={ + !expanded ? ( + + setExpanded(true)}> + Show apps + + + ) : searchTerm ? ( + 'No apps matching your search criteria' + ) : ( + 'No apps found' + ) + } + /> + ) +} diff --git a/frontend/src/scenes/plugins/types.ts b/frontend/src/scenes/plugins/types.ts index 8eab513b7f398..f662a93708dd5 100644 --- a/frontend/src/scenes/plugins/types.ts +++ b/frontend/src/scenes/plugins/types.ts @@ -37,6 +37,7 @@ export enum PluginInstallationType { export enum PluginTab { Apps = 'apps', + AppsManagement = 'apps_management', BatchExports = 'batch_exports', History = 'history', }