Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin editor #2743

Merged
merged 32 commits into from
Dec 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
994f2b6
fix tsc error
mariusandra Dec 10, 2020
8260b95
add plugin source, create plugin source version model, add plugin_type
mariusandra Dec 10, 2020
62ca0c2
can create and save source plugins via the api
mariusandra Dec 10, 2020
f488976
Merge branch 'master' into plugin-editor
mariusandra Dec 10, 2020
42690b6
make empty source plugins
mariusandra Dec 11, 2020
4c62371
message if no config options available
mariusandra Dec 11, 2020
72a3db5
different image or tag for source plugins
mariusandra Dec 11, 2020
c7267c2
fix some types
mariusandra Dec 11, 2020
5edbef9
second drawer
mariusandra Dec 11, 2020
cd1424a
add fields into drawer
mariusandra Dec 11, 2020
4506233
add monaco editor
mariusandra Dec 11, 2020
e4a17e1
refactor drawer
mariusandra Dec 11, 2020
383644c
save plugin and must be json
mariusandra Dec 11, 2020
4b82b3e
close tab on save
mariusandra Dec 11, 2020
7ca6ad0
fix default code
mariusandra Dec 11, 2020
e08936c
upgrade to plugin server 0.5.0 - with scheduled plugins
mariusandra Dec 11, 2020
95b0b0f
less height
mariusandra Dec 11, 2020
4cb8c79
upgrade to plugin server 0.5.1 - with posthog.capture
mariusandra Dec 11, 2020
e5ea44e
remove minimap from editor
mariusandra Dec 14, 2020
7666acc
upgrade to 0.5.1
mariusandra Dec 14, 2020
9039fea
use enum
mariusandra Dec 14, 2020
3d1fb0f
reduce quirk
mariusandra Dec 14, 2020
a2f9a21
must enter a name/url
mariusandra Dec 14, 2020
55f31d0
use the source
mariusandra Dec 14, 2020
a4331fb
simpler text
mariusandra Dec 14, 2020
03e7182
sync image
mariusandra Dec 14, 2020
d69c918
add link to docs
mariusandra Dec 14, 2020
8c0cae4
add a link to the documentation inside the source code drawer
mariusandra Dec 14, 2020
8147f7b
text / ux simplifications
mariusandra Dec 14, 2020
b14c03d
nicer first plugin experience
mariusandra Dec 14, 2020
c5cafa7
nicer django model choices
mariusandra Dec 14, 2020
7c62386
Merge branch 'master' into plugin-editor
mariusandra Dec 14, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/scenes/plugins/InstalledPlugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function InstalledPlugins(): JSX.Element {
name={plugin.name}
url={plugin.url}
description={plugin.description}
pluginType={plugin.plugin_type}
pluginConfig={plugin.pluginConfig}
error={plugin.error}
/>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/plugins/OptInPlugins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function OptInPlugins(): JSX.Element {
)}
<div style={{ marginBottom: 20 }}>
<Checkbox checked={optIn} onChange={() => setOptIn(!optIn)} disabled={serverStatus !== 'online'}>
I understand the risks and wish to try this beta feature now for <b>{user?.team.name}</b>.
I understand the risks and wish to try this beta feature now for <b>{user?.team?.name}</b>.
</Checkbox>
</div>
<div>
Expand Down
28 changes: 21 additions & 7 deletions frontend/src/scenes/plugins/PluginCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Col, Card, Button, Switch, Popconfirm, Skeleton } from 'antd'
import { Button, Card, Col, Popconfirm, Skeleton, Switch } from 'antd'
import { useActions, useValues } from 'kea'
import React from 'react'
import { pluginsLogic } from './pluginsLogic'
Expand All @@ -9,17 +9,28 @@ import { Link } from 'lib/components/Link'
import { PluginImage } from './PluginImage'
import { PluginError } from 'scenes/plugins/PluginError'
import { LocalPluginTag } from 'scenes/plugins/LocalPluginTag'
import { PluginInstallationType } from 'scenes/plugins/types'
import { SourcePluginTag } from 'scenes/plugins/SourcePluginTag'

interface PluginCardProps {
name: string
description: string
url: string
description?: string
url?: string
pluginConfig?: PluginConfigType
pluginType?: PluginInstallationType
pluginId?: number
error?: PluginErrorType
}

