diff --git a/.dockerignore b/.dockerignore index 5ca4b6168b2db..f5e7562caf969 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,6 +7,7 @@ !manage.py !gunicorn.config.py !babel.config.js +!posthog.json !package.json !yarn.lock !webpack.config.js diff --git a/.gitignore b/.gitignore index 2b4466b007e4c..f60b28cd25a06 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ yarn-error.log yalc.lock cypress/screenshots/* docker-compose.prod.yml +posthog.json diff --git a/dev.Dockerfile b/dev.Dockerfile index a67b5ab0e6df5..cf1c8fdf9c4af 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -21,6 +21,7 @@ RUN mkdir /code/requirements/ COPY requirements/dev.txt /code/requirements/ RUN pip install -r requirements/dev.txt --compile +COPY posthog.json /code/ COPY package.json /code/ COPY yarn.lock /code/ COPY webpack.config.js /code/ diff --git a/frontend/src/initKea.tsx b/frontend/src/initKea.tsx index 6b322aca56a0b..cbccd6cef856c 100644 --- a/frontend/src/initKea.tsx +++ b/frontend/src/initKea.tsx @@ -18,7 +18,7 @@ export function initKea(): void {

Error loading "{reducerKey}".

Action "{actionKey}" responded with

-

"{error.message}"

+

"{error.message || error.detail}"

) window['Sentry'] ? window['Sentry'].captureException(error) : console.error(error) diff --git a/frontend/src/layout/Sidebar.js b/frontend/src/layout/Sidebar.js index 6b62a9a6f0fc0..e2d6101859d6d 100644 --- a/frontend/src/layout/Sidebar.js +++ b/frontend/src/layout/Sidebar.js @@ -20,6 +20,7 @@ import { TeamOutlined, LockOutlined, WalletOutlined, + ApiOutlined, DatabaseOutlined, } from '@ant-design/icons' import { useActions, useValues } from 'kea' @@ -64,6 +65,7 @@ const submenuOverride = { annotations: 'settings', billing: 'settings', licenses: 'settings', + plugins: 'settings', systemStatus: 'settings', } @@ -279,6 +281,14 @@ export function Sidebar({ user, sidebarCollapsed, setSidebarCollapsed }) { )} + + {user.plugin_access?.configure && ( + + + Plugins + + + )} diff --git a/frontend/src/scenes/plugins/CustomPlugin.tsx b/frontend/src/scenes/plugins/CustomPlugin.tsx new file mode 100644 index 0000000000000..aec295bc968ed --- /dev/null +++ b/frontend/src/scenes/plugins/CustomPlugin.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { Button, Col, Input, Row } from 'antd' +import { useActions, useValues } from 'kea' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' + +export function CustomPlugin(): JSX.Element { + const { customPluginUrl, pluginError, loading } = useValues(pluginsLogic) + const { setCustomPluginUrl, installPlugin } = useActions(pluginsLogic) + + return ( +
+

Install Custom Plugin

+

+ Paste the URL of the Plugin's Github Repository to install it +

+ + + + setCustomPluginUrl(e.target.value)} + placeholder="https://github.com/user/repo" + /> + + + + + + {pluginError ?

{pluginError}

: null} +
+ ) +} diff --git a/frontend/src/scenes/plugins/InstalledPlugins.tsx b/frontend/src/scenes/plugins/InstalledPlugins.tsx new file mode 100644 index 0000000000000..0017fc6470dba --- /dev/null +++ b/frontend/src/scenes/plugins/InstalledPlugins.tsx @@ -0,0 +1,105 @@ +import React from 'react' +import { Button, Col, Row, Table, Tooltip } from 'antd' +import { useActions, useValues } from 'kea' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { GithubOutlined, CheckOutlined, ToolOutlined, PauseOutlined } from '@ant-design/icons' +import { PluginTypeWithConfig } from 'scenes/plugins/types' +import { userLogic } from 'scenes/userLogic' + +function trimTag(tag: string): string { + if (tag.match(/^[a-f0-9]{40}$/)) { + return tag.substring(0, 7) + } + if (tag.length >= 20) { + return tag.substring(0, 17) + '...' + } + return tag +} + +export function InstalledPlugins(): JSX.Element { + const { user } = useValues(userLogic) + const { installedPlugins, loading } = useValues(pluginsLogic) + const { editPlugin } = useActions(pluginsLogic) + + const canInstall = user?.plugin_access?.install + + return ( +
+

{canInstall ? 'Installed Plugins' : 'Plugins'}

+ plugin.name} + pagination={{ pageSize: 99999, hideOnSinglePage: true }} + dataSource={installedPlugins} + columns={[ + { + title: 'Plugin', + key: 'name', + render: function RenderPlugin(plugin: PluginTypeWithConfig): JSX.Element { + return ( + <> + + + {plugin.name} + + + + + {plugin.pluginConfig?.enabled ? ( +
+ {' '} + {plugin.pluginConfig?.global ? 'Globally Enabled' : 'Enabled'} +
+ ) : ( +
+ Disabled +
+ )} + + + {!plugin.url?.startsWith('file:') && ( + + {trimTag(plugin.tag)} + + )} + + + + ) + }, + }, + { + title: 'Description', + key: 'description', + render: function RenderDescription(plugin: PluginTypeWithConfig): JSX.Element { + return
{plugin.description}
+ }, + }, + { + title: '', + key: 'config', + align: 'right', + render: function RenderConfig(plugin: PluginTypeWithConfig): JSX.Element | null { + return !plugin.pluginConfig?.global ? ( + + + + )} + + + + } + > +
+ {editingPlugin ? ( +
+

{editingPlugin.name}

+

{editingPlugin.description}

+ + + + + {Object.keys(editingPlugin.config_schema).map((configKey) => ( + + + + ))} +
+ ) : null} + + + ) +} diff --git a/frontend/src/scenes/plugins/Plugins.tsx b/frontend/src/scenes/plugins/Plugins.tsx new file mode 100644 index 0000000000000..92f83a14efab6 --- /dev/null +++ b/frontend/src/scenes/plugins/Plugins.tsx @@ -0,0 +1,43 @@ +import React, { useEffect } from 'react' +import { hot } from 'react-hot-loader/root' +import { PluginModal } from 'scenes/plugins/PluginModal' +import { CustomPlugin } from 'scenes/plugins/CustomPlugin' +import { Repository } from 'scenes/plugins/Repository' +import { InstalledPlugins } from 'scenes/plugins/InstalledPlugins' +import { useValues } from 'kea' +import { userLogic } from 'scenes/userLogic' + +export const Plugins = hot(_Plugins) +function _Plugins(): JSX.Element { + const { user } = useValues(userLogic) + + if (!user) { + return
+ } + + if (!user?.plugin_access?.configure) { + useEffect(() => { + window.location.href = '/' + }, []) + return
+ } + + return ( +
+ + + {user.plugin_access?.install ? ( + <> +
+
+ +
+
+ + + ) : null} + + +
+ ) +} diff --git a/frontend/src/scenes/plugins/Repository.tsx b/frontend/src/scenes/plugins/Repository.tsx new file mode 100644 index 0000000000000..57f13f2d9b2b0 --- /dev/null +++ b/frontend/src/scenes/plugins/Repository.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { Button, Table, Tooltip } from 'antd' +import { useActions, useValues } from 'kea' +import { pluginsLogic } from 'scenes/plugins/pluginsLogic' +import { PluginType } from '~/types' +import { DownloadOutlined } from '@ant-design/icons' +import { PluginRepositoryEntry } from 'scenes/plugins/types' + +export function Repository(): JSX.Element { + const { loading, repositoryLoading, uninstalledPlugins } = useValues(pluginsLogic) + const { installPlugin } = useActions(pluginsLogic) + + return ( +
+

Plugin Repository

+
plugin.name} + pagination={{ pageSize: 99999, hideOnSinglePage: true }} + dataSource={uninstalledPlugins} + columns={[ + { + title: 'Plugin', + key: 'name', + render: function RenderPlugin(plugin: PluginType): JSX.Element { + return ( + + {plugin.name} + + ) + }, + }, + { + title: 'Description', + key: 'description', + render: function RenderDescription(plugin: PluginRepositoryEntry): JSX.Element { + return
{plugin.description}
+ }, + }, + { + title: '', + key: 'install', + align: 'right', + render: function RenderInstall(plugin: PluginRepositoryEntry): JSX.Element { + return ( + +