Skip to content

Commit

Permalink
feat: Apps update for the new pipeline UI (#19666)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Matloka <[email protected]>
  • Loading branch information
tiina303 and Twixes authored Jan 10, 2024
1 parent afb34a9 commit e5bdceb
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 5 deletions.
59 changes: 56 additions & 3 deletions frontend/src/scenes/pipeline/AppsManagement.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LemonBanner, LemonDivider, LemonMenu, LemonTable, Tooltip } from '@posthog/lemon-ui'
import { LemonBanner, LemonDivider, LemonMenu, LemonTable, LemonTag, Tooltip } from '@posthog/lemon-ui'
import { Popconfirm } from 'antd'
import { useActions, useValues } from 'kea'
import { IconDelete, IconLock, IconLockOpen } from 'lib/lemon-ui/icons'
import { IconCloudDownload, IconDelete, IconLock, IconLockOpen, IconRefresh } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonInput } from 'lib/lemon-ui/LemonInput'
import { Link } from 'lib/lemon-ui/Link'
Expand All @@ -21,6 +21,11 @@ export const scene: SceneExport = {
}

export function AppsManagement(): JSX.Element {
// NOTE: We don't want to unmount appsManagementLogic once it's mounted. This is a memoization technique for
// `checkForUpdates`, as otherwise leaving the page and coming back to it would result in us checking for updates
// each time. Normally such a hack is a bit of a smell, but this is a staff-only page, so totally fine.
appsManagementLogic.mount()

const {
canInstallPlugins,
canGloballyManagePlugins,
Expand Down Expand Up @@ -50,6 +55,7 @@ export function AppsManagement(): JSX.Element {
<LemonDivider className="my-6" />

<h2>Installed apps</h2>
<AppsToUpdate />
{globalPlugins && (
<>
<h3 className="mt-3">Global apps</h3>
Expand All @@ -73,9 +79,39 @@ type RenderAppsTable = {
plugins: PluginType[]
}

function AppsToUpdate(): JSX.Element {
const { updatablePlugins, pluginsNeedingUpdates, checkingForUpdates } = useValues(appsManagementLogic)
const { checkForUpdates } = useActions(appsManagementLogic)

return (
<>
{updatablePlugins && (
<LemonButton
type="secondary"
icon={<IconRefresh />}
onClick={checkForUpdates}
loading={checkingForUpdates}
>
{checkingForUpdates
? `Checking ${Object.keys(updatablePlugins).length} apps for updates`
: // we by default already check all apps for updates on initial load
'Check again for updates'}
</LemonButton>
)}
{pluginsNeedingUpdates.length > 0 && (
<>
<h3 className="mt-3">Apps to update</h3>
<p>These apps have newer commits in the repository they link to.</p>
<AppsTable plugins={pluginsNeedingUpdates} />
</>
)}
</>
)
}

function AppsTable({ plugins }: RenderAppsTable): JSX.Element {
const { unusedPlugins } = useValues(appsManagementLogic)
const { uninstallPlugin, patchPlugin } = useActions(appsManagementLogic)
const { uninstallPlugin, patchPlugin, updatePlugin } = useActions(appsManagementLogic)

const data = plugins.map((plugin) => ({ ...plugin, key: plugin.id }))
return (
Expand All @@ -96,6 +132,13 @@ function AppsTable({ plugins }: RenderAppsTable): JSX.Element {
<>
<div className="flex gap-2 items-center">
<span className="font-semibold truncate">{plugin.name}</span>
{plugin.latest_tag && plugin.tag && plugin.latest_tag !== plugin.tag && (
<Link
to={plugin.url + '/compare/' + plugin.tag + '...' + plugin.latest_tag}
>
<LemonTag type="completion">See update diff</LemonTag>
</Link>
)}
</div>
<div className="text-sm">{plugin.description}</div>
</>
Expand Down Expand Up @@ -127,6 +170,16 @@ function AppsTable({ plugins }: RenderAppsTable): JSX.Element {
render: function RenderAccess(_, plugin) {
return (
<div className="flex items-center gap-2 justify-end">
{plugin.latest_tag && plugin.tag != plugin.latest_tag && (
<LemonButton
type="secondary"
size="small"
icon={<IconCloudDownload />}
onClick={() => updatePlugin(plugin.id)}
>
Update
</LemonButton>
)}
{plugin.is_global ? (
<Tooltip
title={
Expand Down
67 changes: 66 additions & 1 deletion frontend/src/scenes/pipeline/appsManagementLogic.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { actions, afterMount, connect, kea, path, reducers, selectors } from 'kea'
import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import api from 'lib/api'
import { lemonToast } from 'lib/lemon-ui/LemonToast/LemonToast'
Expand Down Expand Up @@ -56,6 +56,12 @@ function capturePluginEvent(event: string, plugin: PluginType, type: PluginInsta
plugin_installation_type: type,
})
}
export interface PluginUpdateStatusType {
latest_tag: string
upToDate: boolean
updated: boolean
error: string | null
}

export const appsManagementLogic = kea<appsManagementLogicType>([
path(['scenes', 'pipeline', 'appsManagementLogic']),
Expand All @@ -73,6 +79,10 @@ export const appsManagementLogic = kea<appsManagementLogicType>([
installSourcePlugin: (name: string) => ({ name }),
installLocalPlugin: (path: string) => ({ path }),
patchPlugin: (id: number, pluginChanges: Partial<PluginType> = {}) => ({ id, pluginChanges }),
updatePlugin: (id: number) => ({ id }),
checkForUpdates: true,
checkedForUpdates: true,
setPluginLatestTag: (id: number, latestTag: string) => ({ id, latestTag }),
}),
loaders(({ values }) => ({
plugins: [
Expand Down Expand Up @@ -124,9 +134,25 @@ export const appsManagementLogic = kea<appsManagementLogicType>([
return rest
},
patchPlugin: async ({ id, pluginChanges }) => {
if (!values.canGloballyManagePlugins) {
lemonToast.error("You don't have permission to update apps.")
}
const response = await api.update(`api/organizations/@current/plugins/${id}`, pluginChanges)
return { ...values.plugins, [id]: response }
},
setPluginLatestTag: async ({ id, latestTag }) => {
return { ...values.plugins, [id]: { ...values.plugins[id], latest_tag: latestTag } }
},
updatePlugin: async ({ id }) => {
if (!values.canGloballyManagePlugins) {
lemonToast.error("You don't have permission to update apps.")
}
// TODO: the update failed
const response = await api.create(`api/organizations/@current/plugins/${id}/upgrade`)
capturePluginEvent(`plugin updated`, values.plugins[id], values.plugins[id].plugin_type)
lemonToast.success(`Plugin ${response.name} updated!`)
return { ...values.plugins, [id]: response }
},
},
],
unusedPlugins: [
Expand Down Expand Up @@ -177,6 +203,13 @@ export const appsManagementLogic = kea<appsManagementLogicType>([
installPluginSuccess: () => SourcePluginKind.FilterEvent,
},
],
checkingForUpdates: [
false,
{
checkForUpdates: () => true,
checkedForUpdates: () => false,
},
],
}),
selectors({
canInstallPlugins: [(s) => [s.user], (user) => canInstallPlugins(user?.organization)],
Expand Down Expand Up @@ -206,9 +239,41 @@ export const appsManagementLogic = kea<appsManagementLogicType>([
)
},
],
updatablePlugins: [
(s) => [s.plugins],
(plugins) =>
Object.values(plugins).filter(
(plugin) => plugin.plugin_type !== PluginInstallationType.Source && !plugin.url?.startsWith('file:')
),
],
pluginsNeedingUpdates: [
(s) => [s.updatablePlugins],
(plugins) => {
return plugins.filter((plugin) => plugin.latest_tag && plugin.tag !== plugin.latest_tag)
},
],
}),
listeners(({ actions, values }) => ({
checkForUpdates: async () => {
await Promise.all(
values.updatablePlugins.map(async (plugin) => {
try {
const updates = await api.get(
`api/organizations/@current/plugins/${plugin.id}/check_for_updates`
)
actions.setPluginLatestTag(plugin.id, updates.plugin.latest_tag)
} catch (e) {
lemonToast.error(`Error checking for updates for ${plugin.name}: ${JSON.stringify(e)}`)
}
})
)

actions.checkedForUpdates()
},
})),
afterMount(({ actions }) => {
actions.loadPlugins()
actions.loadUnusedPlugins()
actions.checkForUpdates()
}),
])
2 changes: 1 addition & 1 deletion frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1518,7 +1518,7 @@ export interface PluginType {
url?: string
tag?: string
icon?: string
latest_tag?: string
latest_tag?: string // apps management page: The latest git hash for the repo behind the url
config_schema: Record<string, PluginConfigSchema> | PluginConfigSchema[]
source?: string
maintainer?: string
Expand Down

0 comments on commit e5bdceb

Please sign in to comment.