2022-11-30 01:14:11 +03:00
|
|
|
import { Button, Checkbox, Form, Input, Modal, Space, Table, Typography } from 'antd';
|
|
|
|
import _ from 'lodash';
|
2023-01-16 09:31:36 +03:00
|
|
|
import dynamic from 'next/dynamic';
|
2023-01-16 10:11:44 +03:00
|
|
|
import React, { ReactElement, useContext, useEffect, useState } from 'react';
|
2023-01-10 07:57:29 +03:00
|
|
|
import { FormStatusIndicator } from '../../components/admin/FormStatusIndicator';
|
2022-11-30 01:14:11 +03:00
|
|
|
import { ExternalAction } from '../../interfaces/external-action';
|
2021-03-16 01:27:19 +03:00
|
|
|
import {
|
|
|
|
API_EXTERNAL_ACTIONS,
|
2021-07-09 21:42:01 +03:00
|
|
|
postConfigUpdateToAPI,
|
2021-03-16 01:27:19 +03:00
|
|
|
RESET_TIMEOUT,
|
2022-04-20 05:57:27 +03:00
|
|
|
} from '../../utils/config-constants';
|
|
|
|
import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses';
|
|
|
|
import { ServerStatusContext } from '../../utils/server-status-context';
|
2022-12-14 22:52:11 +03:00
|
|
|
import { isValidUrl, DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/urls';
|
2021-03-16 01:27:19 +03:00
|
|
|
|
2023-01-16 10:11:44 +03:00
|
|
|
import { AdminLayout } from '../../components/layouts/AdminLayout';
|
|
|
|
|
2021-03-16 01:27:19 +03:00
|
|
|
const { Title, Paragraph } = Typography;
|
2023-01-16 09:31:36 +03:00
|
|
|
|
|
|
|
// Lazy loaded components
|
|
|
|
|
|
|
|
const DeleteOutlined = dynamic(() => import('@ant-design/icons/DeleteOutlined'), {
|
|
|
|
ssr: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
const EditOutlined = dynamic(() => import('@ant-design/icons/EditOutlined'), {
|
|
|
|
ssr: false,
|
|
|
|
});
|
|
|
|
|
2021-03-16 01:27:19 +03:00
|
|
|
let resetTimer = null;
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
onCancel: () => void;
|
2022-11-30 01:14:11 +03:00
|
|
|
onOk: (
|
|
|
|
oldAction: ExternalAction | null,
|
|
|
|
actionUrl: string,
|
|
|
|
actionTitle: string,
|
|
|
|
actionDescription: string,
|
|
|
|
actionIcon: string,
|
|
|
|
actionColor: string,
|
|
|
|
openExternally: boolean,
|
|
|
|
) => void;
|
2022-11-02 00:50:23 +03:00
|
|
|
open: boolean;
|
2022-11-30 01:14:11 +03:00
|
|
|
action: ExternalAction | null;
|
2021-03-16 01:27:19 +03:00
|
|
|
}
|
|
|
|
|
2022-11-30 01:14:11 +03:00
|
|
|
const ActionModal = (props: Props) => {
|
|
|
|
const { onOk, onCancel, open, action } = props;
|
2021-03-16 01:27:19 +03:00
|
|
|
|
|
|
|
const [actionUrl, setActionUrl] = useState('');
|
|
|
|
const [actionTitle, setActionTitle] = useState('');
|
|
|
|
const [actionDescription, setActionDescription] = useState('');
|
|
|
|
const [actionIcon, setActionIcon] = useState('');
|
|
|
|
const [actionColor, setActionColor] = useState('');
|
|
|
|
const [openExternally, setOpenExternally] = useState(false);
|
|
|
|
|
2022-11-30 01:14:11 +03:00
|
|
|
useEffect(() => {
|
|
|
|
setActionUrl(action?.url || '');
|
|
|
|
setActionTitle(action?.title || '');
|
|
|
|
setActionDescription(action?.description || '');
|
|
|
|
setActionIcon(action?.icon || '');
|
|
|
|
setActionColor(action?.color || '');
|
|
|
|
setOpenExternally(action?.openExternally || false);
|
|
|
|
}, [action]);
|
|
|
|
|
2021-03-16 01:27:19 +03:00
|
|
|
function save() {
|
2022-11-30 01:14:11 +03:00
|
|
|
onOk(
|
|
|
|
action,
|
|
|
|
actionUrl,
|
|
|
|
actionTitle,
|
|
|
|
actionDescription,
|
|
|
|
actionIcon,
|
|
|
|
actionColor,
|
|
|
|
openExternally,
|
|
|
|
);
|
2021-05-23 09:27:51 +03:00
|
|
|
setActionUrl('');
|
|
|
|
setActionTitle('');
|
|
|
|
setActionDescription('');
|
|
|
|
setActionIcon('');
|
|
|
|
setActionColor('');
|
|
|
|
setOpenExternally(false);
|
2021-03-16 01:27:19 +03:00
|
|
|
}
|
|
|
|
|
2021-03-18 07:26:27 +03:00
|
|
|
function canSave(): Boolean {
|
2022-12-15 10:33:32 +03:00
|
|
|
return isValidUrl(actionUrl, ['https:']) && actionTitle !== '';
|
2021-03-18 07:26:27 +03:00
|
|
|
}
|
|
|
|
|
2021-03-16 01:27:19 +03:00
|
|
|
const okButtonProps = {
|
2021-03-18 07:26:27 +03:00
|
|
|
disabled: !canSave(),
|
2021-03-16 01:27:19 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
const onOpenExternallyChanged = checkbox => {
|
|
|
|
setOpenExternally(checkbox.target.checked);
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Modal
|
2022-11-30 01:14:11 +03:00
|
|
|
destroyOnClose
|
|
|
|
title={action == null ? 'Create New Action' : 'Edit Action'}
|
2022-11-02 00:50:23 +03:00
|
|
|
open={open}
|
2021-03-16 01:27:19 +03:00
|
|
|
onOk={save}
|
|
|
|
onCancel={onCancel}
|
|
|
|
okButtonProps={okButtonProps}
|
|
|
|
>
|
2022-11-30 01:14:11 +03:00
|
|
|
<Form initialValues={action}>
|
2021-03-23 06:43:25 +03:00
|
|
|
Add the URL for the external action you want to present.{' '}
|
|
|
|
<strong>Only HTTPS urls are supported.</strong>
|
|
|
|
<p>
|
2022-01-14 02:17:09 +03:00
|
|
|
<a
|
|
|
|
href="https://owncast.online/thirdparty/actions/"
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
>
|
2021-03-23 06:43:25 +03:00
|
|
|
Read more about external actions.
|
|
|
|
</a>
|
|
|
|
</p>
|
2022-11-30 01:14:11 +03:00
|
|
|
<Form.Item name="url">
|
2021-03-16 01:27:19 +03:00
|
|
|
<Input
|
|
|
|
required
|
|
|
|
placeholder="https://myserver.com/action (required)"
|
2021-05-23 09:27:51 +03:00
|
|
|
onChange={input => setActionUrl(input.currentTarget.value.trim())}
|
|
|
|
type="url"
|
|
|
|
pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
|
2021-03-16 01:27:19 +03:00
|
|
|
/>
|
2022-11-30 01:14:11 +03:00
|
|
|
</Form.Item>
|
|
|
|
<Form.Item name="title">
|
2021-03-16 01:27:19 +03:00
|
|
|
<Input
|
|
|
|
value={actionTitle}
|
|
|
|
required
|
|
|
|
placeholder="Your action title (required)"
|
|
|
|
onChange={input => setActionTitle(input.currentTarget.value)}
|
|
|
|
/>
|
2022-11-30 01:14:11 +03:00
|
|
|
</Form.Item>
|
|
|
|
<Form.Item name="description">
|
2021-03-16 01:27:19 +03:00
|
|
|
<Input
|
|
|
|
value={actionDescription}
|
|
|
|
placeholder="Optional description"
|
|
|
|
onChange={input => setActionDescription(input.currentTarget.value)}
|
|
|
|
/>
|
2022-11-30 01:14:11 +03:00
|
|
|
</Form.Item>
|
|
|
|
<Form.Item name="icon">
|
2021-03-16 01:27:19 +03:00
|
|
|
<Input
|
|
|
|
value={actionIcon}
|
|
|
|
placeholder="https://myserver.com/action/icon.png (optional)"
|
|
|
|
onChange={input => setActionIcon(input.currentTarget.value)}
|
|
|
|
/>
|
2022-11-30 01:14:11 +03:00
|
|
|
</Form.Item>
|
|
|
|
<div>
|
|
|
|
<Form.Item name="color" style={{ marginBottom: '0px' }}>
|
|
|
|
<Input
|
|
|
|
type="color"
|
|
|
|
value={actionColor}
|
|
|
|
onChange={input => setActionColor(input.currentTarget.value)}
|
|
|
|
/>
|
|
|
|
</Form.Item>
|
2021-03-18 07:26:27 +03:00
|
|
|
Optional background color of the action button.
|
2022-11-30 01:14:11 +03:00
|
|
|
</div>
|
|
|
|
<Form.Item name="openExternally">
|
|
|
|
<Checkbox
|
|
|
|
checked={openExternally}
|
|
|
|
defaultChecked={openExternally}
|
|
|
|
onChange={onOpenExternallyChanged}
|
|
|
|
>
|
|
|
|
Open in a new tab instead of within your page.
|
|
|
|
</Checkbox>
|
|
|
|
</Form.Item>
|
|
|
|
</Form>
|
2021-03-16 01:27:19 +03:00
|
|
|
</Modal>
|
|
|
|
);
|
2022-09-07 10:00:28 +03:00
|
|
|
};
|
2021-03-16 01:27:19 +03:00
|
|
|
|
2022-09-07 10:00:28 +03:00
|
|
|
const Actions = () => {
|
2021-03-16 01:27:19 +03:00
|
|
|
const serverStatusData = useContext(ServerStatusContext);
|
|
|
|
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
|
|
|
const { externalActions } = serverConfig;
|
2022-11-30 01:14:11 +03:00
|
|
|
const [actions, setActions] = useState<ExternalAction[]>(null);
|
2022-11-02 00:50:23 +03:00
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
2021-03-16 01:27:19 +03:00
|
|
|
const [submitStatus, setSubmitStatus] = useState(null);
|
2022-11-30 01:14:11 +03:00
|
|
|
const [editAction, setEditAction] = useState<ExternalAction>(null);
|
2021-03-16 01:27:19 +03:00
|
|
|
|
|
|
|
const resetStates = () => {
|
|
|
|
setSubmitStatus(null);
|
|
|
|
resetTimer = null;
|
|
|
|
clearTimeout(resetTimer);
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setActions(externalActions || []);
|
|
|
|
}, [externalActions]);
|
|
|
|
|
2021-07-09 21:42:01 +03:00
|
|
|
async function save(actionsData) {
|
|
|
|
await postConfigUpdateToAPI({
|
|
|
|
apiPath: API_EXTERNAL_ACTIONS,
|
|
|
|
data: { value: actionsData },
|
|
|
|
onSuccess: () => {
|
|
|
|
setFieldInConfigState({ fieldName: 'externalActions', value: actionsData, path: '' });
|
|
|
|
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Updated.'));
|
|
|
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
2021-03-16 01:27:19 +03:00
|
|
|
},
|
2021-07-09 21:42:01 +03:00
|
|
|
onError: (message: string) => {
|
|
|
|
console.log(message);
|
|
|
|
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
|
|
|
|
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
2021-03-16 01:27:19 +03:00
|
|
|
},
|
2021-07-09 21:42:01 +03:00
|
|
|
});
|
|
|
|
}
|
2021-03-16 01:27:19 +03:00
|
|
|
|
|
|
|
async function handleDelete(action) {
|
2021-07-09 21:42:01 +03:00
|
|
|
const actionsData = [...actions];
|
2021-03-16 01:27:19 +03:00
|
|
|
const index = actions.findIndex(item => item.url === action.url);
|
|
|
|
actionsData.splice(index, 1);
|
|
|
|
|
|
|
|
try {
|
2021-07-09 21:42:01 +03:00
|
|
|
setActions(actionsData);
|
|
|
|
save(actionsData);
|
2021-03-16 01:27:19 +03:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function handleSave(
|
2022-11-30 01:14:11 +03:00
|
|
|
oldAction: ExternalAction | null,
|
2021-03-16 01:27:19 +03:00
|
|
|
url: string,
|
|
|
|
title: string,
|
|
|
|
description: string,
|
|
|
|
icon: string,
|
|
|
|
color: string,
|
|
|
|
openExternally: boolean,
|
|
|
|
) {
|
|
|
|
try {
|
2021-07-09 21:42:01 +03:00
|
|
|
const actionsData = [...actions];
|
2022-11-30 01:14:11 +03:00
|
|
|
|
|
|
|
const newAction: ExternalAction = {
|
2021-03-18 07:26:27 +03:00
|
|
|
url,
|
|
|
|
title,
|
|
|
|
description,
|
|
|
|
icon,
|
|
|
|
color,
|
|
|
|
openExternally,
|
2022-11-30 01:14:11 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
// Replace old action if edited or append the new action
|
|
|
|
const index = oldAction ? actions.findIndex(item => _.isEqual(item, oldAction)) : -1;
|
|
|
|
if (index >= 0) {
|
|
|
|
actionsData[index] = newAction;
|
|
|
|
} else {
|
|
|
|
actionsData.push(newAction);
|
|
|
|
}
|
|
|
|
|
|
|
|
setActions(actionsData);
|
|
|
|
await save(actionsData);
|
2021-03-16 01:27:19 +03:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-30 01:14:11 +03:00
|
|
|
async function handleEdit(action: ExternalAction) {
|
|
|
|
setEditAction(action);
|
|
|
|
setIsModalOpen(true);
|
|
|
|
}
|
|
|
|
|
2021-03-16 01:27:19 +03:00
|
|
|
const showCreateModal = () => {
|
2022-11-30 01:14:11 +03:00
|
|
|
setEditAction(null);
|
2022-11-02 00:50:23 +03:00
|
|
|
setIsModalOpen(true);
|
2021-03-16 01:27:19 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
const handleModalSaveButton = (
|
2022-11-30 01:14:11 +03:00
|
|
|
oldAction: ExternalAction | null,
|
2021-03-16 01:27:19 +03:00
|
|
|
actionUrl: string,
|
|
|
|
actionTitle: string,
|
|
|
|
actionDescription: string,
|
|
|
|
actionIcon: string,
|
|
|
|
actionColor: string,
|
|
|
|
openExternally: boolean,
|
|
|
|
) => {
|
2022-11-02 00:50:23 +03:00
|
|
|
setIsModalOpen(false);
|
2022-11-30 01:14:11 +03:00
|
|
|
handleSave(
|
|
|
|
oldAction,
|
|
|
|
actionUrl,
|
|
|
|
actionTitle,
|
|
|
|
actionDescription,
|
|
|
|
actionIcon,
|
|
|
|
actionColor,
|
|
|
|
openExternally,
|
|
|
|
);
|
|
|
|
setEditAction(null);
|
2021-03-16 01:27:19 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
const handleModalCancelButton = () => {
|
2022-11-02 00:50:23 +03:00
|
|
|
setIsModalOpen(false);
|
2021-03-16 01:27:19 +03:00
|
|
|
};
|
|
|
|
|
2021-07-09 21:42:01 +03:00
|
|
|
const columns = [
|
|
|
|
{
|
|
|
|
title: '',
|
2022-11-30 01:14:11 +03:00
|
|
|
key: 'delete-edit',
|
2021-07-09 21:42:01 +03:00
|
|
|
render: (text, record) => (
|
|
|
|
<Space size="middle">
|
|
|
|
<Button onClick={() => handleDelete(record)} icon={<DeleteOutlined />} />
|
2022-11-30 01:14:11 +03:00
|
|
|
<Button onClick={() => handleEdit(record)} icon={<EditOutlined />} />
|
2021-07-09 21:42:01 +03:00
|
|
|
</Space>
|
|
|
|
),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'Name',
|
|
|
|
dataIndex: 'title',
|
|
|
|
key: 'title',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'Description',
|
|
|
|
dataIndex: 'description',
|
|
|
|
key: 'description',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'URL',
|
|
|
|
dataIndex: 'url',
|
|
|
|
key: 'url',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'Icon',
|
|
|
|
dataIndex: 'icon',
|
|
|
|
key: 'icon',
|
|
|
|
render: (url: string) => (url ? <img style={{ width: '2vw' }} src={url} alt="" /> : null),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'Color',
|
|
|
|
dataIndex: 'color',
|
|
|
|
key: 'color',
|
|
|
|
render: (color: string) =>
|
|
|
|
color ? <div style={{ backgroundColor: color, height: '30px' }}>{color}</div> : null,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'Opens',
|
|
|
|
dataIndex: 'openExternally',
|
|
|
|
key: 'openExternally',
|
|
|
|
render: (openExternally: boolean) => (openExternally ? 'In a new tab' : 'In a modal'),
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2021-03-16 01:27:19 +03:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<Title>External Actions</Title>
|
2021-04-05 06:06:26 +03:00
|
|
|
<Paragraph>
|
|
|
|
External action URLs are 3rd party UI you can display, embedded, into your Owncast page when
|
|
|
|
a user clicks on a button to launch your action.
|
|
|
|
</Paragraph>
|
2021-03-16 01:27:19 +03:00
|
|
|
<Paragraph>
|
|
|
|
Read more about how to use actions, with examples, at{' '}
|
2021-05-23 09:27:51 +03:00
|
|
|
<a
|
|
|
|
href="https://owncast.online/thirdparty/?source=admin"
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
>
|
|
|
|
our documentation
|
|
|
|
</a>
|
|
|
|
.
|
2021-03-16 01:27:19 +03:00
|
|
|
</Paragraph>
|
|
|
|
|
2021-05-23 09:27:51 +03:00
|
|
|
<Table
|
|
|
|
rowKey={record => `${record.title}-${record.url}`}
|
|
|
|
columns={columns}
|
|
|
|
dataSource={actions}
|
|
|
|
pagination={false}
|
|
|
|
/>
|
2021-03-16 01:27:19 +03:00
|
|
|
<br />
|
|
|
|
<Button type="primary" onClick={showCreateModal}>
|
|
|
|
Create New Action
|
|
|
|
</Button>
|
|
|
|
<FormStatusIndicator status={submitStatus} />
|
|
|
|
|
2022-11-30 01:14:11 +03:00
|
|
|
<ActionModal
|
|
|
|
action={editAction}
|
2022-11-02 00:50:23 +03:00
|
|
|
open={isModalOpen}
|
2021-03-16 01:27:19 +03:00
|
|
|
onOk={handleModalSaveButton}
|
|
|
|
onCancel={handleModalCancelButton}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
2022-09-07 10:00:28 +03:00
|
|
|
};
|
2023-01-16 10:11:44 +03:00
|
|
|
|
|
|
|
Actions.getLayout = function getLayout(page: ReactElement) {
|
|
|
|
return <AdminLayout page={page} />;
|
|
|
|
};
|
2022-09-07 10:00:28 +03:00
|
|
|
export default Actions;
|