mirror of
https://github.com/owncast/owncast.git
synced 2024-10-10 19:16:02 +00:00
Admin support for creating 3rd party external link actions (#72)
* WIP external actions * Add comment * Add support for external actions icons and colors
This commit is contained in:
parent
f2092f928e
commit
195ac23bb2
@ -182,6 +182,9 @@ export default function MainLayout(props) {
|
||||
<Menu.Item key="access-tokens">
|
||||
<Link href="/access-tokens">Access Tokens</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="actions">
|
||||
<Link href="/actions">External Actions</Link>
|
||||
</Menu.Item>
|
||||
</SubMenu>
|
||||
<Menu.Item key="help" icon={<QuestionCircleOutlined />} title="Help">
|
||||
<Link href="/help">Help</Link>
|
||||
|
276
web/pages/actions.tsx
Normal file
276
web/pages/actions.tsx
Normal file
@ -0,0 +1,276 @@
|
||||
// comment
|
||||
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Table, Space, Button, Modal, Checkbox, Input, Typography } from 'antd';
|
||||
import { ServerStatusContext } from '../utils/server-status-context';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import isValidUrl from '../utils/urls';
|
||||
import FormStatusIndicator from '../components/config/form-status-indicator';
|
||||
import {
|
||||
createInputStatus,
|
||||
StatusState,
|
||||
STATUS_ERROR,
|
||||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../utils/input-statuses';
|
||||
|
||||
import {
|
||||
postConfigUpdateToAPI,
|
||||
API_EXTERNAL_ACTIONS,
|
||||
RESET_TIMEOUT,
|
||||
} from '../utils/config-constants';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
let resetTimer = null;
|
||||
|
||||
interface Props {
|
||||
onCancel: () => void;
|
||||
onOk: any; // todo: make better type
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
function NewActionModal(props: Props) {
|
||||
const { onOk, onCancel, visible } = props;
|
||||
|
||||
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);
|
||||
|
||||
function save() {
|
||||
onOk(actionUrl, actionTitle, actionDescription, actionIcon, actionColor, openExternally);
|
||||
}
|
||||
|
||||
const okButtonProps = {
|
||||
disabled: !isValidUrl(actionUrl) || actionTitle === '',
|
||||
};
|
||||
|
||||
const onOpenExternallyChanged = checkbox => {
|
||||
setOpenExternally(checkbox.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Create New Action"
|
||||
visible={visible}
|
||||
onOk={save}
|
||||
onCancel={onCancel}
|
||||
okButtonProps={okButtonProps}
|
||||
>
|
||||
<div>
|
||||
<p>
|
||||
<Input
|
||||
value={actionUrl}
|
||||
required
|
||||
placeholder="https://myserver.com/action (required)"
|
||||
onChange={input => setActionUrl(input.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Input
|
||||
value={actionTitle}
|
||||
required
|
||||
placeholder="Your action title (required)"
|
||||
onChange={input => setActionTitle(input.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Input
|
||||
value={actionDescription}
|
||||
placeholder="Optional description"
|
||||
onChange={input => setActionDescription(input.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Input
|
||||
value={actionIcon}
|
||||
placeholder="https://myserver.com/action/icon.png (optional)"
|
||||
onChange={input => setActionIcon(input.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<Input
|
||||
type="color"
|
||||
value={actionColor}
|
||||
onChange={input => setActionColor(input.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Checkbox
|
||||
checked={openExternally}
|
||||
defaultChecked={openExternally}
|
||||
onChange={onOpenExternallyChanged}
|
||||
>
|
||||
Open in a new tab instead of within your page.
|
||||
</Checkbox>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Actions() {
|
||||
const serverStatusData = useContext(ServerStatusContext);
|
||||
const { serverConfig, setFieldInConfigState } = serverStatusData || {};
|
||||
const { externalActions } = serverConfig;
|
||||
const [actions, setActions] = useState([]);
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState(null);
|
||||
|
||||
const resetStates = () => {
|
||||
setSubmitStatus(null);
|
||||
resetTimer = null;
|
||||
clearTimeout(resetTimer);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setActions(externalActions || []);
|
||||
}, [externalActions]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '',
|
||||
key: 'delete',
|
||||
render: (text, record) => (
|
||||
<Space size="middle">
|
||||
<Button onClick={() => handleDelete(record)} icon={<DeleteOutlined />} />
|
||||
</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) => {
|
||||
return url ? <img src={url} /> : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Color',
|
||||
dataIndex: 'color',
|
||||
key: 'color',
|
||||
render: (color: string) => {
|
||||
return color ? (<div style={{backgroundColor: color, height: '30px'}}>{color}</div>) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Opens',
|
||||
dataIndex: 'openExternally',
|
||||
key: 'openExternally',
|
||||
render: (openExternally: boolean) => {
|
||||
return openExternally ? 'In a new tab' : 'In a modal';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function handleDelete(action) {
|
||||
let actionsData = [...actions];
|
||||
const index = actions.findIndex(item => item.url === action.url);
|
||||
actionsData.splice(index, 1);
|
||||
|
||||
setActions(actionsData);
|
||||
save(actionsData);
|
||||
try {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave(
|
||||
url: string,
|
||||
title: string,
|
||||
description: string,
|
||||
icon: string,
|
||||
color: string,
|
||||
openExternally: boolean,
|
||||
) {
|
||||
try {
|
||||
let actionsData = [...actions];
|
||||
const updatedActions = actionsData.concat({ url, title, description, icon, color, openExternally });
|
||||
setActions(updatedActions);
|
||||
await save(updatedActions);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
onError: (message: string) => {
|
||||
console.log(message);
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, message));
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const showCreateModal = () => {
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleModalSaveButton = (
|
||||
actionUrl: string,
|
||||
actionTitle: string,
|
||||
actionDescription: string,
|
||||
actionIcon: string,
|
||||
actionColor: string,
|
||||
openExternally: boolean,
|
||||
) => {
|
||||
setIsModalVisible(false);
|
||||
handleSave(actionUrl, actionTitle, actionDescription, actionIcon, actionColor, openExternally);
|
||||
};
|
||||
|
||||
const handleModalCancelButton = () => {
|
||||
setIsModalVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>External Actions</Title>
|
||||
<Paragraph>Description goes here.</Paragraph>
|
||||
<Paragraph>
|
||||
Read more about how to use actions, with examples, at{' '}
|
||||
<a href="https://owncast.online/thirdparty/?source=admin">our documentation</a>.
|
||||
</Paragraph>
|
||||
|
||||
<Table rowKey="id" columns={columns} dataSource={actions} pagination={false} />
|
||||
<br />
|
||||
<Button type="primary" onClick={showCreateModal}>
|
||||
Create New Action
|
||||
</Button>
|
||||
<FormStatusIndicator status={submitStatus} />
|
||||
|
||||
<NewActionModal
|
||||
visible={isModalVisible}
|
||||
onOk={handleModalSaveButton}
|
||||
onCancel={handleModalCancelButton}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -80,6 +80,13 @@ export interface S3Field {
|
||||
servingEndpoint?: string;
|
||||
}
|
||||
|
||||
export interface ExternalAction {
|
||||
title: string,
|
||||
description: string;
|
||||
url: string;
|
||||
openExternally: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigDetails {
|
||||
ffmpegPath: string;
|
||||
instanceDetails: ConfigInstanceDetailsFields;
|
||||
@ -90,4 +97,5 @@ export interface ConfigDetails {
|
||||
yp: ConfigDirectoryFields;
|
||||
videoSettings: VideoSettingsFields;
|
||||
chatDisabled: boolean;
|
||||
externalActions: ExternalAction[];
|
||||
}
|
||||
|
@ -60,9 +60,13 @@ export const DELETE_WEBHOOK = `${API_LOCATION}webhooks/delete`;
|
||||
|
||||
// Create a single webhook
|
||||
export const CREATE_WEBHOOK = `${API_LOCATION}webhooks/create`;
|
||||
|
||||
// hard coded social icons list
|
||||
export const SOCIAL_PLATFORMS_LIST = `${NEXT_PUBLIC_API_HOST}api/socialplatforms`;
|
||||
|
||||
// set external action links
|
||||
export const EXTERNAL_ACTIONS = `${API_LOCATION}api/externalactions`
|
||||
|
||||
export const API_YP_RESET = `${API_LOCATION}yp/reset`;
|
||||
|
||||
export const TEMP_UPDATER_API = LOGS_ALL;
|
||||
|
@ -27,6 +27,7 @@ export const API_VIDEO_VARIANTS = '/video/streamoutputvariants';
|
||||
export const API_WEB_PORT = '/webserverport';
|
||||
export const API_YP_SWITCH = '/directoryenabled';
|
||||
export const API_CHAT_DISABLE = '/chat/disable';
|
||||
export const API_EXTERNAL_ACTIONS = '/externalactions'
|
||||
|
||||
export async function postConfigUpdateToAPI(args: ApiPostArgs) {
|
||||
const { apiPath, data, onSuccess, onError } = args;
|
||||
|
@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { STATUS, fetchData, FETCH_INTERVAL, SERVER_CONFIG } from './apis';
|
||||
import { ConfigDetails, UpdateArgs } from '../types/config-section';
|
||||
import { ConfigDetails, UpdateArgs, ExternalAction } from '../types/config-section';
|
||||
import { DEFAULT_VARIANT_STATE } from './config-constants';
|
||||
|
||||
export const initialServerConfigState: ConfigDetails = {
|
||||
@ -44,6 +44,7 @@ export const initialServerConfigState: ConfigDetails = {
|
||||
cpuUsageLevel: 3,
|
||||
videoQualityVariants: [DEFAULT_VARIANT_STATE],
|
||||
},
|
||||
externalActions: [],
|
||||
};
|
||||
|
||||
const initialServerStatusState = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user