Merge branch 'develop' into gw/2021-05-22/admin-width

This commit is contained in:
gingervitis 2021-05-22 23:36:13 -07:00
commit e5db35590c
15 changed files with 145 additions and 55 deletions

View file

@ -13,7 +13,7 @@ import {
OTHER_SOCIAL_HANDLE_OPTION, OTHER_SOCIAL_HANDLE_OPTION,
} from '../../utils/config-constants'; } from '../../utils/config-constants';
import { SocialHandle, UpdateArgs } from '../../types/config-section'; import { SocialHandle, UpdateArgs } from '../../types/config-section';
import isValidUrl from '../../utils/urls'; import isValidUrl, { DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/urls';
import TextField from './form-textfield'; import TextField from './form-textfield';
import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses'; import { createInputStatus, STATUS_ERROR, STATUS_SUCCESS } from '../../utils/input-statuses';
import FormStatusIndicator from './form-status-indicator'; import FormStatusIndicator from './form-status-indicator';
@ -62,6 +62,10 @@ export default function EditSocialLinks() {
} }
}; };
const isPredefinedSocial = (platform: string) => {
return availableIconsList.find(item => item.key === platform) || false;
};
const selectedOther = const selectedOther =
modalDataState.platform !== '' && modalDataState.platform !== '' &&
!availableIconsList.find(item => item.key === modalDataState.platform); !availableIconsList.find(item => item.key === modalDataState.platform);
@ -172,9 +176,20 @@ export default function EditSocialLinks() {
key: 'combo', key: 'combo',
render: (data, record) => { render: (data, record) => {
const { platform, url } = record; const { platform, url } = record;
const platformInfo = availableIconsList.find(item => item.key === platform); const platformInfo = isPredefinedSocial(platform);
// custom platform case
if (!platformInfo) { if (!platformInfo) {
return platform; return (
<div className="social-handle-cell">
<p className="option-label">
<strong>{platform}</strong>
<span className="handle-url" title={url}>
{url}
</span>
</p>
</div>
);
} }
const { icon, platform: platformName } = platformInfo; const { icon, platform: platformName } = platformInfo;
const iconUrl = NEXT_PUBLIC_API_HOST + `${icon.slice(1)}`; const iconUrl = NEXT_PUBLIC_API_HOST + `${icon.slice(1)}`;
@ -182,11 +197,13 @@ export default function EditSocialLinks() {
return ( return (
<div className="social-handle-cell"> <div className="social-handle-cell">
<span className="option-icon"> <span className="option-icon">
<img src={ iconUrl } alt="" className="option-icon" /> <img src={iconUrl} alt="" className="option-icon" />
</span> </span>
<p className="option-label"> <p className="option-label">
<strong>{platformName}</strong> <strong>{platformName}</strong>
<span className="handle-url" title={url}>{url}</span> <span className="handle-url" title={url}>
{url}
</span>
</p> </p>
</div> </div>
); );
@ -201,9 +218,13 @@ export default function EditSocialLinks() {
<Button <Button
size="small" size="small"
onClick={() => { onClick={() => {
const platformInfo = currentSocialHandles[index];
setEditId(index); setEditId(index);
setModalDataState({ ...currentSocialHandles[index] }); setModalDataState({ ...platformInfo });
setDisplayModal(true); setDisplayModal(true);
if (!isPredefinedSocial(platformInfo.platform)) {
setDisplayOther(true);
}
}} }}
> >
Edit Edit
@ -251,7 +272,7 @@ export default function EditSocialLinks() {
className="social-handles-table" className="social-handles-table"
pagination={false} pagination={false}
size="small" size="small"
rowKey={record => record.url} rowKey={record => `${record.platform}-${record.url}`}
columns={socialHandlesColumns} columns={socialHandlesColumns}
dataSource={currentSocialHandles} dataSource={currentSocialHandles}
/> />
@ -278,6 +299,9 @@ export default function EditSocialLinks() {
placeholder={PLACEHOLDERS[modalDataState.platform] || 'Url to page'} placeholder={PLACEHOLDERS[modalDataState.platform] || 'Url to page'}
value={modalDataState.url} value={modalDataState.url}
onChange={handleUrlChange} onChange={handleUrlChange}
useTrim
type="url"
pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
/> />
<FormStatusIndicator status={submitStatus} /> <FormStatusIndicator status={submitStatus} />
</div> </div>

View file

@ -41,6 +41,7 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
apiPath, apiPath,
configPath = '', configPath = '',
initialValue, initialValue,
useTrim,
...textFieldProps // rest of props ...textFieldProps // rest of props
} = props; } = props;
@ -70,7 +71,10 @@ export default function TextFieldWithSubmit(props: TextFieldWithSubmitProps) {
// if field is required but value is empty, or equals initial value, then don't show submit/update button. otherwise clear out any result messaging and display button. // if field is required but value is empty, or equals initial value, then don't show submit/update button. otherwise clear out any result messaging and display button.
const handleChange = ({ fieldName: changedFieldName, value: changedValue }: UpdateArgs) => { const handleChange = ({ fieldName: changedFieldName, value: changedValue }: UpdateArgs) => {
if (onChange) { if (onChange) {
onChange({ fieldName: changedFieldName, value: changedValue }); onChange({
fieldName: changedFieldName,
value: useTrim ? changedValue.trim() : changedValue,
});
} }
}; };

View file

@ -22,11 +22,13 @@ export interface TextFieldProps {
disabled?: boolean; disabled?: boolean;
label?: string; label?: string;
maxLength?: number; maxLength?: number;
pattern?: string;
placeholder?: string; placeholder?: string;
required?: boolean; required?: boolean;
status?: StatusState; status?: StatusState;
tip?: string; tip?: string;
type?: string; type?: string;
useTrim?: boolean;
value?: string | number; value?: string | number;
onBlur?: FieldUpdaterFunc; onBlur?: FieldUpdaterFunc;
onChange?: FieldUpdaterFunc; onChange?: FieldUpdaterFunc;
@ -42,20 +44,21 @@ export default function TextField(props: TextFieldProps) {
onBlur, onBlur,
onChange, onChange,
onPressEnter, onPressEnter,
pattern,
placeholder, placeholder,
required, required,
status, status,
tip, tip,
type, type,
useTrim,
value, value,
} = props; } = props;
// if field is required but value is empty, or equals initial value, then don't show submit/update button. otherwise clear out any result messaging and display button.
const handleChange = (e: any) => { const handleChange = (e: any) => {
const val = type === TEXTFIELD_TYPE_NUMBER ? e : e.target.value;
// if an extra onChange handler was sent in as a prop, let's run that too. // if an extra onChange handler was sent in as a prop, let's run that too.
if (onChange) { if (onChange) {
onChange({ fieldName, value: val }); const val = type === TEXTFIELD_TYPE_NUMBER ? e : e.target.value;
onChange({ fieldName, value: useTrim ? val.trim() : val });
} }
}; };
@ -100,6 +103,7 @@ export default function TextField(props: TextFieldProps) {
} else if (type === TEXTFIELD_TYPE_URL) { } else if (type === TEXTFIELD_TYPE_URL) {
fieldProps = { fieldProps = {
type: 'url', type: 'url',
pattern,
}; };
} }
@ -114,6 +118,7 @@ export default function TextField(props: TextFieldProps) {
required, required,
[`status-${statusType}`]: status, [`status-${statusType}`]: status,
}); });
return ( return (
<div className={containerClass}> <div className={containerClass}>
{label ? ( {label ? (
@ -130,7 +135,7 @@ export default function TextField(props: TextFieldProps) {
id={fieldId} id={fieldId}
className={`field ${className} ${fieldId}`} className={`field ${className} ${fieldId}`}
{...fieldProps} {...fieldProps}
allowClear {...(type !== TEXTFIELD_TYPE_NUMBER && { allowClear: true })}
placeholder={placeholder} placeholder={placeholder}
maxLength={maxLength} maxLength={maxLength}
onChange={handleChange} onChange={handleChange}

View file

@ -120,7 +120,7 @@ export default function CodecSelector() {
<Title level={3} className="section-title"> <Title level={3} className="section-title">
Video Codec Video Codec
</Title> </Title>
<p className="description"> <div className="description">
If you have access to specific hardware with the drivers and software installed for them, If you have access to specific hardware with the drivers and software installed for them,
you may be able to improve your video encoding performance. you may be able to improve your video encoding performance.
<p> <p>
@ -133,7 +133,7 @@ export default function CodecSelector() {
unplayable. unplayable.
</a> </a>
</p> </p>
</p> </div>
<div className="segment-slider-container"> <div className="segment-slider-container">
<Popconfirm <Popconfirm
title={`Are you sure you want to change your video codec to ${pendingSaveCodec} and understand what this means?`} title={`Are you sure you want to change your video codec to ${pendingSaveCodec} and understand what this means?`}

View file

@ -153,7 +153,7 @@ export default function VideoVariantForm({
<p className="selected-value-note">{cpuUsageNote()}</p> <p className="selected-value-note">{cpuUsageNote()}</p>
</div> </div>
<p className="read-more-subtext"> <p className="read-more-subtext">
This could mean GPU or CPU usage depending on your server environment. {' '} This could mean GPU or CPU usage depending on your server environment.{' '}
<a <a
href="https://owncast.online/docs/video/?source=admin#cpu-usage" href="https://owncast.online/docs/video/?source=admin#cpu-usage"
target="_blank" target="_blank"

View file

@ -4,7 +4,7 @@ import React, { useState, useEffect, useContext } from 'react';
import { Table, Space, Button, Modal, Checkbox, Input, Typography } from 'antd'; import { Table, Space, Button, Modal, Checkbox, Input, Typography } from 'antd';
import { ServerStatusContext } from '../utils/server-status-context'; import { ServerStatusContext } from '../utils/server-status-context';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import isValidUrl from '../utils/urls'; import isValidUrl, { DEFAULT_TEXTFIELD_URL_PATTERN } from '../utils/urls';
import FormStatusIndicator from '../components/config/form-status-indicator'; import FormStatusIndicator from '../components/config/form-status-indicator';
import { import {
createInputStatus, createInputStatus,
@ -41,12 +41,12 @@ function NewActionModal(props: Props) {
function save() { function save() {
onOk(actionUrl, actionTitle, actionDescription, actionIcon, actionColor, openExternally); onOk(actionUrl, actionTitle, actionDescription, actionIcon, actionColor, openExternally);
setActionUrl('') setActionUrl('');
setActionTitle('') setActionTitle('');
setActionDescription('') setActionDescription('');
setActionIcon('') setActionIcon('');
setActionColor('') setActionColor('');
setOpenExternally(false) setOpenExternally(false);
} }
function canSave(): Boolean { function canSave(): Boolean {
@ -91,7 +91,9 @@ function NewActionModal(props: Props) {
value={actionUrl} value={actionUrl}
required required
placeholder="https://myserver.com/action (required)" placeholder="https://myserver.com/action (required)"
onChange={input => setActionUrl(input.currentTarget.value)} onChange={input => setActionUrl(input.currentTarget.value.trim())}
type="url"
pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
/> />
</p> </p>
<p> <p>
@ -184,7 +186,7 @@ export default function Actions() {
dataIndex: 'icon', dataIndex: 'icon',
key: 'icon', key: 'icon',
render: (url: string) => { render: (url: string) => {
return url ? <img style={{width: '2vw'}} src={url} /> : null; return url ? <img style={{ width: '2vw' }} src={url} /> : null;
}, },
}, },
{ {
@ -289,11 +291,22 @@ export default function Actions() {
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
Read more about how to use actions, with examples, at{' '} Read more about how to use actions, with examples, at{' '}
<a href="https://owncast.online/thirdparty/?source=admin" target="_blank" <a
rel="noopener noreferrer">our documentation</a>. href="https://owncast.online/thirdparty/?source=admin"
target="_blank"
rel="noopener noreferrer"
>
our documentation
</a>
.
</Paragraph> </Paragraph>
<Table rowKey="id" columns={columns} dataSource={actions} pagination={false} /> <Table
rowKey={record => `${record.title}-${record.url}`}
columns={columns}
dataSource={actions}
pagination={false}
/>
<br /> <br />
<Button type="primary" onClick={showCreateModal}> <Button type="primary" onClick={showCreateModal}>
Create New Action Create New Action

View file

@ -37,7 +37,7 @@ export default function ConfigVideoSettings() {
<VideoLatency /> <VideoLatency />
</div> </div>
<Collapse className="advanced-settings"> <Collapse className="advanced-settings codec-module">
<Panel header="Advanced Settings" key="1"> <Panel header="Advanced Settings" key="1">
<div className="form-module variants-table-module"> <div className="form-module variants-table-module">
<VideoCodecSelector /> <VideoCodecSelector />

View file

@ -75,7 +75,7 @@ export default function HardwareInfo() {
<Col> <Col>
<StatisticItem <StatisticItem
title={series[0].name} title={series[0].name}
value={`${currentCPUUsage}`} value={`${currentCPUUsage || 0}`}
prefix={<LaptopOutlined style={{ color: series[0].color }} />} prefix={<LaptopOutlined style={{ color: series[0].color }} />}
color={series[0].color} color={series[0].color}
progress progress
@ -85,7 +85,7 @@ export default function HardwareInfo() {
<Col> <Col>
<StatisticItem <StatisticItem
title={series[1].name} title={series[1].name}
value={`${currentRamUsage}`} value={`${currentRamUsage || 0}`}
prefix={<BulbOutlined style={{ color: series[1].color }} />} prefix={<BulbOutlined style={{ color: series[1].color }} />}
color={series[1].color} color={series[1].color}
progress progress
@ -95,7 +95,7 @@ export default function HardwareInfo() {
<Col> <Col>
<StatisticItem <StatisticItem
title={series[2].name} title={series[2].name}
value={`${currentDiskUsage}`} value={`${currentDiskUsage || 0}`}
prefix={<SaveOutlined style={{ color: series[2].color }} />} prefix={<SaveOutlined style={{ color: series[2].color }} />}
color={series[2].color} color={series[2].color}
progress progress

View file

@ -205,8 +205,8 @@ export default function Help() {
<Title level={2}>Common tasks</Title> <Title level={2}>Common tasks</Title>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{questions.map(question => ( {questions.map(question => (
<Col xs={24} lg={12}> <Col xs={24} lg={12} key={question.title}>
<Card key={question.title}> <Card>
<Meta avatar={question.icon} title={question.title} description={question.content} /> <Meta avatar={question.icon} title={question.title} description={question.content} />
</Card> </Card>
</Col> </Col>
@ -216,8 +216,8 @@ export default function Help() {
<Title level={2}>Other</Title> <Title level={2}>Other</Title>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{otherResources.map(question => ( {otherResources.map(question => (
<Col xs={24} lg={12}> <Col xs={24} lg={12} key={question.title}>
<Card key={question.title}> <Card>
<Meta avatar={question.icon} title={question.title} description={question.content} /> <Meta avatar={question.icon} title={question.title} description={question.content} />
</Card> </Card>
</Col> </Col>

View file

@ -23,7 +23,15 @@ function AssetTable(assets) {
}, },
]; ];
return <Table dataSource={data} columns={columns} rowKey="id" size="large" pagination={false} />; return (
<Table
dataSource={data}
columns={columns}
rowKey={record => record.id}
size="large"
pagination={false}
/>
);
} }
export default function Logs() { export default function Logs() {

View file

@ -13,7 +13,7 @@ import {
Col, Col,
} from 'antd'; } from 'antd';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import isValidUrl from '../utils/urls'; import isValidUrl, { DEFAULT_TEXTFIELD_URL_PATTERN } from '../utils/urls';
import { fetchData, DELETE_WEBHOOK, CREATE_WEBHOOK, WEBHOOKS } from '../utils/apis'; import { fetchData, DELETE_WEBHOOK, CREATE_WEBHOOK, WEBHOOKS } from '../utils/apis';
@ -86,7 +86,11 @@ function NewWebhookModal(props: Props) {
}; };
const checkboxes = events.map(function (singleEvent) { const checkboxes = events.map(function (singleEvent) {
return (<Col span={8} key={singleEvent.value}><Checkbox value={singleEvent.value}>{singleEvent.label}</Checkbox></Col>) return (
<Col span={8} key={singleEvent.value}>
<Checkbox value={singleEvent.value}>{singleEvent.label}</Checkbox>
</Col>
);
}); });
return ( return (
@ -101,15 +105,15 @@ function NewWebhookModal(props: Props) {
<Input <Input
value={webhookUrl} value={webhookUrl}
placeholder="https://myserver.com/webhook" placeholder="https://myserver.com/webhook"
onChange={input => setWebhookUrl(input.currentTarget.value)} onChange={input => setWebhookUrl(input.currentTarget.value.trim())}
type="url"
pattern={DEFAULT_TEXTFIELD_URL_PATTERN}
/> />
</div> </div>
<p>Select the events that will be sent to this webhook.</p> <p>Select the events that will be sent to this webhook.</p>
<Checkbox.Group style={{ width: '100%' }} value={selectedEvents} onChange={onChange}> <Checkbox.Group style={{ width: '100%' }} value={selectedEvents} onChange={onChange}>
<Row> <Row>{checkboxes}</Row>
{checkboxes}
</Row>
</Checkbox.Group> </Checkbox.Group>
<p> <p>
<Button type="primary" onClick={selectAll}> <Button type="primary" onClick={selectAll}>
@ -225,7 +229,12 @@ export default function Webhooks() {
. .
</Paragraph> </Paragraph>
<Table rowKey="id" columns={columns} dataSource={webhooks} pagination={false} /> <Table
rowKey={record => record.id}
columns={columns}
dataSource={webhooks}
pagination={false}
/>
<br /> <br />
<Button type="primary" onClick={showCreateModal}> <Button type="primary" onClick={showCreateModal}>
Create Webhook Create Webhook

View file

@ -62,3 +62,8 @@
.read-more-subtext { .read-more-subtext {
font-size: 0.8rem; font-size: 0.8rem;
} }
.codec-module {
.ant-collapse-content-active {
background-color: var(--white-15);
}
}

View file

@ -30,6 +30,7 @@ a {
p, p,
p.description, p.description,
.description,
.ant-typography { .ant-typography {
font-weight: 300; font-weight: 300;
margin: 1em 0; margin: 1em 0;
@ -65,6 +66,10 @@ strong {
margin: 2em auto; margin: 2em auto;
padding: 1em; padding: 1em;
border: 1px solid var(--gray-dark); border: 1px solid var(--gray-dark);
canvas {
max-width: 100%;
}
} }
.form-module { .form-module {
@ -106,3 +111,11 @@ strong {
font-size: 0.92em; font-size: 0.92em;
} }
} }
input {
&:not(:focus) {
&:invalid {
color: var(--ant-error);
}
}
}

View file

@ -1,6 +1,8 @@
// DEFAULT VALUES // DEFAULT VALUES
import { fetchData, SERVER_CONFIG_UPDATE_URL } from './apis'; import { fetchData, SERVER_CONFIG_UPDATE_URL } from './apis';
import { ApiPostArgs, VideoVariant, SocialHandle } from '../types/config-section'; import { ApiPostArgs, VideoVariant, SocialHandle } from '../types/config-section';
import { TEXTFIELD_TYPE_URL } from '../components/config/form-textfield';
import { DEFAULT_TEXTFIELD_URL_PATTERN } from './urls';
export const TEXT_MAXLENGTH = 255; export const TEXT_MAXLENGTH = 255;
@ -76,8 +78,7 @@ export const TEXTFIELD_PROPS_SERVER_WELCOME_MESSAGE = {
maxLength: 500, maxLength: 500,
placeholder: '', placeholder: '',
label: 'Welcome Message', label: 'Welcome Message',
tip: tip: 'A system chat message sent to viewers when they first connect to chat. Leave blank to disable.',
'A system chat message sent to viewers when they first connect to chat. Leave blank to disable.',
}; };
export const TEXTFIELD_PROPS_LOGO = { export const TEXTFIELD_PROPS_LOGO = {
apiPath: API_LOGO, apiPath: API_LOGO,
@ -85,8 +86,7 @@ export const TEXTFIELD_PROPS_LOGO = {
maxLength: 255, maxLength: 255,
placeholder: '/img/mylogo.png', placeholder: '/img/mylogo.png',
label: 'Logo', label: 'Logo',
tip: tip: 'Upload your logo if you have one. We recommend that you use a square image that is at least 256x256.',
'Upload your logo if you have one. We recommend that you use a square image that is at least 256x256.',
}; };
export const TEXTFIELD_PROPS_STREAM_KEY = { export const TEXTFIELD_PROPS_STREAM_KEY = {
apiPath: API_STREAM_KEY, apiPath: API_STREAM_KEY,
@ -131,6 +131,9 @@ export const TEXTFIELD_PROPS_INSTANCE_URL = {
placeholder: 'https://owncast.mysite.com', placeholder: 'https://owncast.mysite.com',
label: 'Server URL', label: 'Server URL',
tip: 'The full url to your Owncast server.', tip: 'The full url to your Owncast server.',
type: TEXTFIELD_TYPE_URL,
pattern: DEFAULT_TEXTFIELD_URL_PATTERN,
useTrim: true,
}; };
// MISC FIELDS // MISC FIELDS
export const FIELD_PROPS_TAGS = { export const FIELD_PROPS_TAGS = {
@ -147,8 +150,7 @@ export const FIELD_PROPS_NSFW = {
apiPath: API_NSFW_SWITCH, apiPath: API_NSFW_SWITCH,
configPath: 'instanceDetails', configPath: 'instanceDetails',
label: 'NSFW?', label: 'NSFW?',
tip: tip: "Turn this ON if you plan to steam explicit or adult content. Please respectfully set this flag so unexpected eyes won't accidentally see it in the Directory.",
"Turn this ON if you plan to steam explicit or adult content. Please respectfully set this flag so unexpected eyes won't accidentally see it in the Directory.",
}; };
export const FIELD_PROPS_YP = { export const FIELD_PROPS_YP = {
@ -217,8 +219,7 @@ export const FRAMERATE_DEFAULTS = {
defaultValue: 24, defaultValue: 24,
unit: 'fps', unit: 'fps',
incrementBy: null, incrementBy: null,
tip: tip: 'Reducing your framerate will decrease the amount of video that needs to be encoded and sent to your viewers, saving CPU and bandwidth at the expense of smoothness. A lower value is generally is fine for most content.',
'Reducing your framerate will decrease the amount of video that needs to be encoded and sent to your viewers, saving CPU and bandwidth at the expense of smoothness. A lower value is generally is fine for most content.',
}; };
export const FRAMERATE_SLIDER_MARKS = { export const FRAMERATE_SLIDER_MARKS = {
[FRAMERATE_DEFAULTS.min]: `${FRAMERATE_DEFAULTS.min} ${FRAMERATE_DEFAULTS.unit}`, [FRAMERATE_DEFAULTS.min]: `${FRAMERATE_DEFAULTS.min} ${FRAMERATE_DEFAULTS.unit}`,
@ -247,7 +248,7 @@ export const VIDEO_BITRATE_DEFAULTS = {
export const VIDEO_NAME_DEFAULTS = { export const VIDEO_NAME_DEFAULTS = {
fieldName: 'name', fieldName: 'name',
label: 'Name', label: 'Name',
maxLength: 12, maxLength: 15,
placeholder: 'HD or Low', placeholder: 'HD or Low',
tip: 'Human-readable name for for displaying in the player.', tip: 'Human-readable name for for displaying in the player.',
}; };
@ -313,7 +314,10 @@ export const S3_TEXT_FIELDS_INFO = {
label: 'Endpoint', label: 'Endpoint',
maxLength: 255, maxLength: 255,
placeholder: 'https://your.s3.provider.endpoint.com', placeholder: 'https://your.s3.provider.endpoint.com',
tip: 'The full URL endpoint your storage provider gave you.', tip: 'The full URL (with "https://") endpoint from your storage provider.',
useTrim: true,
type: TEXTFIELD_TYPE_URL,
pattern: DEFAULT_TEXTFIELD_URL_PATTERN,
}, },
region: { region: {
fieldName: 'region', fieldName: 'region',
@ -334,7 +338,9 @@ export const S3_TEXT_FIELDS_INFO = {
label: 'Serving Endpoint', label: 'Serving Endpoint',
maxLength: 255, maxLength: 255,
placeholder: 'http://cdn.ss3.provider.endpoint.com', placeholder: 'http://cdn.ss3.provider.endpoint.com',
tip: tip: 'Optional URL that content should be accessed from instead of the default. Used with CDNs and specific storage providers. Generally not required.',
'Optional URL that content should be accessed from instead of the default. Used with CDNs and specific storage providers. Generally not required.', type: TEXTFIELD_TYPE_URL,
pattern: DEFAULT_TEXTFIELD_URL_PATTERN,
useTrim: true,
}, },
}; };

View file

@ -1,3 +1,6 @@
// to use with <input type="url"> fields, as the default pattern only checks for `:`,
export const DEFAULT_TEXTFIELD_URL_PATTERN = 'https?://.*';
export default function isValidUrl(url: string): boolean { export default function isValidUrl(url: string): boolean {
const validProtocols = ['http:', 'https:']; const validProtocols = ['http:', 'https:'];