Merge branch 'develop' into feature/server-mounts

This commit is contained in:
Matthew Penner 2020-07-04 15:20:01 -06:00 committed by GitHub
commit 29876e023b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
166 changed files with 5482 additions and 4130 deletions

View file

@ -1,9 +1,7 @@
import http from '@/api/http';
export default (code: string): Promise<void> => {
return new Promise((resolve, reject) => {
http.post('/api/client/account/two-factor', { code })
.then(() => resolve())
.catch(reject);
});
export default async (code: string): Promise<string[]> => {
const { data } = await http.post('/api/client/account/two-factor', { code });
return data.attributes.tokens;
};

View file

@ -1,13 +1,14 @@
import http from '@/api/http';
import { LoginResponse } from '@/api/auth/login';
export default (token: string, code: string): Promise<LoginResponse> => {
export default (token: string, code: string, recoveryToken?: string): Promise<LoginResponse> => {
return new Promise((resolve, reject) => {
http.post('/auth/login/checkpoint', {
// eslint-disable-next-line @typescript-eslint/camelcase
/* eslint-disable @typescript-eslint/camelcase */
confirmation_token: token,
// eslint-disable-next-line @typescript-eslint/camelcase
authentication_code: code,
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
/* eslint-enable @typescript-eslint/camelcase */
})
.then(response => resolve({
complete: response.data.data.complete,

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import loginCheckpoint from '@/api/auth/loginCheckpoint';
import { httpErrorToHuman } from '@/api/http';
@ -14,6 +14,7 @@ import Field from '@/components/elements/Field';
interface Values {
code: string;
recoveryCode: '',
}
type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }>
@ -24,7 +25,8 @@ type Props = OwnProps & {
}
const LoginCheckpointContainer = () => {
const { isSubmitting } = useFormikContext<Values>();
const { isSubmitting, setFieldValue } = useFormikContext<Values>();
const [ isMissingDevice, setIsMissingDevice ] = useState(false);
return (
<LoginFormContainer
@ -34,10 +36,14 @@ const LoginCheckpointContainer = () => {
<div className={'mt-6'}>
<Field
light={true}
name={'code'}
title={'Authentication Code'}
description={'Enter the two-factor token generated by your device.'}
type={'number'}
name={isMissingDevice ? 'recoveryCode' : 'code'}
title={isMissingDevice ? 'Recovery Code' : 'Authentication Code'}
description={
isMissingDevice
? 'Enter one of the recovery codes generated when you setup 2-Factor authentication on this account in order to continue.'
: 'Enter the two-factor token generated by your device.'
}
type={isMissingDevice ? 'text' : 'number'}
autoFocus={true}
/>
</div>
@ -54,6 +60,18 @@ const LoginCheckpointContainer = () => {
}
</button>
</div>
<div className={'mt-6 text-center'}>
<span
onClick={() => {
setFieldValue('code', '');
setFieldValue('recoveryCode', '');
setIsMissingDevice(s => !s);
}}
className={'cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'}
>
{!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'}
</span>
</div>
<div className={'mt-6 text-center'}>
<Link
to={'/auth/login'}
@ -67,10 +85,9 @@ const LoginCheckpointContainer = () => {
};
const EnhancedForm = withFormik<Props, Values>({
handleSubmit: ({ code }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { addError, clearFlashes, location } }) => {
clearFlashes();
console.log(location.state.token, code);
loginCheckpoint(location.state?.token || '', code)
loginCheckpoint(location.state?.token || '', code, recoveryCode)
.then(response => {
if (response.complete) {
// @ts-ignore
@ -89,11 +106,7 @@ const EnhancedForm = withFormik<Props, Values>({
mapPropsToValues: () => ({
code: '',
}),
validationSchema: object().shape({
code: string().required('An authentication code must be provided.')
.length(6, 'Authentication code must be 6 digits in length.'),
recoveryCode: '',
}),
})(LoginCheckpointContainer);

View file

@ -52,6 +52,8 @@ export default ({ server, className }: { server: Server; className: string | und
alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
}
const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited";
const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited";
return (
<Link to={`/server/${server.id}`} className={`grey-row-box cursor-pointer ${className}`}>
@ -127,7 +129,7 @@ export default ({ server, className }: { server: Server; className: string | und
{bytesToHuman(stats.memoryUsageInBytes)}
</p>
</div>
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {bytesToHuman(server.limits.memory * 1000 * 1000)}</p>
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {memorylimit}</p>
</div>
<div className={'flex-1 ml-4'}>
<div className={'flex justify-center'}>
@ -147,9 +149,7 @@ export default ({ server, className }: { server: Server; className: string | und
{bytesToHuman(stats.diskUsageInBytes)}
</p>
</div>
<p className={'text-xs text-neutral-600 text-center mt-1'}>
of {bytesToHuman(server.limits.disk * 1000 * 1000)}
</p>
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {disklimit}</p>
</div>
</React.Fragment>
}

View file

@ -2,21 +2,22 @@ import React, { useEffect, useState } from 'react';
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import { Form, Formik, FormikHelpers } from 'formik';
import { object, string } from 'yup';
import Field from '@/components/elements/Field';
import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
import FlashMessageRender from '@/components/FlashMessageRender';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender';
import Field from '@/components/elements/Field';
interface Values {
code: string;
}
export default ({ ...props }: RequiredModalProps) => {
export default ({ onDismissed, ...props }: RequiredModalProps) => {
const [ token, setToken ] = useState('');
const [ loading, setLoading ] = useState(true);
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
@ -27,22 +28,30 @@ export default ({ ...props }: RequiredModalProps) => {
.then(setToken)
.catch(error => {
console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
});
}, []);
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('account:two-factor');
enableAccountTwoFactor(code)
.then(() => {
updateUserData({ useTotp: true });
props.onDismissed();
.then(tokens => {
setRecoveryTokens(tokens);
})
.catch(error => {
console.error(error);
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
setSubmitting(false);
});
})
.then(() => setSubmitting(false));
};
const dismiss = () => {
if (recoveryTokens.length > 0) {
updateUserData({ useTotp: true });
}
onDismissed();
};
return (
@ -58,47 +67,73 @@ export default ({ ...props }: RequiredModalProps) => {
{({ isSubmitting, isValid }) => (
<Modal
{...props}
onDismissed={dismiss}
dismissable={!isSubmitting}
showSpinnerOverlay={loading || isSubmitting}
closeOnEscape={!recoveryTokens}
closeOnBackground={!recoveryTokens}
>
<Form className={'mb-0'}>
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
<div className={'flex flex-wrap'}>
<div className={'w-full md:flex-1'}>
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
{!token || !token.length ?
<img
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
className={'w-64 h-64 rounded'}
{recoveryTokens.length > 0 ?
<>
<h2 className={'mb-4'}>Two-factor authentication enabled</h2>
<p className={'text-neutral-300'}>
Two-factor authentication has been enabled on your account. Should you loose access to
this device you'll need to use on of the codes displayed below in order to access your
account.
</p>
<p className={'text-neutral-300 mt-4'}>
<strong>These codes will not be displayed again.</strong> Please take note of them now
by storing them in a secure repository such as a password manager.
</p>
<pre className={'mt-4 rounded font-mono bg-neutral-900 p-4'}>
{recoveryTokens.map(token => <code key={token} className={'block mb-1'}>{token}</code>)}
</pre>
<div className={'text-right'}>
<button className={'mt-6 btn btn-lg btn-primary'} onClick={dismiss}>
Close
</button>
</div>
</>
:
<Form className={'mb-0'}>
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
<div className={'flex flex-wrap'}>
<div className={'w-full md:flex-1'}>
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
{!token || !token.length ?
<img
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
className={'w-64 h-64 rounded'}
/>
:
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
onLoad={() => setLoading(false)}
className={'w-full h-full shadow-none rounded-0'}
/>
}
</div>
</div>
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
<div className={'flex-1'}>
<Field
id={'code'}
name={'code'}
type={'text'}
title={'Code From Authenticator'}
description={'Enter the code from your authenticator device after scanning the QR image.'}
autoFocus={!loading}
/>
:
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
onLoad={() => setLoading(false)}
className={'w-full h-full shadow-none rounded-0'}
/>
}
</div>
<div className={'mt-6 md:mt-0 text-right'}>
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
Setup
</button>
</div>
</div>
</div>
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
<div className={'flex-1'}>
<Field
id={'code'}
name={'code'}
type={'text'}
title={'Code From Authenticator'}
description={'Enter the code from your authenticator device after scanning the QR image.'}
autoFocus={!loading}
/>
</div>
<div className={'mt-6 md:mt-0 text-right'}>
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
Setup
</button>
</div>
</div>
</div>
</Form>
</Form>
}
</Modal>
)}
</Formik>

View file

@ -81,6 +81,9 @@ export default () => {
};
}, [ instance, connected ]);
const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited";
const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited";
return (
<PageContentBlock className={'flex'}>
<div className={'w-1/4'}>
@ -112,8 +115,8 @@ export default () => {
className={'mr-1'}
/>
&nbsp;{bytesToHuman(memory)}
<span className={'text-neutral-500'}> / {bytesToHuman(server.limits.memory * 1000 * 1000)}</span>
</p>
<span className={'text-neutral-500'}> / {memorylimit}</span>
</p>
<p className={'text-xs mt-2'}>
<FontAwesomeIcon
icon={faHdd}
@ -121,7 +124,7 @@ export default () => {
className={'mr-1'}
/>
&nbsp;{bytesToHuman(disk)}
<span className={'text-neutral-500'}> / {bytesToHuman(server.limits.disk * 1000 * 1000)}</span>
<span className={'text-neutral-500'}> / {disklimit}</span>
</p>
</TitledGreyBox>
{!server.isInstalling ?

View file

@ -12,7 +12,7 @@ import { ServerContext } from '@/state/server';
import PageContentBlock from '@/components/elements/PageContentBlock';
export default () => {
const { uuid } = useServer();
const { uuid, featureLimits } = useServer();
const { addError, clearFlashes } = useFlash();
const [ loading, setLoading ] = useState(true);
@ -50,10 +50,22 @@ export default () => {
/>)}
</div>
}
{featureLimits.backups === 0 &&
<p className="text-center text-sm text-neutral-400">
Backups cannot be created for this server.
</p>
}
<Can action={'backup.create'}>
{(featureLimits.backups > 0 && backups.length > 0) &&
<p className="text-center text-xs text-neutral-400 mt-2">
{backups.length} of {featureLimits.backups} backups have been created for this server.
</p>
}
{featureLimits.backups > 0 && featureLimits.backups !== backups.length &&
<div className={'mt-6 flex justify-end'}>
<CreateBackupButton/>
</div>
}
</Can>
</PageContentBlock>
);

View file

@ -59,7 +59,12 @@ export default () => {
</p>
}
<Can action={'database.create'}>
{featureLimits.databases > 0 &&
{(featureLimits.databases > 0 && databases.length > 0) &&
<p className="text-center text-xs text-neutral-400 mt-2">
{databases.length} of {featureLimits.databases} databases have been allocated to this server.
</p>
}
{featureLimits.databases > 0 && featureLimits.databases !== databases.length &&
<div className={'mt-6 flex justify-end'}>
<CreateDatabaseButton/>
</div>

View file

@ -110,7 +110,7 @@ export default () => {
fetchContent={value => {
fetchFileContent = value;
}}
onContentSaved={() => null}
onContentSaved={() => save()}
/>
</div>
<div className={'flex justify-end mt-4'}>

View file

@ -25,10 +25,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
.filter(directory => !!directory)
.map((directory, index, dirs) => {
if (!withinFileEditor && index === dirs.length - 1) {
return { name: directory };
return { name: decodeURIComponent(directory) };
}
return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` };
return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` };
});
return (
@ -57,7 +57,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
}
{file &&
<React.Fragment>
<span className={'px-1 text-neutral-300'}>{file}</span>
<span className={'px-1 text-neutral-300'}>{decodeURIComponent(file)}</span>
</React.Fragment>
}
</div>

View file

@ -70,7 +70,12 @@ export default () => {
/>
<p className={'text-xs mt-2 text-neutral-400'}>
<span className={'text-neutral-200'}>This directory will be created as</span>
&nbsp;/home/container/<span className={'text-cyan-200'}>{join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')}</span>
&nbsp;/home/container/
<span className={'text-cyan-200'}>
{decodeURIComponent(
join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''),
)}
</span>
</p>
<div className={'flex justify-end'}>
<button className={'btn btn-sm btn-primary mt-8'}>

View file

@ -14,12 +14,26 @@ import Can from '@/components/elements/Can';
import useServer from '@/plugins/useServer';
import useFlash from '@/plugins/useFlash';
import { ServerContext } from '@/state/server';
import { faFileArchive } from '@fortawesome/free-solid-svg-icons/faFileArchive';
interface Props {
schedule: Schedule;
task: Task;
}
const getActionDetails = (action: string): [ string, any ] => {
switch (action) {
case 'command':
return ['Send Command', faCode];
case 'power':
return ['Send Power Action', faToggleOn];
case 'backup':
return ['Create Backup', faFileArchive];
default:
return ['Unknown Action', faCode];
}
};
export default ({ schedule, task }: Props) => {
const { uuid } = useServer();
const { clearFlashes, addError } = useFlash();
@ -43,6 +57,8 @@ export default ({ schedule, task }: Props) => {
});
};
const [ title, icon ] = getActionDetails(task.action);
return (
<div className={'flex items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}>
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
@ -56,14 +72,19 @@ export default ({ schedule, task }: Props) => {
onDismissed={() => setVisible(false)}
onConfirmed={() => onConfirmDeletion()}
/>
<FontAwesomeIcon icon={task.action === 'command' ? faCode : faToggleOn} className={'text-lg text-white'}/>
<FontAwesomeIcon icon={icon} className={'text-lg text-white'}/>
<div className={'flex-1'}>
<p className={'ml-6 text-neutral-300 mb-2 uppercase text-xs'}>
{task.action === 'command' ? 'Send command' : 'Send power action'}
<p className={'ml-6 text-neutral-300 uppercase text-xs'}>
{title}
</p>
<code className={'ml-6 font-mono bg-neutral-800 rounded py-1 px-2 text-sm'}>
{task.payload}
</code>
{task.payload &&
<div className={'ml-6 mt-2'}>
{task.action === 'backup' && <p className={'text-xs uppercase text-neutral-400 mb-1'}>Ignoring files & folders:</p>}
<div className={'font-mono bg-neutral-800 rounded py-1 px-2 text-sm w-auto whitespace-pre inline-block'}>
{task.payload}
</div>
</div>
}
</div>
{task.sequenceId > 1 &&
<div className={'mr-6'}>

View file

@ -71,7 +71,10 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
:
<div>
<label className={'input-dark-label'}>Ignored Files</label>
<FormikFieldWrapper name={'payload'}>
<FormikFieldWrapper
name={'payload'}
description={'Optional. Include the files and folders to be excluded in this backup. By default, the contents of your .pteroignore file will be used.'}
>
<FormikField as={'textarea'} name={'payload'} className={'input-dark h-32'}/>
</FormikFieldWrapper>
</div>

View file

@ -48,7 +48,7 @@ const files: ServerFileStore = {
}),
setDirectory: action((state, payload) => {
state.directory = cleanDirectoryPath(payload)
state.directory = cleanDirectoryPath(payload);
}),
};

View file

@ -123,6 +123,7 @@ select.input:not(.appearance-none) {
select.input-dark:not(.appearance-none) {
@apply .bg-neutral-600 .border-neutral-500 .text-neutral-200;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='%23C3D1DF' d='M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z'/%3e%3c/svg%3e ");
background-color: hsl(220deg 21% 16%);
&:hover:not(:disabled), &:focus {
@apply .border-neutral-400;

View file

@ -27,3 +27,39 @@ code.clean {
@apply .mt-4;
}
}
::-webkit-scrollbar {
background: none;
width: 16px;
height: 16px;
}
::-webkit-scrollbar-thumb {
border: solid 0 rgb(0 0 0 / 0%);
border-right-width: 4px;
border-left-width: 4px;
-webkit-border-radius: 9px 4px;
-webkit-box-shadow: inset 0 0 0 1px hsl(211, 10%, 53%), inset 0 0 0 4px hsl(209deg 18% 30%);
}
::-webkit-scrollbar-track-piece {
margin: 4px 0;
}
::-webkit-scrollbar-thumb:horizontal {
border-right-width: 0;
border-left-width: 0;
border-top-width: 4px;
border-bottom-width: 4px;
-webkit-border-radius: 4px 9px;
}
::-webkit-scrollbar-thumb:hover {
-webkit-box-shadow:
inset 0 0 0 1px hsl(212, 92%, 43%),
inset 0 0 0 4px hsl(212, 92%, 43%);
}
::-webkit-scrollbar-corner {
background: transparent;
}

View file

@ -74,7 +74,7 @@
swal({
type: 'success',
title: 'Token created.',
text: '<p>To auto-configure your node run the following command:<br /><small><pre>cd /etc/pterodactyl && ./wings configure --panel-url {{ config('app.url') }} --token ' + data.token + ' --node ' + data.node + '{{ config('app.debug') ? ' --allow-insecure' : '' }}</pre></small></p>',
text: '<p>To auto-configure your node run the following command:<br /><small><pre>cd /etc/pterodactyl && sudo ./wings configure --panel-url {{ config('app.url') }} --token ' + data.token + ' --node ' + data.node + '{{ config('app.debug') ? ' --allow-insecure' : '' }}</pre></small></p>',
html: true
})
}).fail(function () {

View file

@ -176,6 +176,8 @@
<input type="text" id="pMemory" name="memory" class="form-control" value="{{ old('memory') }}" />
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted small">The maximum amount of memory allowed for this container. Setting this to <code>0</code> will allow unlimited memory in a container.</p>
</div>
<div class="form-group col-xs-6">
@ -185,21 +187,18 @@
<input type="text" id="pSwap" name="swap" class="form-control" value="{{ old('swap', 0) }}" />
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted small">Setting this to <code>0</code> will disable swap space on this server. Setting to <code>-1</code> will allow unlimited swap.</p>
</div>
</div>
<div class="box-footer no-border no-pad-top no-pad-bottom">
<p class="text-muted small">If you do not want to assign swap space to a server, simply put <code>0</code> for the value, or <code>-1</code> to allow unlimited swap space. If you want to disable memory limiting on a server, simply enter <code>0</code> into the memory field.<p>
</div>
<div class="box-body row">
<div class="form-group col-xs-6">
<label for="pDisk">Disk Space</label>
<div class="input-group">
<input type="text" id="pDisk" name="disk" class="form-control" value="{{ old('disk') }}" />
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted small">This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to <code>0</code> to allow unlimited disk usage.</p>
</div>
<div class="form-group col-xs-6">

View file

@ -66,7 +66,7 @@
<input type="text" name="disk" class="form-control" value="{{ old('disk', $server->disk) }}"/>
<span class="input-group-addon">MB</span>
</div>
<p class="text-muted small">This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available.</p>
<p class="text-muted small">This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to <code>0</code> to allow unlimited disk usage.</p>
</div>
<div class="form-group">
<label for="io" class="control-label">Block IO Proportion</label>

View file

@ -97,7 +97,13 @@
</tr>
<tr>
<td>Disk Space</td>
<td><code>{{ $server->disk }}MB</code></td>
<td>
@if($server->disk === 0)
<code>Unlimited</code>
@else
<code>{{ $server->disk }}MB</code>
@endif
</td>
</tr>
<tr>
<td>Block IO Weight</td>