mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #6516 from viown/react-resume+streaming
Migrate resume & streaming to React
This commit is contained in:
commit
87911a343c
10 changed files with 257 additions and 172 deletions
|
@ -1,42 +0,0 @@
|
|||
<div id="playbackConfigurationPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${ButtonResume}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="playbackConfigurationForm">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${ButtonResume}</h2>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtMinResumePct" name="txtMinResumePct" pattern="[0-9]*" required min="0" max="100" label="${LabelMinResumePercentage}"></input>
|
||||
<div class="fieldDescription">
|
||||
${LabelMinResumePercentageHelp}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtMaxResumePct" name="txtMaxResumePct" pattern="[0-9]*" required min="1" max="100" label="${LabelMaxResumePercentage}"></input>
|
||||
<div class="fieldDescription">
|
||||
${LabelMaxResumePercentageHelp}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtMinAudiobookResume" name="txtMinAudiobookResume" pattern="[0-9]*" required min="0" max="100" label="${LabelMinAudiobookResume}"></input>
|
||||
<div class="fieldDescription">
|
||||
${LabelMinAudiobookResumeHelp}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtMaxAudiobookResume" name="txtMaxAudiobookResume" pattern="[0-9]*" required min="1" max="100" label="${LabelMaxAudiobookResume}"></input>
|
||||
<div class="fieldDescription">
|
||||
${LabelMaxAudiobookResumeHelp}
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtMinResumeDuration" name="txtMinResumeDuration" pattern="[0-9]*" required min="0" label="${LabelMinResumeDuration}"></input>
|
||||
<div class="fieldDescription">
|
||||
${LabelMinResumeDurationHelp}
|
||||
</div>
|
||||
</div>
|
||||
<div><button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,39 +0,0 @@
|
|||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
function loadPage(page, config) {
|
||||
page.querySelector('#txtMinResumePct').value = config.MinResumePct;
|
||||
page.querySelector('#txtMaxResumePct').value = config.MaxResumePct;
|
||||
page.querySelector('#txtMinAudiobookResume').value = config.MinAudiobookResume;
|
||||
page.querySelector('#txtMaxAudiobookResume').value = config.MaxAudiobookResume;
|
||||
page.querySelector('#txtMinResumeDuration').value = config.MinResumeDurationSeconds;
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
config.MinResumePct = form.querySelector('#txtMinResumePct').value;
|
||||
config.MaxResumePct = form.querySelector('#txtMaxResumePct').value;
|
||||
config.MinAudiobookResume = form.querySelector('#txtMinAudiobookResume').value;
|
||||
config.MaxAudiobookResume = form.querySelector('#txtMaxAudiobookResume').value;
|
||||
config.MinResumeDurationSeconds = form.querySelector('#txtMinResumeDuration').value;
|
||||
|
||||
ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#playbackConfigurationPage', function () {
|
||||
$('.playbackConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#playbackConfigurationPage', function () {
|
||||
loading.show();
|
||||
const page = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
loadPage(page, config);
|
||||
});
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
<div id="streamingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${TabStreaming}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="streamingSettingsForm">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabStreaming}</h2>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtRemoteClientBitrateLimit" inputmode="decimal" pattern="[0-9]*(\.[0-9]+)?" min="0" step=".25" label="${LabelRemoteClientBitrateLimit}" />
|
||||
<div class="fieldDescription">${LabelRemoteClientBitrateLimitHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,31 +0,0 @@
|
|||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
function loadPage(page, config) {
|
||||
page.querySelector('#txtRemoteClientBitrateLimit').value = config.RemoteClientBitrateLimit / 1e6 || '';
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
config.RemoteClientBitrateLimit = parseInt(1e6 * parseFloat(form.querySelector('#txtRemoteClientBitrateLimit').value || '0'), 10);
|
||||
ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#streamingSettingsPage', function () {
|
||||
$('.streamingSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#streamingSettingsPage', function () {
|
||||
loading.show();
|
||||
const page = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
loadPage(page, config);
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,8 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
|||
{ path: 'devices', type: AppType.Dashboard },
|
||||
{ path: 'keys', type: AppType.Dashboard },
|
||||
{ path: 'logs', type: AppType.Dashboard },
|
||||
{ path: 'playback/resume', type: AppType.Dashboard },
|
||||
{ path: 'playback/streaming', type: AppType.Dashboard },
|
||||
{ path: 'playback/trickplay', type: AppType.Dashboard },
|
||||
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard },
|
||||
{ path: 'users', type: AppType.Dashboard },
|
||||
|
|
|
@ -58,13 +58,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
controller: 'metadatanfo',
|
||||
view: 'metadatanfo.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playback/resume',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'playback',
|
||||
view: 'playback.html'
|
||||
}
|
||||
}, {
|
||||
path: 'plugins/catalog',
|
||||
pageProps: {
|
||||
|
@ -128,12 +121,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
controller: 'scheduledtasks/scheduledtasks',
|
||||
view: 'scheduledtasks/scheduledtasks.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playback/streaming',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
view: 'streaming.html',
|
||||
controller: 'streaming'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -8,8 +8,8 @@ import Stack from '@mui/material/Stack';
|
|||
import Switch from '@mui/material/Switch';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { type ActionFunctionArgs, Form, useActionData } from 'react-router-dom';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
|
||||
|
||||
import { getBrandingOptionsQuery, QUERY_KEY, useBrandingOptions } from 'apps/dashboard/features/branding/api/useBrandingOptions';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
|
@ -60,8 +60,9 @@ export const loader = () => {
|
|||
};
|
||||
|
||||
export const Component = () => {
|
||||
const navigation = useNavigation();
|
||||
const actionData = useActionData() as ActionData | undefined;
|
||||
const [ isSubmitting, setIsSubmitting ] = useState(false);
|
||||
const isSubmitting = navigation.state === 'submitting';
|
||||
|
||||
const {
|
||||
data: defaultBrandingOptions,
|
||||
|
@ -69,14 +70,6 @@ export const Component = () => {
|
|||
} = useBrandingOptions();
|
||||
const [ brandingOptions, setBrandingOptions ] = useState(defaultBrandingOptions || {});
|
||||
|
||||
useEffect(() => {
|
||||
setIsSubmitting(false);
|
||||
}, [ actionData ]);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
setIsSubmitting(true);
|
||||
}, []);
|
||||
|
||||
const setSplashscreenEnabled = useCallback((_: React.ChangeEvent<HTMLInputElement>, isEnabled: boolean) => {
|
||||
setBrandingOptions({
|
||||
...brandingOptions,
|
||||
|
@ -98,13 +91,11 @@ export const Component = () => {
|
|||
return (
|
||||
<Page
|
||||
id='brandingPage'
|
||||
title={globalize.translate('HeaderBranding')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<Box className='content-primary'>
|
||||
<Form
|
||||
method='POST'
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form method='POST'>
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h1'>
|
||||
{globalize.translate('HeaderBranding')}
|
||||
|
|
|
@ -11,7 +11,7 @@ import Stack from '@mui/material/Stack';
|
|||
import Switch from '@mui/material/Switch';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { type ActionFunctionArgs, Form, useActionData } from 'react-router-dom';
|
||||
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import { useServerLogs } from 'apps/dashboard/features/logs/api/useServerLogs';
|
||||
import { useConfiguration } from 'hooks/useConfiguration';
|
||||
|
@ -42,9 +42,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||
};
|
||||
};
|
||||
|
||||
const Logs = () => {
|
||||
export const Component = () => {
|
||||
const navigation = useNavigation();
|
||||
const actionData = useActionData() as ActionData | undefined;
|
||||
const [ isSubmitting, setIsSubmitting ] = useState(false);
|
||||
const isSubmitting = navigation.state === 'submitting';
|
||||
|
||||
const { isPending: isLogEntriesPending, data: logs } = useServerLogs();
|
||||
const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration();
|
||||
|
@ -72,10 +73,6 @@ const Logs = () => {
|
|||
});
|
||||
}, [configuration]);
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
setIsSubmitting(true);
|
||||
}, []);
|
||||
|
||||
if (isLogEntriesPending || isConfigurationPending || loading || !logs) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
@ -87,13 +84,13 @@ const Logs = () => {
|
|||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<Box className='content-primary'>
|
||||
<Form method='POST' onSubmit={onSubmit}>
|
||||
<Form method='POST'>
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h1'>
|
||||
{globalize.translate('TabLogs')}
|
||||
</Typography>
|
||||
|
||||
{isSubmitting && actionData?.isSaved && (
|
||||
{!isSubmitting && actionData?.isSaved && (
|
||||
<Alert severity='success'>
|
||||
{globalize.translate('SettingsSaved')}
|
||||
</Alert>
|
||||
|
@ -113,7 +110,7 @@ const Logs = () => {
|
|||
<TextField
|
||||
fullWidth
|
||||
type='number'
|
||||
name={'SlowResponseTime'}
|
||||
name='SlowResponseTime'
|
||||
label={globalize.translate('LabelSlowResponseTime')}
|
||||
value={configuration?.SlowResponseThresholdMs}
|
||||
disabled={!configuration?.EnableSlowResponseWarning}
|
||||
|
@ -136,4 +133,4 @@ const Logs = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Logs;
|
||||
Component.displayName = 'LogsPage';
|
||||
|
|
151
src/apps/dashboard/routes/playback/resume.tsx
Normal file
151
src/apps/dashboard/routes/playback/resume.tsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
import React from 'react';
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'lib/globalize';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
|
||||
import { ActionData } from 'types/actionData';
|
||||
import { useConfiguration } from 'hooks/useConfiguration';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||
const formData = await request.formData();
|
||||
|
||||
const minResumePercentage = formData.get('MinResumePercentage')?.toString();
|
||||
const maxResumePercentage = formData.get('MaxResumePercentage')?.toString();
|
||||
const minAudiobookResume = formData.get('MinAudiobookResume')?.toString();
|
||||
const maxAudiobookResume = formData.get('MaxAudiobookResume')?.toString();
|
||||
const minResumeDuration = formData.get('MinResumeDuration')?.toString();
|
||||
|
||||
if (minResumePercentage) config.MinResumePct = parseInt(minResumePercentage, 10);
|
||||
if (maxResumePercentage) config.MaxResumePct = parseInt(maxResumePercentage, 10);
|
||||
if (minAudiobookResume) config.MinAudiobookResume = parseInt(minAudiobookResume, 10);
|
||||
if (maxAudiobookResume) config.MaxAudiobookResume = parseInt(maxAudiobookResume, 10);
|
||||
if (minResumeDuration) config.MinResumeDurationSeconds = parseInt(minResumeDuration, 10);
|
||||
|
||||
await getConfigurationApi(api)
|
||||
.updateConfiguration({ serverConfiguration: config });
|
||||
|
||||
return {
|
||||
isSaved: true
|
||||
};
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const navigation = useNavigation();
|
||||
const actionData = useActionData() as ActionData | undefined;
|
||||
const isSubmitting = navigation.state === 'submitting';
|
||||
|
||||
const { isPending: isConfigurationPending, data: config } = useConfiguration();
|
||||
|
||||
if (isConfigurationPending) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='playbackConfigurationPage'
|
||||
title={globalize.translate('ButtonResume')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<Box className='content-primary'>
|
||||
<Form method='POST'>
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h2'>
|
||||
{globalize.translate('ButtonResume')}
|
||||
</Typography>
|
||||
|
||||
{!isSubmitting && actionData?.isSaved && (
|
||||
<Alert severity='success'>
|
||||
{globalize.translate('SettingsSaved')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label={globalize.translate('LabelMinResumePercentage')}
|
||||
name='MinResumePercentage'
|
||||
type='number'
|
||||
defaultValue={config?.MinResumePct}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
max: 100,
|
||||
required: true
|
||||
}}
|
||||
helperText={globalize.translate('LabelMinResumePercentageHelp')}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={globalize.translate('LabelMaxResumePercentage')}
|
||||
name='MaxResumePercentage'
|
||||
type='number'
|
||||
defaultValue={config?.MaxResumePct}
|
||||
inputProps={{
|
||||
min: 1,
|
||||
max: 100,
|
||||
required: true
|
||||
}}
|
||||
helperText={globalize.translate('LabelMaxResumePercentageHelp')}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={globalize.translate('LabelMinAudiobookResume')}
|
||||
name='MinAudiobookResume'
|
||||
type='number'
|
||||
defaultValue={config?.MinAudiobookResume}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
max: 100,
|
||||
required: true
|
||||
}}
|
||||
helperText={globalize.translate('LabelMinAudiobookResumeHelp')}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={globalize.translate('LabelMaxAudiobookResume')}
|
||||
name='MaxAudiobookResume'
|
||||
type='number'
|
||||
defaultValue={config?.MaxAudiobookResume}
|
||||
inputProps={{
|
||||
min: 1,
|
||||
max: 100,
|
||||
required: true
|
||||
}}
|
||||
helperText={globalize.translate('LabelMaxAudiobookResumeHelp')}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={globalize.translate('LabelMinResumeDuration')}
|
||||
name='MinResumeDuration'
|
||||
type='number'
|
||||
defaultValue={config?.MinResumeDurationSeconds}
|
||||
inputProps={{
|
||||
min: 0,
|
||||
required: true
|
||||
}}
|
||||
helperText={globalize.translate('LabelMinResumeDurationHelp')}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
size='large'
|
||||
>
|
||||
{globalize.translate('Save')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Form>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
Component.displayName = 'ResumePage';
|
90
src/apps/dashboard/routes/playback/streaming.tsx
Normal file
90
src/apps/dashboard/routes/playback/streaming.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import React from 'react';
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'lib/globalize';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
|
||||
import { useConfiguration } from 'hooks/useConfiguration';
|
||||
import Loading from 'components/loading/LoadingComponent';
|
||||
import { ActionData } from 'types/actionData';
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const api = ServerConnections.getCurrentApi();
|
||||
if (!api) throw new Error('No Api instance available');
|
||||
|
||||
const { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||
const formData = await request.formData();
|
||||
|
||||
const bitrateLimit = formData.get('StreamingBitrateLimit')?.toString();
|
||||
config.RemoteClientBitrateLimit = Math.trunc(1e6 * parseFloat(bitrateLimit || '0'));
|
||||
|
||||
await getConfigurationApi(api)
|
||||
.updateConfiguration({ serverConfiguration: config });
|
||||
|
||||
return {
|
||||
isSaved: true
|
||||
};
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const navigation = useNavigation();
|
||||
const actionData = useActionData() as ActionData | undefined;
|
||||
const isSubmitting = navigation.state === 'submitting';
|
||||
|
||||
const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration();
|
||||
|
||||
if (isConfigurationPending) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='streamingSettingsPage'
|
||||
title={globalize.translate('TabStreaming')}
|
||||
className='mainAnimatedPage type-interior'
|
||||
>
|
||||
<Box className='content-primary'>
|
||||
<Form method='POST'>
|
||||
<Stack spacing={3}>
|
||||
<Typography variant='h2'>
|
||||
{globalize.translate('TabStreaming')}
|
||||
</Typography>
|
||||
|
||||
{!isSubmitting && actionData?.isSaved && (
|
||||
<Alert severity='success'>
|
||||
{globalize.translate('SettingsSaved')}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
type='number'
|
||||
inputMode='decimal'
|
||||
inputProps={{
|
||||
min: 0,
|
||||
step: 0.25
|
||||
}}
|
||||
name='StreamingBitrateLimit'
|
||||
label={globalize.translate('LabelRemoteClientBitrateLimit')}
|
||||
helperText={globalize.translate('LabelRemoteClientBitrateLimitHelp')}
|
||||
defaultValue={defaultConfiguration?.RemoteClientBitrateLimit ? defaultConfiguration?.RemoteClientBitrateLimit / 1e6 : ''}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
size='large'
|
||||
>
|
||||
{globalize.translate('Save')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Form>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
Component.displayName = 'StreamingPage';
|
Loading…
Add table
Add a link
Reference in a new issue