Some code cleanup, add jest coverage and begin using it for utility functions
This commit is contained in:
parent
ca39830333
commit
1eb3ea2ee4
29 changed files with 2044 additions and 134 deletions
|
@ -25,6 +25,7 @@ extends:
|
|||
- "standard"
|
||||
- "plugin:react/recommended"
|
||||
- "plugin:@typescript-eslint/recommended"
|
||||
- "plugin:jest-dom/recommended"
|
||||
rules:
|
||||
quotes:
|
||||
- warn
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@babel/typescript',
|
||||
['@babel/env', {
|
||||
modules: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3,
|
||||
}],
|
||||
'@babel/react',
|
||||
],
|
||||
plugins: [
|
||||
module.exports = function (api) {
|
||||
let targets = {};
|
||||
const plugins = [
|
||||
'babel-plugin-macros',
|
||||
'styled-components',
|
||||
'react-hot-loader/babel',
|
||||
|
@ -19,5 +11,24 @@ module.exports = {
|
|||
'@babel/proposal-optional-chaining',
|
||||
'@babel/proposal-nullish-coalescing-operator',
|
||||
'@babel/syntax-dynamic-import',
|
||||
],
|
||||
];
|
||||
|
||||
if (api.env('test')) {
|
||||
targets = { node: 'current' };
|
||||
plugins.push('@babel/transform-modules-commonjs');
|
||||
}
|
||||
|
||||
return {
|
||||
plugins,
|
||||
presets: [
|
||||
'@babel/typescript',
|
||||
['@babel/env', {
|
||||
modules: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3,
|
||||
targets,
|
||||
}],
|
||||
'@babel/react',
|
||||
]
|
||||
};
|
||||
};
|
||||
|
|
28
jest.config.js
Normal file
28
jest.config.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const { pathsToModuleNameMapper } = require('ts-jest');
|
||||
const { compilerOptions } = require('./tsconfig');
|
||||
|
||||
/** @type {import('ts-jest').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
isolatedModules: true,
|
||||
},
|
||||
},
|
||||
moduleFileExtensions: ['js', 'ts', 'tsx', 'd.ts', 'json', 'node'],
|
||||
moduleNameMapper: {
|
||||
'\\.(jpe?g|png|gif|svg)$': '<rootDir>/resources/scripts/__mocks__/file.ts',
|
||||
'\\.(s?css|less)$': 'identity-obj-proxy',
|
||||
...pathsToModuleNameMapper(compilerOptions.paths, {
|
||||
prefix: '<rootDir>/',
|
||||
}),
|
||||
},
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/resources/scripts/setup-tests.ts',
|
||||
],
|
||||
transform: {
|
||||
'.*\\.[t|j]sx$': 'babel-jest',
|
||||
'.*\\.ts$': 'ts-jest',
|
||||
},
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
};
|
12
package.json
12
package.json
|
@ -60,16 +60,22 @@
|
|||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.18.2",
|
||||
"@babel/plugin-transform-react-jsx": "^7.12.1",
|
||||
"@babel/plugin-transform-runtime": "^7.12.1",
|
||||
"@babel/preset-env": "^7.12.1",
|
||||
"@babel/preset-react": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.12.1",
|
||||
"@babel/runtime": "^7.12.1",
|
||||
"@testing-library/dom": "^8.14.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@testing-library/user-event": "^14.2.1",
|
||||
"@types/chart.js": "^2.8.5",
|
||||
"@types/codemirror": "^0.0.98",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/events": "^3.0.0",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/node": "^14.11.10",
|
||||
"@types/qrcode.react": "^1.0.1",
|
||||
"@types/query-string": "^6.3.0",
|
||||
|
@ -88,6 +94,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||
"@typescript-eslint/parser": "^4.25.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"babel-jest": "^28.1.1",
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"cross-env": "^7.0.2",
|
||||
|
@ -95,11 +102,14 @@
|
|||
"eslint": "^7.27.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.23.3",
|
||||
"eslint-plugin-jest-dom": "^4.0.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.2.10",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^28.1.1",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-import": "^14.1.0",
|
||||
"postcss-loader": "^4.0.0",
|
||||
|
@ -111,6 +121,7 @@
|
|||
"svg-url-loader": "^7.1.1",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-essentials": "^9.1.2",
|
||||
"ts-jest": "^28.0.5",
|
||||
"twin.macro": "^2.8.2",
|
||||
"typescript": "^4.7.3",
|
||||
"webpack": "^4.43.0",
|
||||
|
@ -122,6 +133,7 @@
|
|||
},
|
||||
"scripts": {
|
||||
"clean": "cd public/assets && find . \\( -name \"*.js\" -o -name \"*.map\" \\) -type f -delete",
|
||||
"test": "jest",
|
||||
"lint": "eslint ./resources/scripts/**/*.{ts,tsx} --ext .ts,.tsx",
|
||||
"watch": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --watch --progress",
|
||||
"build": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --progress",
|
||||
|
|
1
resources/scripts/__mocks__/file.ts
Normal file
1
resources/scripts/__mocks__/file.ts
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = 'test-file-stub';
|
|
@ -4,7 +4,7 @@ import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome
|
|||
import { Link } from 'react-router-dom';
|
||||
import { Server } from '@/api/server/getServer';
|
||||
import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage';
|
||||
import { bytesToHuman, formatIp, megabytesToHuman } from '@/helpers';
|
||||
import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
|
||||
import tw from 'twin.macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
@ -74,8 +74,8 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
|
||||
}
|
||||
|
||||
const diskLimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : 'Unlimited';
|
||||
const memoryLimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
||||
const diskLimit = server.limits.disk !== 0 ? bytesToString(mbToBytes(server.limits.disk)) : 'Unlimited';
|
||||
const memoryLimit = server.limits.memory !== 0 ? bytesToString(mbToBytes(server.limits.memory)) : 'Unlimited';
|
||||
const cpuLimit = server.limits.cpu !== 0 ? server.limits.cpu + ' %' : 'Unlimited';
|
||||
|
||||
return (
|
||||
|
@ -98,7 +98,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<React.Fragment key={allocation.ip + allocation.port.toString()}>
|
||||
{allocation.alias || formatIp(allocation.ip)}:{allocation.port}
|
||||
{allocation.alias || ip(allocation.ip)}:{allocation.port}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
<div css={tw`flex justify-center`}>
|
||||
<Icon icon={faMemory} $alarm={alarms.memory}/>
|
||||
<IconDescription $alarm={alarms.memory}>
|
||||
{bytesToHuman(stats.memoryUsageInBytes)}
|
||||
{bytesToString(stats.memoryUsageInBytes)}
|
||||
</IconDescription>
|
||||
</div>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memoryLimit}</p>
|
||||
|
@ -155,7 +155,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
<div css={tw`flex justify-center`}>
|
||||
<Icon icon={faHdd} $alarm={alarms.disk}/>
|
||||
<IconDescription $alarm={alarms.disk}>
|
||||
{bytesToHuman(stats.diskUsageInBytes)}
|
||||
{bytesToString(stats.diskUsageInBytes)}
|
||||
</IconDescription>
|
||||
</div>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {diskLimit}</p>
|
||||
|
|
|
@ -13,7 +13,8 @@ import { Link } from 'react-router-dom';
|
|||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
import Input from '@/components/elements/Input';
|
||||
import { formatIp } from '@/helpers';
|
||||
import { ip } from '@/lib/formatters';
|
||||
|
||||
type Props = RequiredModalProps;
|
||||
|
||||
interface Values {
|
||||
|
@ -109,7 +110,7 @@ export default ({ ...props }: Props) => {
|
|||
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || formatIp(allocation.ip)}:{allocation.port}</span>
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || ip(allocation.ip)}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { FormikErrors, FormikTouched } from 'formik';
|
||||
import tw from 'twin.macro';
|
||||
import { capitalize } from '@/helpers';
|
||||
import { capitalize } from '@/lib/strings';
|
||||
|
||||
interface Props {
|
||||
errors: FormikErrors<any>;
|
||||
|
|
|
@ -8,7 +8,7 @@ import ActivityLogMetaButton from '@/components/elements/activity/ActivityLogMet
|
|||
import { TerminalIcon } from '@heroicons/react/solid';
|
||||
import classNames from 'classnames';
|
||||
import style from './style.module.css';
|
||||
import { isObject } from '@/helpers';
|
||||
import { isObject } from '@/lib/objects';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import useLocationHash from '@/plugins/useLocationHash';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import { faArchive, faEllipsisH, faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { bytesToHuman } from '@/helpers';
|
||||
import { bytesToString } from '@/lib/formatters';
|
||||
import Can from '@/components/elements/Can';
|
||||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
import BackupContextMenu from '@/components/server/backups/BackupContextMenu';
|
||||
|
@ -64,7 +64,7 @@ export default ({ backup, className }: Props) => {
|
|||
{backup.name}
|
||||
</p>
|
||||
{(backup.completedAt !== null && backup.isSuccessful) &&
|
||||
<span css={tw`ml-3 text-neutral-300 text-xs font-extralight hidden sm:inline`}>{bytesToHuman(backup.bytes)}</span>
|
||||
<span css={tw`ml-3 text-neutral-300 text-xs font-extralight hidden sm:inline`}>{bytesToString(backup.bytes)}</span>
|
||||
}
|
||||
</div>
|
||||
<p css={tw`mt-1 md:mt-0 text-xs text-neutral-400 font-mono truncate`}>
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
faMicrochip,
|
||||
faWifi,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { bytesToHuman, formatIp, megabytesToHuman } from '@/helpers';
|
||||
import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { SocketEvent, SocketRequest } from '@/components/server/events';
|
||||
import UptimeDuration from '@/components/server/UptimeDuration';
|
||||
|
@ -41,7 +41,7 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
|||
const allocation = ServerContext.useStoreState(state => {
|
||||
const match = state.server.data!.allocations.find(allocation => allocation.isDefault);
|
||||
|
||||
return !match ? 'n/a' : `${match.alias || formatIp(match.ip)}:${match.port}`;
|
||||
return !match ? 'n/a' : `${match.alias || ip(match.ip)}:${match.port}`;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -106,14 +106,14 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
|||
title={'Memory'}
|
||||
color={getBackgroundColor(stats.memory / 1024, limits.memory * 1024)}
|
||||
description={limits.memory
|
||||
? `This server is allowed to use up to ${megabytesToHuman(limits.memory)} of memory.`
|
||||
? `This server is allowed to use up to ${bytesToString(mbToBytes(limits.memory))} of memory.`
|
||||
: 'No memory limit has been configured for this server.'
|
||||
}
|
||||
>
|
||||
{status === 'offline' ?
|
||||
<span className={'text-gray-400'}>Offline</span>
|
||||
:
|
||||
bytesToHuman(stats.memory)
|
||||
bytesToString(stats.memory)
|
||||
}
|
||||
</StatBlock>
|
||||
<StatBlock
|
||||
|
@ -121,11 +121,11 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
|||
title={'Disk'}
|
||||
color={getBackgroundColor(stats.disk / 1024, limits.disk * 1024)}
|
||||
description={limits.disk
|
||||
? `This server is allowed to use up to ${megabytesToHuman(limits.disk)} of disk space.`
|
||||
? `This server is allowed to use up to ${bytesToString(mbToBytes(limits.disk))} of disk space.`
|
||||
: 'No disk space limit has been configured for this server.'
|
||||
}
|
||||
>
|
||||
{bytesToHuman(stats.disk)}
|
||||
{bytesToString(stats.disk)}
|
||||
</StatBlock>
|
||||
<StatBlock
|
||||
icon={faCloudDownloadAlt}
|
||||
|
@ -135,7 +135,7 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
|||
{status === 'offline' ?
|
||||
<span className={'text-gray-400'}>Offline</span>
|
||||
:
|
||||
bytesToHuman(stats.tx)
|
||||
bytesToString(stats.tx)
|
||||
}
|
||||
</StatBlock>
|
||||
<StatBlock
|
||||
|
@ -146,7 +146,7 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
|||
{status === 'offline' ?
|
||||
<span className={'text-gray-400'}>Offline</span>
|
||||
:
|
||||
bytesToHuman(stats.rx)
|
||||
bytesToString(stats.rx)
|
||||
}
|
||||
</StatBlock>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,8 @@ import { SocketEvent } from '@/components/server/events';
|
|||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { useChart, useChartTickLabel } from '@/components/server/console/chart';
|
||||
import { bytesToHuman, toRGBA } from '@/helpers';
|
||||
import { hexToRgba } from '@/lib/helpers';
|
||||
import { bytesToString } from '@/lib/formatters';
|
||||
import { CloudDownloadIcon, CloudUploadIcon } from '@heroicons/react/solid';
|
||||
import { theme } from 'twin.macro';
|
||||
import ChartBlock from '@/components/server/console/ChartBlock';
|
||||
|
@ -24,7 +25,7 @@ export default () => {
|
|||
y: {
|
||||
ticks: {
|
||||
callback (value) {
|
||||
return bytesToHuman(typeof value === 'string' ? parseInt(value, 10) : value);
|
||||
return bytesToString(typeof value === 'string' ? parseInt(value, 10) : value);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -35,7 +36,7 @@ export default () => {
|
|||
...opts,
|
||||
label: !index ? 'Network In' : 'Network Out',
|
||||
borderColor: !index ? theme('colors.cyan.400') : theme('colors.yellow.400'),
|
||||
backgroundColor: toRGBA(!index ? theme('colors.cyan.700') : theme('colors.yellow.700'), 0.5),
|
||||
backgroundColor: hexToRgba(!index ? theme('colors.cyan.700') : theme('colors.yellow.700'), 0.5),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ import { DeepPartial } from 'ts-essentials';
|
|||
import { useState } from 'react';
|
||||
import { deepmerge, deepmergeCustom } from 'deepmerge-ts';
|
||||
import { theme } from 'twin.macro';
|
||||
import { toRGBA } from '@/helpers';
|
||||
import { hexToRgba } from '@/lib/helpers';
|
||||
|
||||
ChartJS.register(LineElement, PointElement, Filler, LinearScale);
|
||||
|
||||
|
@ -86,7 +86,7 @@ function getEmptyData (label: string, sets = 1, callback?: ChartDatasetCallback
|
|||
label,
|
||||
data: Array(20).fill(0),
|
||||
borderColor: theme('colors.cyan.400'),
|
||||
backgroundColor: toRGBA(theme('colors.cyan.700'), 0.5),
|
||||
backgroundColor: hexToRgba(theme('colors.cyan.700'), 0.5),
|
||||
}, index)),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import features from './index';
|
||||
import { getObjectKeys } from '@/helpers';
|
||||
import { getObjectKeys } from '@/lib/objects';
|
||||
|
||||
type ListItems = [ string, React.ComponentType ][];
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faFileAlt, faFileArchive, faFileImport, faFolder } from '@fortawesome/free-solid-svg-icons';
|
||||
import { bytesToHuman, encodePathSegments } from '@/helpers';
|
||||
import { encodePathSegments } from '@/helpers';
|
||||
import { differenceInHours, format, formatDistanceToNow } from 'date-fns';
|
||||
import React, { memo } from 'react';
|
||||
import { FileObject } from '@/api/server/files/loadDirectory';
|
||||
|
@ -13,6 +13,7 @@ import styled from 'styled-components/macro';
|
|||
import SelectFileCheckbox from '@/components/server/files/SelectFileCheckbox';
|
||||
import { usePermissions } from '@/plugins/usePermissions';
|
||||
import { join } from 'path';
|
||||
import { bytesToString } from '@/lib/formatters';
|
||||
|
||||
const Row = styled.div`
|
||||
${tw`flex bg-neutral-700 rounded-sm mb-px text-sm hover:text-neutral-100 cursor-pointer items-center no-underline hover:bg-neutral-600`};
|
||||
|
@ -61,7 +62,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
|
|||
</div>
|
||||
{file.isFile &&
|
||||
<div css={tw`w-1/6 text-right mr-4 hidden sm:block`}>
|
||||
{bytesToHuman(file.size)}
|
||||
{bytesToString(file.size)}
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
|
|
|
@ -18,7 +18,7 @@ import CopyOnClick from '@/components/elements/CopyOnClick';
|
|||
import DeleteAllocationButton from '@/components/server/network/DeleteAllocationButton';
|
||||
import setPrimaryServerAllocation from '@/api/server/network/setPrimaryServerAllocation';
|
||||
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||
import { formatIp } from '@/helpers';
|
||||
import { ip } from '@/lib/formatters';
|
||||
import Code from '@/components/elements/Code';
|
||||
|
||||
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
||||
|
@ -67,7 +67,7 @@ const AllocationRow = ({ allocation }: Props) => {
|
|||
<div className={'mr-4 flex-1 md:w-40'}>
|
||||
{allocation.alias ?
|
||||
<CopyOnClick text={allocation.alias}><Code dark className={'w-40 truncate'}>{allocation.alias}</Code></CopyOnClick> :
|
||||
<CopyOnClick text={formatIp(allocation.ip)}><Code dark>{formatIp(allocation.ip)}</Code></CopyOnClick>}
|
||||
<CopyOnClick text={ip(allocation.ip)}><Code dark>{ip(allocation.ip)}</Code></CopyOnClick>}
|
||||
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
||||
</div>
|
||||
<div className={'w-16 md:w-24 overflow-hidden'}>
|
||||
|
|
|
@ -12,7 +12,7 @@ import Label from '@/components/elements/Label';
|
|||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import { formatIp } from '@/helpers';
|
||||
import { ip } from '@/lib/formatters';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
|
||||
export default () => {
|
||||
|
@ -31,10 +31,10 @@ export default () => {
|
|||
<TitledGreyBox title={'SFTP Details'} css={tw`mb-6 md:mb-10`}>
|
||||
<div>
|
||||
<Label>Server Address</Label>
|
||||
<CopyOnClick text={`sftp://${formatIp(sftp.ip)}:${sftp.port}`}>
|
||||
<CopyOnClick text={`sftp://${ip(sftp.ip)}:${sftp.port}`}>
|
||||
<Input
|
||||
type={'text'}
|
||||
value={`sftp://${formatIp(sftp.ip)}:${sftp.port}`}
|
||||
value={`sftp://${ip(sftp.ip)}:${sftp.port}`}
|
||||
readOnly
|
||||
/>
|
||||
</CopyOnClick>
|
||||
|
@ -58,7 +58,7 @@ export default () => {
|
|||
</div>
|
||||
</div>
|
||||
<div css={tw`ml-4`}>
|
||||
<a href={`sftp://${username}.${id}@${formatIp(sftp.ip)}:${sftp.port}`}>
|
||||
<a href={`sftp://${username}.${id}@${ip(sftp.ip)}:${sftp.port}`}>
|
||||
<Button.Text variant={Button.Variants.Secondary}>Launch SFTP</Button.Text>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -1,24 +1,7 @@
|
|||
export const megabytesToBytes = (mb: number) => Math.floor(mb * 1024 * 1024);
|
||||
|
||||
export function bytesToHuman (bytes: number): string {
|
||||
if (bytes < 1) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${Number((bytes / Math.pow(1024, i)).toFixed(2))} ${[ 'Bytes', 'kB', 'MB', 'GB', 'TB' ][i]}`;
|
||||
}
|
||||
|
||||
export function megabytesToHuman (mb: number): string {
|
||||
return bytesToHuman(megabytesToBytes(mb));
|
||||
}
|
||||
|
||||
export const randomInt = (low: number, high: number) => Math.floor(Math.random() * (high - low) + low);
|
||||
|
||||
export const cleanDirectoryPath = (path: string) => path.replace(/(\/(\/*))|(^$)/g, '/');
|
||||
|
||||
export const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
||||
|
||||
export function fileBitsToString (mode: string, directory: boolean): string {
|
||||
const m = parseInt(mode, 8);
|
||||
|
||||
|
@ -61,23 +44,3 @@ export function encodePathSegments (path: string): string {
|
|||
export function hashToPath (hash: string): string {
|
||||
return hash.length > 0 ? decodeURIComponent(hash.substr(1)) : '/';
|
||||
}
|
||||
|
||||
export function formatIp (ip: string): string {
|
||||
return /([a-f0-9:]+:+)+[a-f0-9]+/.test(ip) ? `[${ip}]` : ip;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export const isObject = (o: unknown): o is {} => typeof o === 'object' && o !== null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export const isEmptyObject = (o: {}): boolean =>
|
||||
Object.keys(o).length === 0 && Object.getPrototypeOf(o) === Object.prototype;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export const getObjectKeys = <T extends {}> (o: T): (keyof T)[] => Object.keys(o) as (keyof typeof o)[];
|
||||
|
||||
export const toRGBA = (hex: string, alpha = 1): string => {
|
||||
const [ r, g, b ] = hex.match(/\w\w/g)!.map(v => parseInt(v, 16));
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
|
56
resources/scripts/lib/formatters.spec.ts
Normal file
56
resources/scripts/lib/formatters.spec.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
|
||||
|
||||
describe('@/lib/formatters.ts', function () {
|
||||
describe('mbToBytes()', function () {
|
||||
it('should convert from MB to Bytes', function () {
|
||||
expect(mbToBytes(1)).toBe(1_000_000);
|
||||
expect(mbToBytes(0)).toBe(0);
|
||||
expect(mbToBytes(0.1)).toBe(100_000);
|
||||
expect(mbToBytes(0.001)).toBe(1000);
|
||||
expect(mbToBytes(1024)).toBe(1_024_000_000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bytesToString()', function () {
|
||||
it.each([
|
||||
[ 0, '0 Bytes' ],
|
||||
[ 0.5, '0 Bytes' ],
|
||||
[ 0.9, '0 Bytes' ],
|
||||
[ 100, '100 Bytes' ],
|
||||
[ 100.25, '100.25 Bytes' ],
|
||||
[ 100.998, '101 Bytes' ],
|
||||
[ 512, '512 Bytes' ],
|
||||
[ 1000, '1 KB' ],
|
||||
[ 1024, '1.02 KB' ],
|
||||
[ 5068, '5.07 KB' ],
|
||||
[ 10_000, '10 KB' ],
|
||||
[ 11_864, '11.86 KB' ],
|
||||
[ 1_000_000, '1 MB' ],
|
||||
[ 1_356_000, '1.36 MB' ],
|
||||
[ 1_024_000, '1.02 MB' ],
|
||||
[ 1_000_000_000, '1 GB' ],
|
||||
[ 1_024_000_000, '1.02 GB' ],
|
||||
[ 1_678_342_000, '1.68 GB' ],
|
||||
[ 1_000_000_000_000, '1 TB' ],
|
||||
])('should format %d bytes as "%s"', function (input, output) {
|
||||
expect(bytesToString(input)).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ip()', function () {
|
||||
it('should format an IPv4 address', function () {
|
||||
expect(ip('127.0.0.1')).toBe('127.0.0.1');
|
||||
});
|
||||
|
||||
it('should format an IPv6 address', function () {
|
||||
expect(ip(':::1')).toBe('[:::1]');
|
||||
expect(ip('2001:db8::')).toBe('[2001:db8::]');
|
||||
});
|
||||
|
||||
it('should handle random inputs', function () {
|
||||
expect(ip('1')).toBe('1');
|
||||
expect(ip('foobar')).toBe('foobar');
|
||||
expect(ip('127.0.0.1:25565')).toBe('[127.0.0.1:25565]');
|
||||
});
|
||||
});
|
||||
});
|
35
resources/scripts/lib/formatters.ts
Normal file
35
resources/scripts/lib/formatters.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
const _CONVERSION_UNIT = 1000;
|
||||
|
||||
/**
|
||||
* Given a value in megabytes converts it back down into bytes.
|
||||
*/
|
||||
function mbToBytes (megabytes: number): number {
|
||||
return Math.floor(megabytes * _CONVERSION_UNIT * _CONVERSION_UNIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an amount of bytes, converts them into a human readable string format
|
||||
* using "1000" as the divisor.
|
||||
*/
|
||||
function bytesToString (bytes: number): string {
|
||||
if (bytes < 1) return '0 Bytes';
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(_CONVERSION_UNIT));
|
||||
const value = Number((bytes / Math.pow(_CONVERSION_UNIT, i)).toFixed(2));
|
||||
|
||||
return `${value} ${[ 'Bytes', 'KB', 'MB', 'GB', 'TB' ][i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an IPv4 or IPv6 address.
|
||||
*/
|
||||
function ip (value: string): string {
|
||||
// noinspection RegExpSimplifiable
|
||||
return /([a-f0-9:]+:+)+[a-f0-9]+/.test(value) ? `[${value}]` : value;
|
||||
}
|
||||
|
||||
export {
|
||||
ip,
|
||||
mbToBytes,
|
||||
bytesToString,
|
||||
};
|
29
resources/scripts/lib/helpers.spec.ts
Normal file
29
resources/scripts/lib/helpers.spec.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { hexToRgba } from '@/lib/helpers';
|
||||
|
||||
describe('@/lib/helpers.ts', function () {
|
||||
describe('hexToRgba()', function () {
|
||||
it('should return the expected rgba', function () {
|
||||
expect(hexToRgba('#ffffff')).toBe('rgba(255, 255, 255, 1)');
|
||||
expect(hexToRgba('#00aabb')).toBe('rgba(0, 170, 187, 1)');
|
||||
expect(hexToRgba('#efefef')).toBe('rgba(239, 239, 239, 1)');
|
||||
});
|
||||
|
||||
it('should ignore case', function () {
|
||||
expect(hexToRgba('#FF00A3')).toBe('rgba(255, 0, 163, 1)');
|
||||
});
|
||||
|
||||
it('should allow alpha channel changes', function () {
|
||||
expect(hexToRgba('#ece5a8', 0.5)).toBe('rgba(236, 229, 168, 0.5)');
|
||||
expect(hexToRgba('#ece5a8', 0.1)).toBe('rgba(236, 229, 168, 0.1)');
|
||||
expect(hexToRgba('#000000', 0)).toBe('rgba(0, 0, 0, 0)');
|
||||
});
|
||||
|
||||
it('should handle invalid strings', function () {
|
||||
expect(hexToRgba('')).toBe('');
|
||||
expect(hexToRgba('foobar')).toBe('foobar');
|
||||
expect(hexToRgba('#fff')).toBe('#fff');
|
||||
expect(hexToRgba('#')).toBe('#');
|
||||
expect(hexToRgba('#fffffy')).toBe('#fffffy');
|
||||
});
|
||||
});
|
||||
});
|
17
resources/scripts/lib/helpers.ts
Normal file
17
resources/scripts/lib/helpers.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Given a valid six character HEX color code, converts it into its associated
|
||||
* RGBA value with a user controllable alpha channel.
|
||||
*/
|
||||
function hexToRgba (hex: string, alpha = 1): string {
|
||||
// noinspection RegExpSimplifiable
|
||||
if (!/#?([a-fA-F0-9]{2}){3}/.test(hex)) {
|
||||
return hex;
|
||||
}
|
||||
|
||||
// noinspection RegExpSimplifiable
|
||||
const [ r, g, b ] = hex.match(/[a-fA-F0-9]{2}/g)!.map(v => parseInt(v, 16));
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
export { hexToRgba };
|
30
resources/scripts/lib/objects.spec.ts
Normal file
30
resources/scripts/lib/objects.spec.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { isObject } from '@/lib/objects';
|
||||
|
||||
describe('@/lib/objects.ts', function () {
|
||||
describe('isObject()', function () {
|
||||
it('should return true for objects', function () {
|
||||
expect(isObject({})).toBe(true);
|
||||
expect(isObject({ foo: 123 })).toBe(true);
|
||||
expect(isObject(Object.freeze({}))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for null', function () {
|
||||
expect(isObject(null)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
undefined,
|
||||
123,
|
||||
'foobar',
|
||||
() => ({}),
|
||||
Function,
|
||||
String(123),
|
||||
isObject,
|
||||
() => null,
|
||||
[],
|
||||
[ 1, 2, 3 ],
|
||||
])('should return false for %p', function (value) {
|
||||
expect(isObject(value)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
29
resources/scripts/lib/objects.ts
Normal file
29
resources/scripts/lib/objects.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Determines if the value provided to the function is an object type that
|
||||
* is not null.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function isObject (val: unknown): val is {} {
|
||||
return typeof val === 'object' && val !== null && !Array.isArray(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an object is truly empty by looking at the keys present
|
||||
* and the prototype value.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function isEmptyObject (val: {}): boolean {
|
||||
return Object.keys(val).length === 0 && Object.getPrototypeOf(val) === Object.prototype;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function for use in TypeScript that returns all of the keys
|
||||
* for an object, but in a typed manner to make working with them a little
|
||||
* easier.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function getObjectKeys<T extends {}> (o: T): (keyof T)[] {
|
||||
return Object.keys(o) as (keyof typeof o)[];
|
||||
}
|
||||
|
||||
export { isObject, isEmptyObject, getObjectKeys };
|
14
resources/scripts/lib/strings.spec.ts
Normal file
14
resources/scripts/lib/strings.spec.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { capitalize } from '@/lib/strings';
|
||||
|
||||
describe('@/lib/strings.ts', function () {
|
||||
describe('capitalize()', function () {
|
||||
it('should capitalize a string', function () {
|
||||
expect(capitalize('foo bar')).toBe('Foo bar');
|
||||
expect(capitalize('FOOBAR')).toBe('Foobar');
|
||||
});
|
||||
|
||||
it('should handle empty strings', function () {
|
||||
expect(capitalize('')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
5
resources/scripts/lib/strings.ts
Normal file
5
resources/scripts/lib/strings.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
function capitalize (value: string): string {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
export { capitalize };
|
|
@ -3,7 +3,7 @@
|
|||
* undefined, or empty string key values. This allows the parameters to be used for
|
||||
* caching without having to account for all of the different data combinations.
|
||||
*/
|
||||
import { isEmptyObject, isObject } from '@/helpers';
|
||||
import { isEmptyObject, isObject } from '@/lib/objects';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export default <T extends {}>(data: T): T => {
|
||||
|
|
1
resources/scripts/setup-tests.ts
Normal file
1
resources/scripts/setup-tests.ts
Normal file
|
@ -0,0 +1 @@
|
|||
import '@testing-library/jest-dom';
|
Loading…
Reference in a new issue