export function PluginCard({ name, description, url, pluginConfig, pluginId, error }: PluginCardProps): JSX.Element {
export function PluginCard({
name,
description,
url,
pluginType,
pluginConfig,
pluginId,
error,
}: PluginCardProps): JSX.Element {
const { editPlugin, toggleEnabled, installPlugin, resetPluginConfigError } = useActions(pluginsLogic)
const { loading } = useValues(pluginsLogic)

Expand Down Expand Up @@ -49,6 +60,9 @@ export function PluginCard({ name, description, url, pluginConfig, pluginId, err
style={{ height: '100%', display: 'flex', marginBottom: 20 }}
bodyStyle={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}
>
{pluginType === 'source' ? (
<SourcePluginTag style={{ position: 'absolute', top: 10, left: 10, cursor: 'pointer' }} />
) : null}
{url?.startsWith('file:') ? (
<LocalPluginTag
url={url}
Expand All @@ -64,7 +78,7 @@ export function PluginCard({ name, description, url, pluginConfig, pluginId, err
) : error ? (
<PluginError error={error} />
) : null}
<PluginImage url={url} />
<PluginImage pluginType={pluginType} url={url} />
<div className="text-center mb" style={{ marginBottom: 16 }}>
<b>{name}</b>
</div>
Expand Down Expand Up @@ -99,7 +113,7 @@ export function PluginCard({ name, description, url, pluginConfig, pluginId, err
</div>
</Popconfirm>
)}
{!pluginConfig && (
{!pluginConfig && url && (
<>
<Link to={url} target="_blank" rel="noopener noreferrer">
Learn more
Expand All @@ -113,7 +127,7 @@ export function PluginCard({ name, description, url, pluginConfig, pluginId, err
<Button
type="primary"
loading={loading}
onClick={() => installPlugin(url, 'repository')}
onClick={url ? () => installPlugin(url, PluginInstallationType.Repository) : undefined}
icon={<PlusOutlined />}
>
Install
Expand Down
235 changes: 129 additions & 106 deletions frontend/src/scenes/plugins/PluginDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'
import { useActions, useValues } from 'kea'
import { pluginsLogic } from 'scenes/plugins/pluginsLogic'
import { Button, Form, Input, Popconfirm, Switch } from 'antd'
import { DeleteOutlined, ArrowRightOutlined } from '@ant-design/icons'
import { DeleteOutlined, ArrowRightOutlined, CodeOutlined } from '@ant-design/icons'
import { userLogic } from 'scenes/userLogic'
import { PluginImage } from './PluginImage'
import { Link } from 'lib/components/Link'
Expand All @@ -11,6 +11,8 @@ import { LocalPluginTag } from 'scenes/plugins/LocalPluginTag'
import { UploadField } from 'scenes/plugins/UploadField'
import { getConfigSchemaArray } from 'scenes/plugins/utils'
import Markdown from 'react-markdown'
import { SourcePluginTag } from 'scenes/plugins/SourcePluginTag'
import { PluginSource } from 'scenes/plugins/PluginSource'

function EnabledDisabledSwitch({
value,
Expand All @@ -29,8 +31,8 @@ function EnabledDisabledSwitch({

export function PluginDrawer(): JSX.Element {
const { user } = useValues(userLogic)
const { editingPlugin, loading } = useValues(pluginsLogic)
const { editPlugin, savePluginConfig, uninstallPlugin } = useActions(pluginsLogic)
const { editingPlugin, loading, editingSource } = useValues(pluginsLogic)
const { editPlugin, savePluginConfig, uninstallPlugin, setEditingSource } = useActions(pluginsLogic)
const [form] = Form.useForm()

const canDelete = user?.plugin_access.install
Expand All @@ -47,114 +49,135 @@ export function PluginDrawer(): JSX.Element {
}, [editingPlugin?.name])

return (
<Drawer
forceRender={true}
visible={!!editingPlugin}
onClose={() => editPlugin(null)}
width="min(90vw, 420px)"
title={editingPlugin?.name}
footer={
<>
<div style={{ display: 'flex' }}>
<div style={{ flexGrow: 1 }}>
{canDelete && (
<Popconfirm
placement="topLeft"
title="Are you sure you wish to uninstall this plugin?"
onConfirm={editingPlugin ? () => uninstallPlugin(editingPlugin.name) : () => {}}
okText="Yes"
cancelText="No"
>
<Button style={{ color: 'var(--red)', float: 'left' }} type="link">
<DeleteOutlined /> Uninstall
</Button>
</Popconfirm>
)}
</div>
<div>
<Button onClick={() => editPlugin(null)} style={{ marginRight: 16 }}>
Cancel
</Button>
<Button type="primary" loading={loading} onClick={form.submit}>
Save
</Button>
</div>
</div>
</>
}
>
<Form form={form} layout="vertical" name="basic" onFinish={savePluginConfig}>
{editingPlugin ? (
<div>
<div style={{ display: 'flex', marginBottom: 16 }}>
<>
<Drawer
forceRender={true}
visible={!!editingPlugin}
onClose={() => editPlugin(null)}
width="min(90vw, 420px)"
title={editingPlugin?.name}
footer={
<>
<div style={{ display: 'flex' }}>
<div style={{ flexGrow: 1 }}>
{canDelete && (
<Popconfirm
placement="topLeft"
title="Are you sure you wish to uninstall this plugin?"
onConfirm={editingPlugin ? () => uninstallPlugin(editingPlugin.name) : () => {}}
okText="Uninstall"
cancelText="Cancel"
>
<Button style={{ color: 'var(--red)', float: 'left' }} type="link">
<DeleteOutlined /> Uninstall
</Button>
</Popconfirm>
)}
</div>
<div>
<PluginImage url={editingPlugin.url} />
<Button onClick={() => editPlugin(null)} style={{ marginRight: 16 }}>
Cancel
</Button>
<Button type="primary" loading={loading} onClick={form.submit}>
Save
</Button>
</div>
<div style={{ flexGrow: 1, paddingLeft: 16 }}>
{editingPlugin.description}
<div style={{ marginTop: 5 }}>
{editingPlugin.url?.startsWith('file:') ? (
<LocalPluginTag url={editingPlugin.url} title="Installed Locally" />
) : (
<Link to={editingPlugin.url} target="_blank" rel="noopener noreferrer">
View plugin <ArrowRightOutlined />
</Link>
)}
</div>
</>
}
>
<Form form={form} layout="vertical" name="basic" onFinish={savePluginConfig}>
{editingPlugin ? (
<div>
<div style={{ display: 'flex', marginBottom: 16 }}>
<div>
<PluginImage pluginType={editingPlugin.plugin_type} url={editingPlugin.url} />
</div>
<div style={{ display: 'flex', alignItems: 'center', marginTop: 5 }}>
<Form.Item
fieldKey="__enabled"
name="__enabled"
style={{ display: 'inline-block', marginBottom: 0 }}
>
<EnabledDisabledSwitch />
</Form.Item>
<div style={{ flexGrow: 1, paddingLeft: 16 }}>
{editingPlugin.description}
<div style={{ marginTop: 5 }}>
{editingPlugin?.plugin_type === 'local' && editingPlugin.url ? (
<LocalPluginTag url={editingPlugin.url} title="Installed Locally" />
) : editingPlugin.plugin_type === 'source' ? (
<SourcePluginTag />
) : editingPlugin.url ? (
<Link to={editingPlugin.url} target="_blank" rel="noopener noreferrer">
View plugin <ArrowRightOutlined />
</Link>
) : null}
</div>
<div style={{ display: 'flex', alignItems: 'center', marginTop: 5 }}>
<Form.Item
fieldKey="__enabled"
name="__enabled"
style={{ display: 'inline-block', marginBottom: 0 }}
>
<EnabledDisabledSwitch />
</Form.Item>
</div>
</div>
</div>
</div>
<h3 className="l3" style={{ marginTop: 32 }}>
Configuration
</h3>
{getConfigSchemaArray(editingPlugin.config_schema).map((fieldConfig, index) => (
<React.Fragment key={fieldConfig.key || `__key__${index}`}>
{fieldConfig.markdown ? (
<Markdown source={fieldConfig.markdown} linkTarget="_blank" />
) : null}
{fieldConfig.type ? (
<Form.Item
label={fieldConfig.name || fieldConfig.key}
extra={
fieldConfig.hint ? (
<Markdown source={fieldConfig.hint} linkTarget="_blank" />
) : null
}
name={fieldConfig.key}
required={fieldConfig.required}
rules={[
{
required: fieldConfig.required,
message: 'Please enter a value!',
},
]}

{editingPlugin.plugin_type === 'source' ? (
<div>
<Button
type={editingSource ? 'default' : 'primary'}
icon={<CodeOutlined />}
onClick={() => setEditingSource(!editingSource)}
>
{fieldConfig.type === 'attachment' ? (
<UploadField />
) : fieldConfig.type === 'string' ? (
<Input />
) : (
<strong style={{ color: 'var(--red)' }}>
Unknown field type "<code>{fieldConfig.type}</code>".
<br />
You may need to upgrade PostHog!
</strong>
)}
</Form.Item>
) : null}
</React.Fragment>
))}
</div>
) : null}
</Form>
</Drawer>
Edit Source
</Button>
</div>
) : null}

<h3 className="l3" style={{ marginTop: 32 }}>
Configuration
</h3>
{getConfigSchemaArray(editingPlugin.config_schema).length === 0 ? (
<div>This plugin is not configurable.</div>
) : null}
{getConfigSchemaArray(editingPlugin.config_schema).map((fieldConfig, index) => (
<React.Fragment key={fieldConfig.key || `__key__${index}`}>
{fieldConfig.markdown ? (
<Markdown source={fieldConfig.markdown} linkTarget="_blank" />
) : null}
{fieldConfig.type ? (
<Form.Item
label={fieldConfig.name || fieldConfig.key}
extra={
fieldConfig.hint ? (
<Markdown source={fieldConfig.hint} linkTarget="_blank" />
) : null
}
name={fieldConfig.key}
required={fieldConfig.required}
rules={[
{
required: fieldConfig.required,
message: 'Please enter a value!',
},
]}
>
{fieldConfig.type === 'attachment' ? (
<UploadField />
) : fieldConfig.type === 'string' ? (
<Input />
) : (
<strong style={{ color: 'var(--red)' }}>
Unknown field type "<code>{fieldConfig.type}</code>".
<br />
You may need to upgrade PostHog!
</strong>
)}
</Form.Item>
) : null}
</React.Fragment>
))}
</div>
) : null}
</Form>
</Drawer>
{editingPlugin?.plugin_type === 'source' ? <PluginSource /> : null}
</>
)
}
Loading