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',
}