Apply new eslint rules; default to prettier for styling
This commit is contained in:
parent
f22cce8881
commit
dc84af9937
218 changed files with 3876 additions and 3564 deletions
57
.eslintrc.js
Normal file
57
.eslintrc.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
const prettier = {
|
||||||
|
singleQuote: true,
|
||||||
|
jsxSingleQuote: true,
|
||||||
|
printWidth: 120,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 6,
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: './',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
pragma: 'React',
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
linkComponents: [
|
||||||
|
{name: 'Link', linkAttribute: 'to'},
|
||||||
|
{name: 'NavLink', linkAttribute: 'to'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'react',
|
||||||
|
'react-hooks',
|
||||||
|
'prettier',
|
||||||
|
'@typescript-eslint',
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
// 'standard',
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:jest-dom/recommended',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
eqeqeq: 'error',
|
||||||
|
'prettier/prettier': ['error', prettier],
|
||||||
|
// This setup is required to avoid a spam of errors when running eslint about React being
|
||||||
|
// used before it is defined.
|
||||||
|
//
|
||||||
|
// @see https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md#how-to-use
|
||||||
|
'no-use-before-define': 0,
|
||||||
|
'@typescript-eslint/no-use-before-define': 'warn',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', {argsIgnorePattern: '^_', varsIgnorePattern: '^_'}],
|
||||||
|
'@typescript-eslint/ban-ts-comment': ['error', {'ts-expect-error': 'allow-with-description'}],
|
||||||
|
}
|
||||||
|
};
|
|
@ -53,8 +53,6 @@ rules:
|
||||||
multiline-ternary: 0
|
multiline-ternary: 0
|
||||||
"react-hooks/rules-of-hooks":
|
"react-hooks/rules-of-hooks":
|
||||||
- error
|
- error
|
||||||
"react-hooks/exhaustive-deps": 0
|
|
||||||
"@typescript-eslint/explicit-function-return-type": 0
|
|
||||||
"@typescript-eslint/explicit-member-accessibility": 0
|
"@typescript-eslint/explicit-member-accessibility": 0
|
||||||
"@typescript-eslint/ban-ts-ignore": 0
|
"@typescript-eslint/ban-ts-ignore": 0
|
||||||
"@typescript-eslint/no-explicit-any": 0
|
"@typescript-eslint/no-explicit-any": 0
|
||||||
|
|
16
package.json
16
package.json
|
@ -99,14 +99,17 @@
|
||||||
"babel-plugin-styled-components": "^2.0.7",
|
"babel-plugin-styled-components": "^2.0.7",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"css-loader": "^5.2.7",
|
"css-loader": "^5.2.7",
|
||||||
"eslint": "^7.27.0",
|
"eslint": "^8.18.0",
|
||||||
"eslint-config-standard": "^16.0.3",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-import": "^2.23.3",
|
"eslint-config-standard": "^17.0.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jest-dom": "^4.0.2",
|
"eslint-plugin-jest-dom": "^4.0.2",
|
||||||
|
"eslint-plugin-n": "^15.2.3",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^5.1.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-react": "^7.23.2",
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react": "^7.30.1",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.2.10",
|
"fork-ts-checker-webpack-plugin": "^6.2.10",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^28.1.1",
|
"jest": "^28.1.1",
|
||||||
|
@ -115,6 +118,7 @@
|
||||||
"postcss-loader": "^4.0.0",
|
"postcss-loader": "^4.0.0",
|
||||||
"postcss-nesting": "^10.1.8",
|
"postcss-nesting": "^10.1.8",
|
||||||
"postcss-preset-env": "^7.7.1",
|
"postcss-preset-env": "^7.7.1",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
"redux-devtools-extension": "^2.13.8",
|
"redux-devtools-extension": "^2.13.8",
|
||||||
"source-map-loader": "^1.1.3",
|
"source-map-loader": "^1.1.3",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import tw from 'twin.macro';
|
||||||
|
|
||||||
const StyledSwitchTransition = styled(SwitchTransition)`
|
const StyledSwitchTransition = styled(SwitchTransition)`
|
||||||
${tw`relative`};
|
${tw`relative`};
|
||||||
|
|
||||||
& section {
|
& section {
|
||||||
${tw`absolute w-full top-0 left-0`};
|
${tw`absolute w-full top-0 left-0`};
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,7 @@ const TransitionRouter: React.FC = ({ children }) => {
|
||||||
render={({ location }) => (
|
render={({ location }) => (
|
||||||
<StyledSwitchTransition>
|
<StyledSwitchTransition>
|
||||||
<Fade timeout={150} key={location.pathname + location.search} in appear unmountOnExit>
|
<Fade timeout={150} key={location.pathname + location.search} in appear unmountOnExit>
|
||||||
<section>
|
<section>{children}</section>
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
</Fade>
|
</Fade>
|
||||||
</StyledSwitchTransition>
|
</StyledSwitchTransition>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -8,19 +8,26 @@ import useFilteredObject from '@/plugins/useFilteredObject';
|
||||||
|
|
||||||
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
|
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
|
||||||
|
|
||||||
const useActivityLogs = (filters?: ActivityLogFilters, config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
|
const useActivityLogs = (
|
||||||
const key = useUserSWRContentKey([ 'account', 'activity', JSON.stringify(useFilteredObject(filters || {})) ]);
|
filters?: ActivityLogFilters,
|
||||||
|
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
|
||||||
|
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
|
||||||
|
const key = useUserSWRContentKey(['account', 'activity', JSON.stringify(useFilteredObject(filters || {}))]);
|
||||||
|
|
||||||
return useSWR<PaginatedResult<ActivityLog>>(key, async () => {
|
return useSWR<PaginatedResult<ActivityLog>>(
|
||||||
const { data } = await http.get('/api/client/account/activity', {
|
key,
|
||||||
params: {
|
async () => {
|
||||||
...withQueryBuilderParams(filters),
|
const { data } = await http.get('/api/client/account/activity', {
|
||||||
include: [ 'actor' ],
|
params: {
|
||||||
},
|
...withQueryBuilderParams(filters),
|
||||||
});
|
include: ['actor'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return toPaginatedSet(data, Transformers.toActivityLog);
|
return toPaginatedSet(data, Transformers.toActivityLog);
|
||||||
}, { revalidateOnMount: false, ...(config || {}) });
|
},
|
||||||
|
{ revalidateOnMount: false, ...(config || {}) }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { useActivityLogs };
|
export { useActivityLogs };
|
||||||
|
|
|
@ -7,11 +7,13 @@ export default (description: string, allowedIps: string): Promise<ApiKey & { sec
|
||||||
description,
|
description,
|
||||||
allowed_ips: allowedIps.length > 0 ? allowedIps.split('\n') : [],
|
allowed_ips: allowedIps.length > 0 ? allowedIps.split('\n') : [],
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve({
|
.then(({ data }) =>
|
||||||
...rawDataToApiKey(data.attributes),
|
resolve({
|
||||||
// eslint-disable-next-line camelcase
|
...rawDataToApiKey(data.attributes),
|
||||||
secretToken: data.meta?.secret_token ?? '',
|
// eslint-disable-next-line camelcase
|
||||||
}))
|
secretToken: data.meta?.secret_token ?? '',
|
||||||
|
})
|
||||||
|
)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,15 +5,19 @@ import { SSHKey, Transformers } from '@definitions/user';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
const useSSHKeys = (config?: ConfigInterface<SSHKey[], AxiosError>) => {
|
const useSSHKeys = (config?: ConfigInterface<SSHKey[], AxiosError>) => {
|
||||||
const key = useUserSWRContentKey([ 'account', 'ssh-keys' ]);
|
const key = useUserSWRContentKey(['account', 'ssh-keys']);
|
||||||
|
|
||||||
return useSWR(key, async () => {
|
return useSWR(
|
||||||
const { data } = await http.get('/api/client/account/ssh-keys');
|
key,
|
||||||
|
async () => {
|
||||||
|
const { data } = await http.get('/api/client/account/ssh-keys');
|
||||||
|
|
||||||
return (data as FractalResponseList).data.map((datum: any) => {
|
return (data as FractalResponseList).data.map((datum: any) => {
|
||||||
return Transformers.toSSHKey(datum.attributes);
|
return Transformers.toSSHKey(datum.attributes);
|
||||||
});
|
});
|
||||||
}, { revalidateOnMount: false, ...(config || {}) });
|
},
|
||||||
|
{ revalidateOnMount: false, ...(config || {}) }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSSHKey = async (name: string, publicKey: string): Promise<SSHKey> => {
|
const createSSHKey = async (name: string, publicKey: string): Promise<SSHKey> => {
|
||||||
|
|
|
@ -15,12 +15,14 @@ export interface LoginData {
|
||||||
export default ({ username, password, recaptchaData }: LoginData): Promise<LoginResponse> => {
|
export default ({ username, password, recaptchaData }: LoginData): Promise<LoginResponse> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get('/sanctum/csrf-cookie')
|
http.get('/sanctum/csrf-cookie')
|
||||||
.then(() => http.post('/auth/login', {
|
.then(() =>
|
||||||
user: username,
|
http.post('/auth/login', {
|
||||||
password,
|
user: username,
|
||||||
'g-recaptcha-response': recaptchaData,
|
password,
|
||||||
}))
|
'g-recaptcha-response': recaptchaData,
|
||||||
.then(response => {
|
})
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
if (!(response.data instanceof Object)) {
|
if (!(response.data instanceof Object)) {
|
||||||
return reject(new Error('An error occurred while processing the login request.'));
|
return reject(new Error('An error occurred while processing the login request.'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,14 @@ export default (token: string, code: string, recoveryToken?: string): Promise<Lo
|
||||||
http.post('/auth/login/checkpoint', {
|
http.post('/auth/login/checkpoint', {
|
||||||
confirmation_token: token,
|
confirmation_token: token,
|
||||||
authentication_code: code,
|
authentication_code: code,
|
||||||
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
|
recovery_token: recoveryToken && recoveryToken.length > 0 ? recoveryToken : undefined,
|
||||||
})
|
})
|
||||||
.then(response => resolve({
|
.then((response) =>
|
||||||
complete: response.data.data.complete,
|
resolve({
|
||||||
intended: response.data.data.intended || undefined,
|
complete: response.data.data.complete,
|
||||||
}))
|
intended: response.data.data.intended || undefined,
|
||||||
|
})
|
||||||
|
)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,10 +19,12 @@ export default (email: string, data: Data): Promise<PasswordResetResponse> => {
|
||||||
password: data.password,
|
password: data.password,
|
||||||
password_confirmation: data.passwordConfirmation,
|
password_confirmation: data.passwordConfirmation,
|
||||||
})
|
})
|
||||||
.then(response => resolve({
|
.then((response) =>
|
||||||
redirectTo: response.data.redirect_to,
|
resolve({
|
||||||
sendToLogin: response.data.send_to_login,
|
redirectTo: response.data.redirect_to,
|
||||||
}))
|
sendToLogin: response.data.send_to_login,
|
||||||
|
})
|
||||||
|
)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import http from '@/api/http';
|
||||||
export default (email: string, recaptchaData?: string): Promise<string> => {
|
export default (email: string, recaptchaData?: string): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData })
|
http.post('/auth/password', { email, 'g-recaptcha-response': recaptchaData })
|
||||||
.then(response => resolve(response.data.status || ''))
|
.then((response) => resolve(response.data.status || ''))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,9 +12,21 @@ type TransformerFunc<T> = (callback: FractalResponseData) => T;
|
||||||
const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';
|
const isList = (data: FractalResponseList | FractalResponseData): data is FractalResponseList => data.object === 'list';
|
||||||
|
|
||||||
function transform<T, M>(data: null | undefined, transformer: TransformerFunc<T>, missing?: M): M;
|
function transform<T, M>(data: null | undefined, transformer: TransformerFunc<T>, missing?: M): M;
|
||||||
function transform<T, M>(data: FractalResponseData | null | undefined, transformer: TransformerFunc<T>, missing?: M): T | M;
|
function transform<T, M>(
|
||||||
function transform<T, M>(data: FractalResponseList | FractalPaginatedResponse | null | undefined, transformer: TransformerFunc<T>, missing?: M): T[] | M;
|
data: FractalResponseData | null | undefined,
|
||||||
function transform<T> (data: FractalResponseData | FractalResponseList | FractalPaginatedResponse | null | undefined, transformer: TransformerFunc<T>, missing = undefined) {
|
transformer: TransformerFunc<T>,
|
||||||
|
missing?: M
|
||||||
|
): T | M;
|
||||||
|
function transform<T, M>(
|
||||||
|
data: FractalResponseList | FractalPaginatedResponse | null | undefined,
|
||||||
|
transformer: TransformerFunc<T>,
|
||||||
|
missing?: M
|
||||||
|
): T[] | M;
|
||||||
|
function transform<T>(
|
||||||
|
data: FractalResponseData | FractalResponseList | FractalPaginatedResponse | null | undefined,
|
||||||
|
transformer: TransformerFunc<T>,
|
||||||
|
missing = undefined
|
||||||
|
) {
|
||||||
if (data === undefined || data === null) {
|
if (data === undefined || data === null) {
|
||||||
return missing;
|
return missing;
|
||||||
}
|
}
|
||||||
|
@ -30,9 +42,9 @@ function transform<T> (data: FractalResponseData | FractalResponseList | Fractal
|
||||||
return transformer(data);
|
return transformer(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPaginatedSet<T extends TransformerFunc<Model>> (
|
function toPaginatedSet<T extends TransformerFunc<Model>>(
|
||||||
response: FractalPaginatedResponse,
|
response: FractalPaginatedResponse,
|
||||||
transformer: T,
|
transformer: T
|
||||||
): PaginatedResult<ReturnType<T>> {
|
): PaginatedResult<ReturnType<T>> {
|
||||||
return {
|
return {
|
||||||
items: transform(response, transformer) as ReturnType<T>[],
|
items: transform(response, transformer) as ReturnType<T>[],
|
||||||
|
|
2
resources/scripts/api/definitions/index.d.ts
vendored
2
resources/scripts/api/definitions/index.d.ts
vendored
|
@ -22,7 +22,7 @@ interface ModelWithRelationships extends Model {
|
||||||
*/
|
*/
|
||||||
type WithLoaded<M extends ModelWithRelationships, R extends keyof M['relationships']> = M & {
|
type WithLoaded<M extends ModelWithRelationships, R extends keyof M['relationships']> = M & {
|
||||||
relationships: MarkRequired<M['relationships'], R>;
|
relationships: MarkRequired<M['relationships'], R>;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper type that allows you to infer the type of an object by giving
|
* Helper type that allows you to infer the type of an object by giving
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface User extends Model {
|
||||||
twoFactorEnabled: boolean;
|
twoFactorEnabled: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
permissions: SubuserPermission[];
|
permissions: SubuserPermission[];
|
||||||
can (permission: SubuserPermission): boolean;
|
can(permission: SubuserPermission): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SSHKey extends Model {
|
interface SSHKey extends Model {
|
||||||
|
@ -30,5 +30,5 @@ interface ActivityLog extends Model<'actor'> {
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
relationships: {
|
relationships: {
|
||||||
actor: User | null;
|
actor: User | null;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default class Transformers {
|
||||||
fingerprint: data.fingerprint,
|
fingerprint: data.fingerprint,
|
||||||
createdAt: new Date(data.created_at),
|
createdAt: new Date(data.created_at),
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
static toUser = ({ attributes }: FractalResponseData): Models.User => {
|
static toUser = ({ attributes }: FractalResponseData): Models.User => {
|
||||||
return {
|
return {
|
||||||
|
@ -21,11 +21,11 @@ export default class Transformers {
|
||||||
twoFactorEnabled: attributes['2fa_enabled'],
|
twoFactorEnabled: attributes['2fa_enabled'],
|
||||||
permissions: attributes.permissions || [],
|
permissions: attributes.permissions || [],
|
||||||
createdAt: new Date(attributes.created_at),
|
createdAt: new Date(attributes.created_at),
|
||||||
can (permission): boolean {
|
can(permission): boolean {
|
||||||
return this.permissions.includes(permission);
|
return this.permissions.includes(permission);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => {
|
static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => {
|
||||||
const { actor } = attributes.relationships || {};
|
const { actor } = attributes.relationships || {};
|
||||||
|
@ -43,8 +43,7 @@ export default class Transformers {
|
||||||
actor: transform(actor as FractalResponseData, this.toUser, null),
|
actor: transform(actor as FractalResponseData, this.toUser, null),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MetaTransformers {
|
export class MetaTransformers {}
|
||||||
}
|
|
||||||
|
|
|
@ -15,10 +15,12 @@ export default ({ query, ...params }: QueryParams): Promise<PaginatedResult<Serv
|
||||||
...params,
|
...params,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve({
|
.then(({ data }) =>
|
||||||
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum)),
|
resolve({
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
items: (data.data || []).map((datum: any) => rawDataToServerObject(datum)),
|
||||||
}))
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
|
})
|
||||||
|
)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ const http: AxiosInstance = axios.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
http.interceptors.request.use(req => {
|
http.interceptors.request.use((req) => {
|
||||||
if (!req.url?.endsWith('/resources')) {
|
if (!req.url?.endsWith('/resources')) {
|
||||||
store.getActions().progress.startContinuous();
|
store.getActions().progress.startContinuous();
|
||||||
}
|
}
|
||||||
|
@ -19,17 +19,20 @@ http.interceptors.request.use(req => {
|
||||||
return req;
|
return req;
|
||||||
});
|
});
|
||||||
|
|
||||||
http.interceptors.response.use(resp => {
|
http.interceptors.response.use(
|
||||||
if (!resp.request?.url?.endsWith('/resources')) {
|
(resp) => {
|
||||||
|
if (!resp.request?.url?.endsWith('/resources')) {
|
||||||
|
store.getActions().progress.setComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
store.getActions().progress.setComplete();
|
store.getActions().progress.setComplete();
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return resp;
|
|
||||||
}, error => {
|
|
||||||
store.getActions().progress.setComplete();
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default http;
|
export default http;
|
||||||
|
|
||||||
|
@ -37,7 +40,7 @@ export default http;
|
||||||
* Converts an error into a human readable response. Mostly just a generic helper to
|
* Converts an error into a human readable response. Mostly just a generic helper to
|
||||||
* make sure we display the message from the server back to the user if we can.
|
* make sure we display the message from the server back to the user if we can.
|
||||||
*/
|
*/
|
||||||
export function httpErrorToHuman (error: any): string {
|
export function httpErrorToHuman(error: any): string {
|
||||||
if (error.response && error.response.data) {
|
if (error.response && error.response.data) {
|
||||||
let { data } = error.response;
|
let { data } = error.response;
|
||||||
|
|
||||||
|
@ -104,7 +107,7 @@ export interface PaginationDataSet {
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPaginationSet (data: any): PaginationDataSet {
|
export function getPaginationSet(data: any): PaginationDataSet {
|
||||||
return {
|
return {
|
||||||
total: data.total,
|
total: data.total,
|
||||||
count: data.count,
|
count: data.count,
|
||||||
|
@ -142,11 +145,11 @@ export const withQueryBuilderParams = (data?: QueryBuilderParams): Record<string
|
||||||
|
|
||||||
const sorts = Object.keys(data.sorts || {}).reduce((arr, key) => {
|
const sorts = Object.keys(data.sorts || {}).reduce((arr, key) => {
|
||||||
const value = data.sorts?.[key];
|
const value = data.sorts?.[key];
|
||||||
if (!value || ![ 'asc', 'desc', 1, -1 ].includes(value)) {
|
if (!value || !['asc', 'desc', 1, -1].includes(value)) {
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ ...arr, (value === -1 || value === 'desc' ? '-' : '') + key ];
|
return [...arr, (value === -1 || value === 'desc' ? '-' : '') + key];
|
||||||
}, [] as string[]);
|
}, [] as string[]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,14 +3,17 @@ import { AxiosError } from 'axios';
|
||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
|
|
||||||
export const setupInterceptors = (history: History) => {
|
export const setupInterceptors = (history: History) => {
|
||||||
http.interceptors.response.use(resp => resp, (error: AxiosError) => {
|
http.interceptors.response.use(
|
||||||
if (error.response?.status === 400) {
|
(resp) => resp,
|
||||||
if (error.response?.data.errors?.[0].code === 'TwoFactorAuthRequiredException') {
|
(error: AxiosError) => {
|
||||||
if (!window.location.pathname.startsWith('/account')) {
|
if (error.response?.status === 400) {
|
||||||
history.replace('/account', { twoFactorRedirect: true });
|
if (error.response?.data.errors?.[0].code === 'TwoFactorAuthRequiredException') {
|
||||||
|
if (!window.location.pathname.startsWith('/account')) {
|
||||||
|
history.replace('/account', { twoFactorRedirect: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
throw error;
|
);
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,20 +9,27 @@ import { ServerContext } from '@/state/server';
|
||||||
|
|
||||||
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
|
export type ActivityLogFilters = QueryBuilderParams<'ip' | 'event', 'timestamp'>;
|
||||||
|
|
||||||
const useActivityLogs = (filters?: ActivityLogFilters, config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
|
const useActivityLogs = (
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
|
filters?: ActivityLogFilters,
|
||||||
const key = useUserSWRContentKey([ 'server', 'activity', useFilteredObject(filters || {}) ]);
|
config?: ConfigInterface<PaginatedResult<ActivityLog>, AxiosError>
|
||||||
|
): responseInterface<PaginatedResult<ActivityLog>, AxiosError> => {
|
||||||
|
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
|
||||||
|
const key = useUserSWRContentKey(['server', 'activity', useFilteredObject(filters || {})]);
|
||||||
|
|
||||||
return useSWR<PaginatedResult<ActivityLog>>(key, async () => {
|
return useSWR<PaginatedResult<ActivityLog>>(
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/activity`, {
|
key,
|
||||||
params: {
|
async () => {
|
||||||
...withQueryBuilderParams(filters),
|
const { data } = await http.get(`/api/client/servers/${uuid}/activity`, {
|
||||||
include: [ 'actor' ],
|
params: {
|
||||||
},
|
...withQueryBuilderParams(filters),
|
||||||
});
|
include: ['actor'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return toPaginatedSet(data, Transformers.toActivityLog);
|
return toPaginatedSet(data, Transformers.toActivityLog);
|
||||||
}, { revalidateOnMount: false, ...(config || {}) });
|
},
|
||||||
|
{ revalidateOnMount: false, ...(config || {}) }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { useActivityLogs };
|
export { useActivityLogs };
|
||||||
|
|
|
@ -3,13 +3,17 @@ import http from '@/api/http';
|
||||||
|
|
||||||
export default (uuid: string, data: { connectionsFrom: string; databaseName: string }): Promise<ServerDatabase> => {
|
export default (uuid: string, data: { connectionsFrom: string; databaseName: string }): Promise<ServerDatabase> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.post(`/api/client/servers/${uuid}/databases`, {
|
http.post(
|
||||||
database: data.databaseName,
|
`/api/client/servers/${uuid}/databases`,
|
||||||
remote: data.connectionsFrom,
|
{
|
||||||
}, {
|
database: data.databaseName,
|
||||||
params: { include: 'password' },
|
remote: data.connectionsFrom,
|
||||||
})
|
},
|
||||||
.then(response => resolve(rawDataToServerDatabase(response.data.attributes)))
|
{
|
||||||
|
params: { include: 'password' },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => resolve(rawDataToServerDatabase(response.data.attributes)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,8 @@ export const rawDataToServerDatabase = (data: any): ServerDatabase => ({
|
||||||
username: data.username,
|
username: data.username,
|
||||||
connectionString: `${data.host.address}:${data.host.port}`,
|
connectionString: `${data.host.address}:${data.host.port}`,
|
||||||
allowConnectionsFrom: data.connections_from,
|
allowConnectionsFrom: data.connections_from,
|
||||||
password: data.relationships && data.relationships.password ? data.relationships.password.attributes.password : undefined,
|
password:
|
||||||
|
data.relationships && data.relationships.password ? data.relationships.password.attributes.password : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default (uuid: string, includePassword = true): Promise<ServerDatabase[]> => {
|
export default (uuid: string, includePassword = true): Promise<ServerDatabase[]> => {
|
||||||
|
@ -23,9 +24,9 @@ export default (uuid: string, includePassword = true): Promise<ServerDatabase[]>
|
||||||
http.get(`/api/client/servers/${uuid}/databases`, {
|
http.get(`/api/client/servers/${uuid}/databases`, {
|
||||||
params: includePassword ? { include: 'password' } : undefined,
|
params: includePassword ? { include: 'password' } : undefined,
|
||||||
})
|
})
|
||||||
.then(response => resolve(
|
.then((response) =>
|
||||||
(response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes))
|
resolve((response.data.data || []).map((item: any) => rawDataToServerDatabase(item.attributes)))
|
||||||
))
|
)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,10 +3,15 @@ import http from '@/api/http';
|
||||||
import { rawDataToFileObject } from '@/api/transformers';
|
import { rawDataToFileObject } from '@/api/transformers';
|
||||||
|
|
||||||
export default async (uuid: string, directory: string, files: string[]): Promise<FileObject> => {
|
export default async (uuid: string, directory: string, files: string[]): Promise<FileObject> => {
|
||||||
const { data } = await http.post(`/api/client/servers/${uuid}/files/compress`, { root: directory, files }, {
|
const { data } = await http.post(
|
||||||
timeout: 60000,
|
`/api/client/servers/${uuid}/files/compress`,
|
||||||
timeoutErrorMessage: 'It looks like this archive is taking a long time to generate. It will appear once completed.',
|
{ root: directory, files },
|
||||||
});
|
{
|
||||||
|
timeout: 60000,
|
||||||
|
timeoutErrorMessage:
|
||||||
|
'It looks like this archive is taking a long time to generate. It will appear once completed.',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return rawDataToFileObject(data);
|
return rawDataToFileObject(data);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export default async (uuid: string, directory: string, file: string): Promise<void> => {
|
export default async (uuid: string, directory: string, file: string): Promise<void> => {
|
||||||
await http.post(`/api/client/servers/${uuid}/files/decompress`, { root: directory, file }, {
|
await http.post(
|
||||||
timeout: 300000,
|
`/api/client/servers/${uuid}/files/decompress`,
|
||||||
timeoutErrorMessage: 'It looks like this archive is taking a long time to be unarchived. Once completed the unarchived files will appear.',
|
{ root: directory, file },
|
||||||
});
|
{
|
||||||
|
timeout: 300000,
|
||||||
|
timeoutErrorMessage:
|
||||||
|
'It looks like this archive is taking a long time to be unarchived. Once completed the unarchived files will appear.',
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ export default (server: string, file: string): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${server}/files/contents`, {
|
http.get(`/api/client/servers/${server}/files/contents`, {
|
||||||
params: { file },
|
params: { file },
|
||||||
transformResponse: res => res,
|
transformResponse: (res) => res,
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve(data))
|
.then(({ data }) => resolve(data))
|
||||||
|
|
|
@ -5,7 +5,7 @@ export interface FileObject {
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
modeBits: string,
|
modeBits: string;
|
||||||
size: number;
|
size: number;
|
||||||
isFile: boolean;
|
isFile: boolean;
|
||||||
isSymlink: boolean;
|
isSymlink: boolean;
|
||||||
|
|
|
@ -58,24 +58,30 @@ export const rawDataToServerObject = ({ attributes: data }: FractalResponseData)
|
||||||
ip: data.sftp_details.ip,
|
ip: data.sftp_details.ip,
|
||||||
port: data.sftp_details.port,
|
port: data.sftp_details.port,
|
||||||
},
|
},
|
||||||
description: data.description ? ((data.description.length > 0) ? data.description : null) : null,
|
description: data.description ? (data.description.length > 0 ? data.description : null) : null,
|
||||||
limits: { ...data.limits },
|
limits: { ...data.limits },
|
||||||
eggFeatures: data.egg_features || [],
|
eggFeatures: data.egg_features || [],
|
||||||
featureLimits: { ...data.feature_limits },
|
featureLimits: { ...data.feature_limits },
|
||||||
isInstalling: data.status === 'installing' || data.status === 'install_failed',
|
isInstalling: data.status === 'installing' || data.status === 'install_failed',
|
||||||
isTransferring: data.is_transferring,
|
isTransferring: data.is_transferring,
|
||||||
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(rawDataToServerEggVariable),
|
variables: ((data.relationships?.variables as FractalResponseList | undefined)?.data || []).map(
|
||||||
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
rawDataToServerEggVariable
|
||||||
|
),
|
||||||
|
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(
|
||||||
|
rawDataToServerAllocation
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default (uuid: string): Promise<[ Server, string[] ]> => {
|
export default (uuid: string): Promise<[Server, string[]]> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${uuid}`)
|
http.get(`/api/client/servers/${uuid}`)
|
||||||
.then(({ data }) => resolve([
|
.then(({ data }) =>
|
||||||
rawDataToServerObject(data),
|
resolve([
|
||||||
// eslint-disable-next-line camelcase
|
rawDataToServerObject(data),
|
||||||
data.meta?.is_server_owner ? [ '*' ] : (data.meta?.user_permissions || []),
|
// eslint-disable-next-line camelcase
|
||||||
]))
|
data.meta?.is_server_owner ? ['*'] : data.meta?.user_permissions || [],
|
||||||
|
])
|
||||||
|
)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,16 +16,18 @@ export interface ServerStats {
|
||||||
export default (server: string): Promise<ServerStats> => {
|
export default (server: string): Promise<ServerStats> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${server}/resources`)
|
http.get(`/api/client/servers/${server}/resources`)
|
||||||
.then(({ data: { attributes } }) => resolve({
|
.then(({ data: { attributes } }) =>
|
||||||
status: attributes.current_state,
|
resolve({
|
||||||
isSuspended: attributes.is_suspended,
|
status: attributes.current_state,
|
||||||
memoryUsageInBytes: attributes.resources.memory_bytes,
|
isSuspended: attributes.is_suspended,
|
||||||
cpuUsagePercent: attributes.resources.cpu_absolute,
|
memoryUsageInBytes: attributes.resources.memory_bytes,
|
||||||
diskUsageInBytes: attributes.resources.disk_bytes,
|
cpuUsagePercent: attributes.resources.cpu_absolute,
|
||||||
networkRxInBytes: attributes.resources.network_rx_bytes,
|
diskUsageInBytes: attributes.resources.disk_bytes,
|
||||||
networkTxInBytes: attributes.resources.network_tx_bytes,
|
networkRxInBytes: attributes.resources.network_rx_bytes,
|
||||||
uptime: attributes.resources.uptime,
|
networkTxInBytes: attributes.resources.network_tx_bytes,
|
||||||
}))
|
uptime: attributes.resources.uptime,
|
||||||
|
})
|
||||||
|
)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,10 +8,12 @@ interface Response {
|
||||||
export default (server: string): Promise<Response> => {
|
export default (server: string): Promise<Response> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${server}/websocket`)
|
http.get(`/api/client/servers/${server}/websocket`)
|
||||||
.then(({ data }) => resolve({
|
.then(({ data }) =>
|
||||||
token: data.data.token,
|
resolve({
|
||||||
socket: data.data.socket,
|
token: data.data.token,
|
||||||
}))
|
socket: data.data.socket,
|
||||||
|
})
|
||||||
|
)
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Allocation } from '@/api/server/getServer';
|
import { Allocation } from '@/api/server/getServer';
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export default async (uuid: string, id: number): Promise<Allocation> => await http.delete(`/api/client/servers/${uuid}/network/allocations/${id}`);
|
export default async (uuid: string, id: number): Promise<Allocation> =>
|
||||||
|
await http.delete(`/api/client/servers/${uuid}/network/allocations/${id}`);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
|
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
type Data = Pick<Schedule, 'cron' | 'name' | 'onlyWhenOnline' | 'isActive'> & { id?: number }
|
type Data = Pick<Schedule, 'cron' | 'name' | 'onlyWhenOnline' | 'isActive'> & { id?: number };
|
||||||
|
|
||||||
export default async (uuid: string, schedule: Data): Promise<Schedule> => {
|
export default async (uuid: string, schedule: Data): Promise<Schedule> => {
|
||||||
const { data } = await http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, {
|
const { data } = await http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, {
|
||||||
|
|
|
@ -9,12 +9,15 @@ interface Data {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (uuid: string, schedule: number, task: number | undefined, data: Data): Promise<Task> => {
|
export default async (uuid: string, schedule: number, task: number | undefined, data: Data): Promise<Task> => {
|
||||||
const { data: response } = await http.post(`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`, {
|
const { data: response } = await http.post(
|
||||||
action: data.action,
|
`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`,
|
||||||
payload: data.payload,
|
{
|
||||||
continue_on_failure: data.continueOnFailure,
|
action: data.action,
|
||||||
time_offset: data.timeOffset,
|
payload: data.payload,
|
||||||
});
|
continue_on_failure: data.continueOnFailure,
|
||||||
|
time_offset: data.timeOffset,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return rawDataToServerTask(response.attributes);
|
return rawDataToServerTask(response.attributes);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default (uuid: string, schedule: number): Promise<Schedule> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, {
|
http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, {
|
||||||
params: {
|
params: {
|
||||||
include: [ 'tasks' ],
|
include: ['tasks'],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
|
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
|
||||||
|
|
|
@ -69,7 +69,7 @@ export const rawDataToServerSchedule = (data: any): Schedule => ({
|
||||||
export default async (uuid: string): Promise<Schedule[]> => {
|
export default async (uuid: string): Promise<Schedule[]> => {
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/schedules`, {
|
const { data } = await http.get(`/api/client/servers/${uuid}/schedules`, {
|
||||||
params: {
|
params: {
|
||||||
include: [ 'tasks' ],
|
include: ['tasks'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ import http from '@/api/http';
|
||||||
import { ServerEggVariable } from '@/api/server/types';
|
import { ServerEggVariable } from '@/api/server/types';
|
||||||
import { rawDataToServerEggVariable } from '@/api/transformers';
|
import { rawDataToServerEggVariable } from '@/api/transformers';
|
||||||
|
|
||||||
export default async (uuid: string, key: string, value: string): Promise<[ ServerEggVariable, string ]> => {
|
export default async (uuid: string, key: string, value: string): Promise<[ServerEggVariable, string]> => {
|
||||||
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
|
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
|
||||||
|
|
||||||
return [ rawDataToServerEggVariable(data), data.meta.startup_command ];
|
return [rawDataToServerEggVariable(data), data.meta.startup_command];
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default (uuid: string, params: Params, subuser?: Subuser): Promise<Subuse
|
||||||
http.post(`/api/client/servers/${uuid}/users${subuser ? `/${subuser.uuid}` : ''}`, {
|
http.post(`/api/client/servers/${uuid}/users${subuser ? `/${subuser.uuid}` : ''}`, {
|
||||||
...params,
|
...params,
|
||||||
})
|
})
|
||||||
.then(data => resolve(rawDataToServerSubuser(data.data)))
|
.then((data) => resolve(rawDataToServerSubuser(data.data)))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const rawDataToServerSubuser = (data: FractalResponseData): Subuser => ({
|
||||||
twoFactorEnabled: data.attributes['2fa_enabled'],
|
twoFactorEnabled: data.attributes['2fa_enabled'],
|
||||||
createdAt: new Date(data.attributes.created_at),
|
createdAt: new Date(data.attributes.created_at),
|
||||||
permissions: data.attributes.permissions || [],
|
permissions: data.attributes.permissions || [],
|
||||||
can: permission => (data.attributes.permissions || []).indexOf(permission) >= 0,
|
can: (permission) => (data.attributes.permissions || []).indexOf(permission) >= 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default (uuid: string): Promise<Subuser[]> => {
|
export default (uuid: string): Promise<Subuser[]> => {
|
||||||
|
|
|
@ -5,11 +5,15 @@ import { rawDataToServerAllocation } from '@/api/transformers';
|
||||||
import { Allocation } from '@/api/server/getServer';
|
import { Allocation } from '@/api/server/getServer';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||||
|
|
||||||
return useSWR<Allocation[]>([ 'server:allocations', uuid ], async () => {
|
return useSWR<Allocation[]>(
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`);
|
['server:allocations', uuid],
|
||||||
|
async () => {
|
||||||
|
const { data } = await http.get(`/api/client/servers/${uuid}/network/allocations`);
|
||||||
|
|
||||||
return (data.data || []).map(rawDataToServerAllocation);
|
return (data.data || []).map(rawDataToServerAllocation);
|
||||||
}, { revalidateOnFocus: false, revalidateOnMount: false });
|
},
|
||||||
|
{ revalidateOnFocus: false, revalidateOnMount: false }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,15 +16,15 @@ type BackupResponse = PaginatedResult<ServerBackup> & { backupCount: number };
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { page } = useContext(Context);
|
const { page } = useContext(Context);
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||||
|
|
||||||
return useSWR<BackupResponse>([ 'server:backups', uuid, page ], async () => {
|
return useSWR<BackupResponse>(['server:backups', uuid, page], async () => {
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } });
|
const { data } = await http.get(`/api/client/servers/${uuid}/backups`, { params: { page } });
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
items: (data.data || []).map(rawDataToServerBackup),
|
items: (data.data || []).map(rawDataToServerBackup),
|
||||||
pagination: getPaginationSet(data.meta.pagination),
|
pagination: getPaginationSet(data.meta.pagination),
|
||||||
backupCount: data.meta.backup_count,
|
backupCount: data.meta.backup_count,
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,14 +9,19 @@ interface Response {
|
||||||
dockerImages: Record<string, string>;
|
dockerImages: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (uuid: string, initialData?: Response | null, config?: ConfigInterface<Response>) => useSWR([ uuid, '/startup' ], async (): Promise<Response> => {
|
export default (uuid: string, initialData?: Response | null, config?: ConfigInterface<Response>) =>
|
||||||
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
|
useSWR(
|
||||||
|
[uuid, '/startup'],
|
||||||
|
async (): Promise<Response> => {
|
||||||
|
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
|
||||||
|
|
||||||
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
|
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
variables,
|
variables,
|
||||||
invocation: data.meta.startup_command,
|
invocation: data.meta.startup_command,
|
||||||
dockerImages: data.meta.docker_images || {},
|
dockerImages: data.meta.docker_images || {},
|
||||||
};
|
};
|
||||||
}, { initialData: initialData || undefined, errorRetryCount: 3, ...(config || {}) });
|
},
|
||||||
|
{ initialData: initialData || undefined, errorRetryCount: 3, ...(config || {}) }
|
||||||
|
);
|
||||||
|
|
|
@ -25,33 +25,31 @@ export const rawDataToFileObject = (data: FractalResponseData): FileObject => ({
|
||||||
modifiedAt: new Date(data.attributes.modified_at),
|
modifiedAt: new Date(data.attributes.modified_at),
|
||||||
|
|
||||||
isArchiveType: function () {
|
isArchiveType: function () {
|
||||||
return this.isFile && [
|
return (
|
||||||
'application/vnd.rar', // .rar
|
this.isFile &&
|
||||||
'application/x-rar-compressed', // .rar (2)
|
[
|
||||||
'application/x-tar', // .tar
|
'application/vnd.rar', // .rar
|
||||||
'application/x-br', // .tar.br
|
'application/x-rar-compressed', // .rar (2)
|
||||||
'application/x-bzip2', // .tar.bz2, .bz2
|
'application/x-tar', // .tar
|
||||||
'application/gzip', // .tar.gz, .gz
|
'application/x-br', // .tar.br
|
||||||
'application/x-gzip',
|
'application/x-bzip2', // .tar.bz2, .bz2
|
||||||
'application/x-lzip', // .tar.lz4, .lz4 (not sure if this mime type is correct)
|
'application/gzip', // .tar.gz, .gz
|
||||||
'application/x-sz', // .tar.sz, .sz (not sure if this mime type is correct)
|
'application/x-gzip',
|
||||||
'application/x-xz', // .tar.xz, .xz
|
'application/x-lzip', // .tar.lz4, .lz4 (not sure if this mime type is correct)
|
||||||
'application/zstd', // .tar.zst, .zst
|
'application/x-sz', // .tar.sz, .sz (not sure if this mime type is correct)
|
||||||
'application/zip', // .zip
|
'application/x-xz', // .tar.xz, .xz
|
||||||
].indexOf(this.mimetype) >= 0;
|
'application/zstd', // .tar.zst, .zst
|
||||||
|
'application/zip', // .zip
|
||||||
|
].indexOf(this.mimetype) >= 0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
isEditable: function () {
|
isEditable: function () {
|
||||||
if (this.isArchiveType() || !this.isFile) return false;
|
if (this.isArchiveType() || !this.isFile) return false;
|
||||||
|
|
||||||
const matches = [
|
const matches = ['application/jar', 'application/octet-stream', 'inode/directory', /^image\//];
|
||||||
'application/jar',
|
|
||||||
'application/octet-stream',
|
|
||||||
'inode/directory',
|
|
||||||
/^image\//,
|
|
||||||
];
|
|
||||||
|
|
||||||
return matches.every(m => !this.mimetype.match(m));
|
return matches.every((m) => !this.mimetype.match(m));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,9 @@ import { ServerContext } from '@/state/server';
|
||||||
import '@/assets/tailwind.css';
|
import '@/assets/tailwind.css';
|
||||||
import Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
|
|
||||||
const DashboardRouter = lazy(() => import(/* webpackChunkName: "dashboard" */'@/routers/DashboardRouter'));
|
const DashboardRouter = lazy(() => import(/* webpackChunkName: "dashboard" */ '@/routers/DashboardRouter'));
|
||||||
const ServerRouter = lazy(() => import(/* webpackChunkName: "server" */'@/routers/ServerRouter'));
|
const ServerRouter = lazy(() => import(/* webpackChunkName: "server" */ '@/routers/ServerRouter'));
|
||||||
const AuthenticationRouter = lazy(() => import(/* webpackChunkName: "auth" */'@/routers/AuthenticationRouter'));
|
const AuthenticationRouter = lazy(() => import(/* webpackChunkName: "auth" */ '@/routers/AuthenticationRouter'));
|
||||||
|
|
||||||
interface ExtendedWindow extends Window {
|
interface ExtendedWindow extends Window {
|
||||||
SiteConfiguration?: SiteSettings;
|
SiteConfiguration?: SiteSettings;
|
||||||
|
@ -38,7 +38,7 @@ interface ExtendedWindow extends Window {
|
||||||
setupInterceptors(history);
|
setupInterceptors(history);
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
|
const { PterodactylUser, SiteConfiguration } = window as ExtendedWindow;
|
||||||
if (PterodactylUser && !store.getState().user.data) {
|
if (PterodactylUser && !store.getState().user.data) {
|
||||||
store.getActions().user.setUserData({
|
store.getActions().user.setUserData({
|
||||||
uuid: PterodactylUser.uuid,
|
uuid: PterodactylUser.uuid,
|
||||||
|
@ -58,31 +58,31 @@ const App = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalStylesheet/>
|
<GlobalStylesheet />
|
||||||
<StoreProvider store={store}>
|
<StoreProvider store={store}>
|
||||||
<ProgressBar/>
|
<ProgressBar />
|
||||||
<div css={tw`mx-auto w-auto`}>
|
<div css={tw`mx-auto w-auto`}>
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={'/auth'}>
|
<Route path={'/auth'}>
|
||||||
<Spinner.Suspense>
|
<Spinner.Suspense>
|
||||||
<AuthenticationRouter/>
|
<AuthenticationRouter />
|
||||||
</Spinner.Suspense>
|
</Spinner.Suspense>
|
||||||
</Route>
|
</Route>
|
||||||
<AuthenticatedRoute path={'/server/:id'}>
|
<AuthenticatedRoute path={'/server/:id'}>
|
||||||
<Spinner.Suspense>
|
<Spinner.Suspense>
|
||||||
<ServerContext.Provider>
|
<ServerContext.Provider>
|
||||||
<ServerRouter/>
|
<ServerRouter />
|
||||||
</ServerContext.Provider>
|
</ServerContext.Provider>
|
||||||
</Spinner.Suspense>
|
</Spinner.Suspense>
|
||||||
</AuthenticatedRoute>
|
</AuthenticatedRoute>
|
||||||
<AuthenticatedRoute path={'/'}>
|
<AuthenticatedRoute path={'/'}>
|
||||||
<Spinner.Suspense>
|
<Spinner.Suspense>
|
||||||
<DashboardRouter/>
|
<DashboardRouter />
|
||||||
</Spinner.Suspense>
|
</Spinner.Suspense>
|
||||||
</AuthenticatedRoute>
|
</AuthenticatedRoute>
|
||||||
<Route path={'*'}>
|
<Route path={'*'}>
|
||||||
<NotFound/>
|
<NotFound />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
@ -2,20 +2,18 @@ import React from 'react';
|
||||||
import BoringAvatar, { AvatarProps } from 'boring-avatars';
|
import BoringAvatar, { AvatarProps } from 'boring-avatars';
|
||||||
import { useStoreState } from '@/state/hooks';
|
import { useStoreState } from '@/state/hooks';
|
||||||
|
|
||||||
const palette = [ '#FFAD08', '#EDD75A', '#73B06F', '#0C8F8F', '#587291' ];
|
const palette = ['#FFAD08', '#EDD75A', '#73B06F', '#0C8F8F', '#587291'];
|
||||||
|
|
||||||
type Props = Omit<AvatarProps, 'colors'>;
|
type Props = Omit<AvatarProps, 'colors'>;
|
||||||
|
|
||||||
const _Avatar = ({ variant = 'beam', ...props }: AvatarProps) => (
|
const _Avatar = ({ variant = 'beam', ...props }: AvatarProps) => (
|
||||||
<BoringAvatar colors={palette} variant={variant} {...props}/>
|
<BoringAvatar colors={palette} variant={variant} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const _UserAvatar = ({ variant = 'beam', ...props }: Omit<Props, 'name'>) => {
|
const _UserAvatar = ({ variant = 'beam', ...props }: Omit<Props, 'name'>) => {
|
||||||
const uuid = useStoreState(state => state.user.data?.uuid);
|
const uuid = useStoreState((state) => state.user.data?.uuid);
|
||||||
|
|
||||||
return (
|
return <BoringAvatar colors={palette} name={uuid || 'system'} variant={variant} {...props} />;
|
||||||
<BoringAvatar colors={palette} name={uuid || 'system'} variant={variant} {...props} />
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_Avatar.displayName = 'Avatar';
|
_Avatar.displayName = 'Avatar';
|
||||||
|
|
|
@ -9,27 +9,22 @@ type Props = Readonly<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const FlashMessageRender = ({ byKey, className }: Props) => {
|
const FlashMessageRender = ({ byKey, className }: Props) => {
|
||||||
const flashes = useStoreState(state => state.flashes.items.filter(
|
const flashes = useStoreState((state) =>
|
||||||
flash => byKey ? flash.key === byKey : true,
|
state.flashes.items.filter((flash) => (byKey ? flash.key === byKey : true))
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
flashes.length ?
|
|
||||||
<div className={className}>
|
|
||||||
{
|
|
||||||
flashes.map((flash, index) => (
|
|
||||||
<React.Fragment key={flash.id || flash.type + flash.message}>
|
|
||||||
{index > 0 && <div css={tw`mt-2`}></div>}
|
|
||||||
<MessageBox type={flash.type} title={flash.title}>
|
|
||||||
{flash.message}
|
|
||||||
</MessageBox>
|
|
||||||
</React.Fragment>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return flashes.length ? (
|
||||||
|
<div className={className}>
|
||||||
|
{flashes.map((flash, index) => (
|
||||||
|
<React.Fragment key={flash.id || flash.type + flash.message}>
|
||||||
|
{index > 0 && <div css={tw`mt-2`}></div>}
|
||||||
|
<MessageBox type={flash.type} title={flash.title}>
|
||||||
|
{flash.message}
|
||||||
|
</MessageBox>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FlashMessageRender;
|
export default FlashMessageRender;
|
||||||
|
|
|
@ -42,26 +42,24 @@ const getBackground = (type?: FlashMessageType): TwStyle | string => {
|
||||||
|
|
||||||
const Container = styled.div<{ $type?: FlashMessageType }>`
|
const Container = styled.div<{ $type?: FlashMessageType }>`
|
||||||
${tw`p-2 border items-center leading-normal rounded flex w-full text-sm text-white`};
|
${tw`p-2 border items-center leading-normal rounded flex w-full text-sm text-white`};
|
||||||
${props => styling(props.$type)};
|
${(props) => styling(props.$type)};
|
||||||
`;
|
`;
|
||||||
Container.displayName = 'MessageBox.Container';
|
Container.displayName = 'MessageBox.Container';
|
||||||
|
|
||||||
const MessageBox = ({ title, children, type }: Props) => (
|
const MessageBox = ({ title, children, type }: Props) => (
|
||||||
<Container css={tw`lg:inline-flex`} $type={type} role={'alert'}>
|
<Container css={tw`lg:inline-flex`} $type={type} role={'alert'}>
|
||||||
{title &&
|
{title && (
|
||||||
<span
|
<span
|
||||||
className={'title'}
|
className={'title'}
|
||||||
css={[
|
css={[
|
||||||
tw`flex rounded-full uppercase px-2 py-1 text-xs font-bold mr-3 leading-none`,
|
tw`flex rounded-full uppercase px-2 py-1 text-xs font-bold mr-3 leading-none`,
|
||||||
getBackground(type),
|
getBackground(type),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
<span css={tw`mr-2 text-left flex-auto`}>
|
<span css={tw`mr-2 text-left flex-auto`}>{children}</span>
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
MessageBox.displayName = 'MessageBox';
|
MessageBox.displayName = 'MessageBox';
|
||||||
|
|
|
@ -14,14 +14,19 @@ import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||||
import Avatar from '@/components/Avatar';
|
import Avatar from '@/components/Avatar';
|
||||||
|
|
||||||
const RightNavigation = styled.div`
|
const RightNavigation = styled.div`
|
||||||
& > a, & > button, & > .navigation-link {
|
& > a,
|
||||||
|
& > button,
|
||||||
|
& > .navigation-link {
|
||||||
${tw`flex items-center h-full no-underline text-neutral-300 px-6 cursor-pointer transition-all duration-150`};
|
${tw`flex items-center h-full no-underline text-neutral-300 px-6 cursor-pointer transition-all duration-150`};
|
||||||
|
|
||||||
&:active, &:hover {
|
&:active,
|
||||||
|
&:hover {
|
||||||
${tw`text-neutral-100 bg-black`};
|
${tw`text-neutral-100 bg-black`};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active, &:hover, &.active {
|
&:active,
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
box-shadow: inset 0 -2px ${theme`colors.cyan.600`.toString()};
|
box-shadow: inset 0 -2px ${theme`colors.cyan.600`.toString()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +35,7 @@ const RightNavigation = styled.div`
|
||||||
export default () => {
|
export default () => {
|
||||||
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
|
||||||
const rootAdmin = useStoreState((state: ApplicationStore) => state.user.data!.rootAdmin);
|
const rootAdmin = useStoreState((state: ApplicationStore) => state.user.data!.rootAdmin);
|
||||||
const [ isLoggingOut, setIsLoggingOut ] = useState(false);
|
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||||
|
|
||||||
const onTriggerLogout = () => {
|
const onTriggerLogout = () => {
|
||||||
setIsLoggingOut(true);
|
setIsLoggingOut(true);
|
||||||
|
@ -42,30 +47,32 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'w-full bg-neutral-900 shadow-md overflow-x-auto'}>
|
<div className={'w-full bg-neutral-900 shadow-md overflow-x-auto'}>
|
||||||
<SpinnerOverlay visible={isLoggingOut}/>
|
<SpinnerOverlay visible={isLoggingOut} />
|
||||||
<div className={'mx-auto w-full flex items-center h-[3.5rem] max-w-[1200px]'}>
|
<div className={'mx-auto w-full flex items-center h-[3.5rem] max-w-[1200px]'}>
|
||||||
<div id={'logo'} className={'flex-1'}>
|
<div id={'logo'} className={'flex-1'}>
|
||||||
<Link
|
<Link
|
||||||
to={'/'}
|
to={'/'}
|
||||||
className={'text-2xl font-header px-4 no-underline text-neutral-200 hover:text-neutral-100 transition-colors duration-150'}
|
className={
|
||||||
|
'text-2xl font-header px-4 no-underline text-neutral-200 hover:text-neutral-100 transition-colors duration-150'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<RightNavigation className={'flex h-full items-center justify-center'}>
|
<RightNavigation className={'flex h-full items-center justify-center'}>
|
||||||
<SearchContainer/>
|
<SearchContainer />
|
||||||
<Tooltip placement={'bottom'} content={'Dashboard'}>
|
<Tooltip placement={'bottom'} content={'Dashboard'}>
|
||||||
<NavLink to={'/'} exact>
|
<NavLink to={'/'} exact>
|
||||||
<FontAwesomeIcon icon={faLayerGroup}/>
|
<FontAwesomeIcon icon={faLayerGroup} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{rootAdmin &&
|
{rootAdmin && (
|
||||||
<Tooltip placement={'bottom'} content={'Admin'}>
|
<Tooltip placement={'bottom'} content={'Admin'}>
|
||||||
<a href={'/admin'} rel={'noreferrer'}>
|
<a href={'/admin'} rel={'noreferrer'}>
|
||||||
<FontAwesomeIcon icon={faCogs}/>
|
<FontAwesomeIcon icon={faCogs} />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
)}
|
||||||
<Tooltip placement={'bottom'} content={'Account Settings'}>
|
<Tooltip placement={'bottom'} content={'Account Settings'}>
|
||||||
<NavLink to={'/account'}>
|
<NavLink to={'/account'}>
|
||||||
<span className={'flex items-center w-5 h-5'}>
|
<span className={'flex items-center w-5 h-5'}>
|
||||||
|
@ -75,7 +82,7 @@ export default () => {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement={'bottom'} content={'Sign Out'}>
|
<Tooltip placement={'bottom'} content={'Sign Out'}>
|
||||||
<button onClick={onTriggerLogout}>
|
<button onClick={onTriggerLogout}>
|
||||||
<FontAwesomeIcon icon={faSignOutAlt}/>
|
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</RightNavigation>
|
</RightNavigation>
|
||||||
|
|
|
@ -19,10 +19,10 @@ interface Values {
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const ref = useRef<Reaptcha>(null);
|
const ref = useRef<Reaptcha>(null);
|
||||||
const [ token, setToken ] = useState('');
|
const [token, setToken] = useState('');
|
||||||
|
|
||||||
const { clearFlashes, addFlash } = useFlash();
|
const { clearFlashes, addFlash } = useFlash();
|
||||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearFlashes();
|
clearFlashes();
|
||||||
|
@ -34,7 +34,7 @@ export default () => {
|
||||||
// If there is no token in the state yet, request the token and then abort this submit request
|
// If there is no token in the state yet, request the token and then abort this submit request
|
||||||
// since it will be re-submitted when the recaptcha data is returned by the component.
|
// since it will be re-submitted when the recaptcha data is returned by the component.
|
||||||
if (recaptchaEnabled && !token) {
|
if (recaptchaEnabled && !token) {
|
||||||
ref.current!.execute().catch(error => {
|
ref.current!.execute().catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
@ -45,11 +45,11 @@ export default () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPasswordResetEmail(email, token)
|
requestPasswordResetEmail(email, token)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
resetForm();
|
resetForm();
|
||||||
addFlash({ type: 'success', title: 'Success', message: response });
|
addFlash({ type: 'success', title: 'Success', message: response });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
addFlash({ type: 'error', title: 'Error', message: httpErrorToHuman(error) });
|
||||||
})
|
})
|
||||||
|
@ -66,47 +66,42 @@ export default () => {
|
||||||
onSubmit={handleSubmission}
|
onSubmit={handleSubmission}
|
||||||
initialValues={{ email: '' }}
|
initialValues={{ email: '' }}
|
||||||
validationSchema={object().shape({
|
validationSchema={object().shape({
|
||||||
email: string().email('A valid email address must be provided to continue.')
|
email: string()
|
||||||
|
.email('A valid email address must be provided to continue.')
|
||||||
.required('A valid email address must be provided to continue.'),
|
.required('A valid email address must be provided to continue.'),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||||
<LoginFormContainer
|
<LoginFormContainer title={'Request Password Reset'} css={tw`w-full flex`}>
|
||||||
title={'Request Password Reset'}
|
|
||||||
css={tw`w-full flex`}
|
|
||||||
>
|
|
||||||
<Field
|
<Field
|
||||||
light
|
light
|
||||||
label={'Email'}
|
label={'Email'}
|
||||||
description={'Enter your account email address to receive instructions on resetting your password.'}
|
description={
|
||||||
|
'Enter your account email address to receive instructions on resetting your password.'
|
||||||
|
}
|
||||||
name={'email'}
|
name={'email'}
|
||||||
type={'email'}
|
type={'email'}
|
||||||
/>
|
/>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Button
|
<Button type={'submit'} size={'xlarge'} disabled={isSubmitting} isLoading={isSubmitting}>
|
||||||
type={'submit'}
|
|
||||||
size={'xlarge'}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Send Email
|
Send Email
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{recaptchaEnabled &&
|
{recaptchaEnabled && (
|
||||||
<Reaptcha
|
<Reaptcha
|
||||||
ref={ref}
|
ref={ref}
|
||||||
size={'invisible'}
|
size={'invisible'}
|
||||||
sitekey={siteKey || '_invalid_key'}
|
sitekey={siteKey || '_invalid_key'}
|
||||||
onVerify={response => {
|
onVerify={(response) => {
|
||||||
setToken(response);
|
setToken(response);
|
||||||
submitForm();
|
submitForm();
|
||||||
}}
|
}}
|
||||||
onExpire={() => {
|
onExpire={() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setToken('');
|
setToken('');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<div css={tw`mt-6 text-center`}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/login'}
|
to={'/auth/login'}
|
||||||
|
|
|
@ -13,18 +13,18 @@ import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
code: string;
|
code: string;
|
||||||
recoveryCode: '',
|
recoveryCode: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }>
|
type OwnProps = RouteComponentProps<Record<string, string | undefined>, StaticContext, { token?: string }>;
|
||||||
|
|
||||||
type Props = OwnProps & {
|
type Props = OwnProps & {
|
||||||
clearAndAddHttpError: ActionCreator<FlashStore['clearAndAddHttpError']['payload']>;
|
clearAndAddHttpError: ActionCreator<FlashStore['clearAndAddHttpError']['payload']>;
|
||||||
}
|
};
|
||||||
|
|
||||||
const LoginCheckpointContainer = () => {
|
const LoginCheckpointContainer = () => {
|
||||||
const { isSubmitting, setFieldValue } = useFormikContext<Values>();
|
const { isSubmitting, setFieldValue } = useFormikContext<Values>();
|
||||||
const [ isMissingDevice, setIsMissingDevice ] = useState(false);
|
const [isMissingDevice, setIsMissingDevice] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginFormContainer title={'Device Checkpoint'} css={tw`w-full flex`}>
|
<LoginFormContainer title={'Device Checkpoint'} css={tw`w-full flex`}>
|
||||||
|
@ -44,12 +44,7 @@ const LoginCheckpointContainer = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Button
|
<Button size={'xlarge'} type={'submit'} disabled={isSubmitting} isLoading={isSubmitting}>
|
||||||
size={'xlarge'}
|
|
||||||
type={'submit'}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,11 +53,11 @@ const LoginCheckpointContainer = () => {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFieldValue('code', '');
|
setFieldValue('code', '');
|
||||||
setFieldValue('recoveryCode', '');
|
setFieldValue('recoveryCode', '');
|
||||||
setIsMissingDevice(s => !s);
|
setIsMissingDevice((s) => !s);
|
||||||
}}
|
}}
|
||||||
css={tw`cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
css={tw`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'}
|
{!isMissingDevice ? "I've Lost My Device" : 'I Have My Device'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6 text-center`}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
|
@ -80,7 +75,7 @@ const LoginCheckpointContainer = () => {
|
||||||
const EnhancedForm = withFormik<Props, Values>({
|
const EnhancedForm = withFormik<Props, Values>({
|
||||||
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { clearAndAddHttpError, location } }) => {
|
handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { clearAndAddHttpError, location } }) => {
|
||||||
loginCheckpoint(location.state?.token || '', code, recoveryCode)
|
loginCheckpoint(location.state?.token || '', code, recoveryCode)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.complete) {
|
if (response.complete) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.location = response.intended || '/';
|
window.location = response.intended || '/';
|
||||||
|
@ -89,7 +84,7 @@ const EnhancedForm = withFormik<Props, Values>({
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
clearAndAddHttpError({ error });
|
clearAndAddHttpError({ error });
|
||||||
|
@ -111,10 +106,7 @@ export default ({ history, location, ...props }: OwnProps) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <EnhancedForm
|
return (
|
||||||
clearAndAddHttpError={clearAndAddHttpError}
|
<EnhancedForm clearAndAddHttpError={clearAndAddHttpError} history={history} location={location} {...props} />
|
||||||
history={history}
|
);
|
||||||
location={location}
|
|
||||||
{...props}
|
|
||||||
/>;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,10 +18,10 @@ interface Values {
|
||||||
|
|
||||||
const LoginContainer = ({ history }: RouteComponentProps) => {
|
const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
const ref = useRef<Reaptcha>(null);
|
const ref = useRef<Reaptcha>(null);
|
||||||
const [ token, setToken ] = useState('');
|
const [token, setToken] = useState('');
|
||||||
|
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
const { enabled: recaptchaEnabled, siteKey } = useStoreState(state => state.settings.data!.recaptcha);
|
const { enabled: recaptchaEnabled, siteKey } = useStoreState((state) => state.settings.data!.recaptcha);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearFlashes();
|
clearFlashes();
|
||||||
|
@ -33,7 +33,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
// If there is no token in the state yet, request the token and then abort this submit request
|
// If there is no token in the state yet, request the token and then abort this submit request
|
||||||
// since it will be re-submitted when the recaptcha data is returned by the component.
|
// since it will be re-submitted when the recaptcha data is returned by the component.
|
||||||
if (recaptchaEnabled && !token) {
|
if (recaptchaEnabled && !token) {
|
||||||
ref.current!.execute().catch(error => {
|
ref.current!.execute().catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
@ -44,7 +44,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
login({ ...values, recaptchaData: token })
|
login({ ...values, recaptchaData: token })
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
if (response.complete) {
|
if (response.complete) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.location = response.intended || '/';
|
window.location = response.intended || '/';
|
||||||
|
@ -53,7 +53,7 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
|
|
||||||
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
setToken('');
|
setToken('');
|
||||||
|
@ -75,42 +75,30 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
>
|
>
|
||||||
{({ isSubmitting, setSubmitting, submitForm }) => (
|
{({ isSubmitting, setSubmitting, submitForm }) => (
|
||||||
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
|
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`}>
|
||||||
<Field
|
<Field light type={'text'} label={'Username or Email'} name={'username'} disabled={isSubmitting} />
|
||||||
light
|
|
||||||
type={'text'}
|
|
||||||
label={'Username or Email'}
|
|
||||||
name={'username'}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field light type={'password'} label={'Password'} name={'password'} disabled={isSubmitting} />
|
||||||
light
|
|
||||||
type={'password'}
|
|
||||||
label={'Password'}
|
|
||||||
name={'password'}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting} disabled={isSubmitting}>
|
<Button type={'submit'} size={'xlarge'} isLoading={isSubmitting} disabled={isSubmitting}>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{recaptchaEnabled &&
|
{recaptchaEnabled && (
|
||||||
<Reaptcha
|
<Reaptcha
|
||||||
ref={ref}
|
ref={ref}
|
||||||
size={'invisible'}
|
size={'invisible'}
|
||||||
sitekey={siteKey || '_invalid_key'}
|
sitekey={siteKey || '_invalid_key'}
|
||||||
onVerify={response => {
|
onVerify={(response) => {
|
||||||
setToken(response);
|
setToken(response);
|
||||||
submitForm();
|
submitForm();
|
||||||
}}
|
}}
|
||||||
onExpire={() => {
|
onExpire={() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setToken('');
|
setToken('');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<div css={tw`mt-6 text-center`}>
|
<div css={tw`mt-6 text-center`}>
|
||||||
<Link
|
<Link
|
||||||
to={'/auth/password'}
|
to={'/auth/password'}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import tw from 'twin.macro';
|
||||||
|
|
||||||
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
${breakpoint('sm')`
|
${breakpoint('sm')`
|
||||||
|
@ -30,24 +30,18 @@ const Container = styled.div`
|
||||||
|
|
||||||
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
||||||
<Container>
|
<Container>
|
||||||
{title &&
|
{title && <h2 css={tw`text-3xl text-center text-neutral-100 font-medium py-4`}>{title}</h2>}
|
||||||
<h2 css={tw`text-3xl text-center text-neutral-100 font-medium py-4`}>
|
<FlashMessageRender css={tw`mb-2 px-1`} />
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
}
|
|
||||||
<FlashMessageRender css={tw`mb-2 px-1`}/>
|
|
||||||
<Form {...props} ref={ref}>
|
<Form {...props} ref={ref}>
|
||||||
<div css={tw`md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1`}>
|
<div css={tw`md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1`}>
|
||||||
<div css={tw`flex-none select-none mb-6 md:mb-0 self-center`}>
|
<div css={tw`flex-none select-none mb-6 md:mb-0 self-center`}>
|
||||||
<img src={'/assets/svgs/pterodactyl.svg'} css={tw`block w-48 md:w-64 mx-auto`}/>
|
<img src={'/assets/svgs/pterodactyl.svg'} css={tw`block w-48 md:w-64 mx-auto`} />
|
||||||
</div>
|
|
||||||
<div css={tw`flex-1`}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div css={tw`flex-1`}>{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
<p css={tw`text-center text-neutral-500 text-xs mt-4`}>
|
<p css={tw`text-center text-neutral-500 text-xs mt-4`}>
|
||||||
© 2015 - {(new Date()).getFullYear()}
|
© 2015 - {new Date().getFullYear()}
|
||||||
<a
|
<a
|
||||||
rel={'noopener nofollow noreferrer'}
|
rel={'noopener nofollow noreferrer'}
|
||||||
href={'https://pterodactyl.io'}
|
href={'https://pterodactyl.io'}
|
||||||
|
|
|
@ -20,7 +20,7 @@ interface Values {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ match, location }: RouteComponentProps<{ token: string }>) => {
|
export default ({ match, location }: RouteComponentProps<{ token: string }>) => {
|
||||||
const [ email, setEmail ] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { clearFlashes, addFlash } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ export default ({ match, location }: RouteComponentProps<{ token: string }>) =>
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.location = '/';
|
window.location = '/';
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
@ -52,22 +52,20 @@ export default ({ match, location }: RouteComponentProps<{ token: string }>) =>
|
||||||
passwordConfirmation: '',
|
passwordConfirmation: '',
|
||||||
}}
|
}}
|
||||||
validationSchema={object().shape({
|
validationSchema={object().shape({
|
||||||
password: string().required('A new password is required.')
|
password: string()
|
||||||
|
.required('A new password is required.')
|
||||||
.min(8, 'Your new password should be at least 8 characters in length.'),
|
.min(8, 'Your new password should be at least 8 characters in length.'),
|
||||||
passwordConfirmation: string()
|
passwordConfirmation: string()
|
||||||
.required('Your new password does not match.')
|
.required('Your new password does not match.')
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
.oneOf([ ref('password'), null ], 'Your new password does not match.'),
|
.oneOf([ref('password'), null], 'Your new password does not match.'),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<LoginFormContainer
|
<LoginFormContainer title={'Reset Password'} css={tw`w-full flex`}>
|
||||||
title={'Reset Password'}
|
|
||||||
css={tw`w-full flex`}
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<label>Email</label>
|
<label>Email</label>
|
||||||
<Input value={email} isLight disabled/>
|
<Input value={email} isLight disabled />
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field
|
||||||
|
@ -79,20 +77,10 @@ export default ({ match, location }: RouteComponentProps<{ token: string }>) =>
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field light label={'Confirm New Password'} name={'passwordConfirmation'} type={'password'} />
|
||||||
light
|
|
||||||
label={'Confirm New Password'}
|
|
||||||
name={'passwordConfirmation'}
|
|
||||||
type={'password'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Button
|
<Button size={'xlarge'} type={'submit'} disabled={isSubmitting} isLoading={isSubmitting}>
|
||||||
size={'xlarge'}
|
|
||||||
type={'submit'}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
>
|
|
||||||
Reset Password
|
Reset Password
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,16 +16,16 @@ import { useFlashKey } from '@/plugins/useFlash';
|
||||||
import Code from '@/components/elements/Code';
|
import Code from '@/components/elements/Code';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
|
const [deleteIdentifier, setDeleteIdentifier] = useState('');
|
||||||
const [ keys, setKeys ] = useState<ApiKey[]>([]);
|
const [keys, setKeys] = useState<ApiKey[]>([]);
|
||||||
const [ loading, setLoading ] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { clearAndAddHttpError } = useFlashKey('account');
|
const { clearAndAddHttpError } = useFlashKey('account');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getApiKeys()
|
getApiKeys()
|
||||||
.then(keys => setKeys(keys))
|
.then((keys) => setKeys(keys))
|
||||||
.then(() => setLoading(false))
|
.then(() => setLoading(false))
|
||||||
.catch(error => clearAndAddHttpError(error));
|
.catch((error) => clearAndAddHttpError(error));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const doDeletion = (identifier: string) => {
|
const doDeletion = (identifier: string) => {
|
||||||
|
@ -33,10 +33,8 @@ export default () => {
|
||||||
|
|
||||||
clearAndAddHttpError();
|
clearAndAddHttpError();
|
||||||
deleteApiKey(identifier)
|
deleteApiKey(identifier)
|
||||||
.then(() => setKeys(s => ([
|
.then(() => setKeys((s) => [...(s || []).filter((key) => key.identifier !== identifier)]))
|
||||||
...(s || []).filter(key => key.identifier !== identifier),
|
.catch((error) => clearAndAddHttpError(error))
|
||||||
])))
|
|
||||||
.catch(error => clearAndAddHttpError(error))
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setDeleteIdentifier('');
|
setDeleteIdentifier('');
|
||||||
|
@ -45,13 +43,13 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={'Account API'}>
|
<PageContentBlock title={'Account API'}>
|
||||||
<FlashMessageRender byKey={'account'}/>
|
<FlashMessageRender byKey={'account'} />
|
||||||
<div css={tw`md:flex flex-nowrap my-10`}>
|
<div css={tw`md:flex flex-nowrap my-10`}>
|
||||||
<ContentBox title={'Create API Key'} css={tw`flex-none w-full md:w-1/2`}>
|
<ContentBox title={'Create API Key'} css={tw`flex-none w-full md:w-1/2`}>
|
||||||
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
|
<CreateApiKeyForm onKeyCreated={(key) => setKeys((s) => [...s!, key])} />
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
<ContentBox title={'API Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
|
<ContentBox title={'API Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
|
||||||
<SpinnerOverlay visible={loading}/>
|
<SpinnerOverlay visible={loading} />
|
||||||
<Dialog.Confirm
|
<Dialog.Confirm
|
||||||
title={'Delete API Key'}
|
title={'Delete API Key'}
|
||||||
confirm={'Delete Key'}
|
confirm={'Delete Key'}
|
||||||
|
@ -61,42 +59,36 @@ export default () => {
|
||||||
>
|
>
|
||||||
All requests using the <Code>{deleteIdentifier}</Code> key will be invalidated.
|
All requests using the <Code>{deleteIdentifier}</Code> key will be invalidated.
|
||||||
</Dialog.Confirm>
|
</Dialog.Confirm>
|
||||||
{
|
{keys.length === 0 ? (
|
||||||
keys.length === 0 ?
|
<p css={tw`text-center text-sm`}>
|
||||||
<p css={tw`text-center text-sm`}>
|
{loading ? 'Loading...' : 'No API keys exist for this account.'}
|
||||||
{loading ? 'Loading...' : 'No API keys exist for this account.'}
|
</p>
|
||||||
</p>
|
) : (
|
||||||
:
|
keys.map((key, index) => (
|
||||||
keys.map((key, index) => (
|
<GreyRowBox
|
||||||
<GreyRowBox
|
key={key.identifier}
|
||||||
key={key.identifier}
|
css={[tw`bg-neutral-600 flex items-center`, index > 0 && tw`mt-2`]}
|
||||||
css={[ tw`bg-neutral-600 flex items-center`, index > 0 && tw`mt-2` ]}
|
>
|
||||||
>
|
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`} />
|
||||||
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`}/>
|
<div css={tw`ml-4 flex-1 overflow-hidden`}>
|
||||||
<div css={tw`ml-4 flex-1 overflow-hidden`}>
|
<p css={tw`text-sm break-words`}>{key.description}</p>
|
||||||
<p css={tw`text-sm break-words`}>{key.description}</p>
|
<p css={tw`text-2xs text-neutral-300 uppercase`}>
|
||||||
<p css={tw`text-2xs text-neutral-300 uppercase`}>
|
Last used:
|
||||||
Last used:
|
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
|
||||||
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p css={tw`text-sm ml-4 hidden md:block`}>
|
|
||||||
<code css={tw`font-mono py-1 px-2 bg-neutral-900 rounded`}>
|
|
||||||
{key.identifier}
|
|
||||||
</code>
|
|
||||||
</p>
|
</p>
|
||||||
<button
|
</div>
|
||||||
css={tw`ml-4 p-2 text-sm`}
|
<p css={tw`text-sm ml-4 hidden md:block`}>
|
||||||
onClick={() => setDeleteIdentifier(key.identifier)}
|
<code css={tw`font-mono py-1 px-2 bg-neutral-900 rounded`}>{key.identifier}</code>
|
||||||
>
|
</p>
|
||||||
<FontAwesomeIcon
|
<button css={tw`ml-4 p-2 text-sm`} onClick={() => setDeleteIdentifier(key.identifier)}>
|
||||||
icon={faTrashAlt}
|
<FontAwesomeIcon
|
||||||
css={tw`text-neutral-400 hover:text-red-400 transition-colors duration-150`}
|
icon={faTrashAlt}
|
||||||
/>
|
css={tw`text-neutral-400 hover:text-red-400 transition-colors duration-150`}
|
||||||
</button>
|
/>
|
||||||
</GreyRowBox>
|
</button>
|
||||||
))
|
</GreyRowBox>
|
||||||
}
|
))
|
||||||
|
)}
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
</div>
|
</div>
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
|
|
|
@ -11,19 +11,19 @@ import MessageBox from '@/components/MessageBox';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
${tw`flex flex-wrap`};
|
${tw`flex flex-wrap`};
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
${tw`w-full`};
|
${tw`w-full`};
|
||||||
|
|
||||||
${breakpoint('sm')`
|
${breakpoint('sm')`
|
||||||
width: calc(50% - 1rem);
|
width: calc(50% - 1rem);
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${breakpoint('md')`
|
${breakpoint('md')`
|
||||||
${tw`w-auto flex-1`};
|
${tw`w-auto flex-1`};
|
||||||
`}
|
`}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
@ -31,28 +31,23 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={'Account Overview'}>
|
<PageContentBlock title={'Account Overview'}>
|
||||||
{state?.twoFactorRedirect &&
|
{state?.twoFactorRedirect && (
|
||||||
<MessageBox title={'2-Factor Required'} type={'error'}>
|
<MessageBox title={'2-Factor Required'} type={'error'}>
|
||||||
Your account must have two-factor authentication enabled in order to continue.
|
Your account must have two-factor authentication enabled in order to continue.
|
||||||
</MessageBox>
|
</MessageBox>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<Container css={[ tw`lg:grid lg:grid-cols-3 mb-10`, state?.twoFactorRedirect ? tw`mt-4` : tw`mt-10` ]}>
|
<Container css={[tw`lg:grid lg:grid-cols-3 mb-10`, state?.twoFactorRedirect ? tw`mt-4` : tw`mt-10`]}>
|
||||||
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
|
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
|
||||||
<UpdatePasswordForm/>
|
<UpdatePasswordForm />
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
<ContentBox
|
<ContentBox css={tw`mt-8 sm:mt-0 sm:ml-8`} title={'Update Email Address'} showFlashes={'account:email'}>
|
||||||
css={tw`mt-8 sm:mt-0 sm:ml-8`}
|
<UpdateEmailAddressForm />
|
||||||
title={'Update Email Address'}
|
|
||||||
showFlashes={'account:email'}
|
|
||||||
>
|
|
||||||
<UpdateEmailAddressForm/>
|
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
<ContentBox css={tw`md:ml-8 mt-8 md:mt-0`} title={'Configure Two Factor'}>
|
<ContentBox css={tw`md:ml-8 mt-8 md:mt-0`} title={'Configure Two Factor'}>
|
||||||
<ConfigureTwoFactorForm/>
|
<ConfigureTwoFactorForm />
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,9 @@ const ApiKeyModal = ({ apiKey }: Props) => {
|
||||||
shown again.
|
shown again.
|
||||||
</p>
|
</p>
|
||||||
<pre css={tw`text-sm bg-neutral-900 rounded py-2 px-4 font-mono`}>
|
<pre css={tw`text-sm bg-neutral-900 rounded py-2 px-4 font-mono`}>
|
||||||
<CopyOnClick text={apiKey}><code css={tw`font-mono`}>{apiKey}</code></CopyOnClick>
|
<CopyOnClick text={apiKey}>
|
||||||
|
<code css={tw`font-mono`}>{apiKey}</code>
|
||||||
|
</CopyOnClick>
|
||||||
</pre>
|
</pre>
|
||||||
<div css={tw`flex justify-end mt-6`}>
|
<div css={tw`flex justify-end mt-6`}>
|
||||||
<Button type={'button'} onClick={() => dismiss()}>
|
<Button type={'button'} onClick={() => dismiss()}>
|
||||||
|
|
|
@ -18,15 +18,15 @@ export default () => {
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const defaultPage = Number(new URLSearchParams(search).get('page') || '1');
|
const defaultPage = Number(new URLSearchParams(search).get('page') || '1');
|
||||||
|
|
||||||
const [ page, setPage ] = useState((!isNaN(defaultPage) && defaultPage > 0) ? defaultPage : 1);
|
const [page, setPage] = useState(!isNaN(defaultPage) && defaultPage > 0 ? defaultPage : 1);
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
const uuid = useStoreState(state => state.user.data!.uuid);
|
const uuid = useStoreState((state) => state.user.data!.uuid);
|
||||||
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
const rootAdmin = useStoreState((state) => state.user.data!.rootAdmin);
|
||||||
const [ showOnlyAdmin, setShowOnlyAdmin ] = usePersistedState(`${uuid}:show_all_servers`, false);
|
const [showOnlyAdmin, setShowOnlyAdmin] = usePersistedState(`${uuid}:show_all_servers`, false);
|
||||||
|
|
||||||
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
|
const { data: servers, error } = useSWR<PaginatedResult<Server>>(
|
||||||
[ '/api/client/servers', (showOnlyAdmin && rootAdmin), page ],
|
['/api/client/servers', showOnlyAdmin && rootAdmin, page],
|
||||||
() => getServers({ page, type: (showOnlyAdmin && rootAdmin) ? 'admin' : undefined }),
|
() => getServers({ page, type: showOnlyAdmin && rootAdmin ? 'admin' : undefined })
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -34,58 +34,53 @@ export default () => {
|
||||||
if (servers.pagination.currentPage > 1 && !servers.items.length) {
|
if (servers.pagination.currentPage > 1 && !servers.items.length) {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}
|
}
|
||||||
}, [ servers?.pagination.currentPage ]);
|
}, [servers?.pagination.currentPage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Don't use react-router to handle changing this part of the URL, otherwise it
|
// Don't use react-router to handle changing this part of the URL, otherwise it
|
||||||
// triggers a needless re-render. We just want to track this in the URL incase the
|
// triggers a needless re-render. We just want to track this in the URL incase the
|
||||||
// user refreshes the page.
|
// user refreshes the page.
|
||||||
window.history.replaceState(null, document.title, `/${page <= 1 ? '' : `?page=${page}`}`);
|
window.history.replaceState(null, document.title, `/${page <= 1 ? '' : `?page=${page}`}`);
|
||||||
}, [ page ]);
|
}, [page]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) clearAndAddHttpError({ key: 'dashboard', error });
|
if (error) clearAndAddHttpError({ key: 'dashboard', error });
|
||||||
if (!error) clearFlashes('dashboard');
|
if (!error) clearFlashes('dashboard');
|
||||||
}, [ error ]);
|
}, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={'Dashboard'} showFlashKey={'dashboard'}>
|
<PageContentBlock title={'Dashboard'} showFlashKey={'dashboard'}>
|
||||||
{rootAdmin &&
|
{rootAdmin && (
|
||||||
<div css={tw`mb-2 flex justify-end items-center`}>
|
<div css={tw`mb-2 flex justify-end items-center`}>
|
||||||
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
|
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
|
||||||
{showOnlyAdmin ? 'Showing others\' servers' : 'Showing your servers'}
|
{showOnlyAdmin ? "Showing others' servers" : 'Showing your servers'}
|
||||||
</p>
|
</p>
|
||||||
<Switch
|
<Switch
|
||||||
name={'show_all_servers'}
|
name={'show_all_servers'}
|
||||||
defaultChecked={showOnlyAdmin}
|
defaultChecked={showOnlyAdmin}
|
||||||
onChange={() => setShowOnlyAdmin(s => !s)}
|
onChange={() => setShowOnlyAdmin((s) => !s)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{!servers ?
|
{!servers ? (
|
||||||
<Spinner centered size={'large'}/>
|
<Spinner centered size={'large'} />
|
||||||
:
|
) : (
|
||||||
<Pagination data={servers} onPageSelect={setPage}>
|
<Pagination data={servers} onPageSelect={setPage}>
|
||||||
{({ items }) => (
|
{({ items }) =>
|
||||||
items.length > 0 ?
|
items.length > 0 ? (
|
||||||
items.map((server, index) => (
|
items.map((server, index) => (
|
||||||
<ServerRow
|
<ServerRow key={server.uuid} server={server} css={index > 0 ? tw`mt-2` : undefined} />
|
||||||
key={server.uuid}
|
|
||||||
server={server}
|
|
||||||
css={index > 0 ? tw`mt-2` : undefined}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
:
|
) : (
|
||||||
<p css={tw`text-center text-sm text-neutral-400`}>
|
<p css={tw`text-center text-sm text-neutral-400`}>
|
||||||
{showOnlyAdmin ?
|
{showOnlyAdmin
|
||||||
'There are no other servers to display.'
|
? 'There are no other servers to display.'
|
||||||
:
|
: 'There are no servers associated with your account.'}
|
||||||
'There are no servers associated with your account.'
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</Pagination>
|
</Pagination>
|
||||||
}
|
)}
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,15 +13,18 @@ import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
// Determines if the current value is in an alarm threshold so we can show it in red rather
|
// Determines if the current value is in an alarm threshold so we can show it in red rather
|
||||||
// than the more faded default style.
|
// than the more faded default style.
|
||||||
const isAlarmState = (current: number, limit: number): boolean => limit > 0 && (current / (limit * 1024 * 1024) >= 0.90);
|
const isAlarmState = (current: number, limit: number): boolean => limit > 0 && current / (limit * 1024 * 1024) >= 0.9;
|
||||||
|
|
||||||
const Icon = memo(styled(FontAwesomeIcon)<{ $alarm: boolean }>`
|
const Icon = memo(
|
||||||
${props => props.$alarm ? tw`text-red-400` : tw`text-neutral-500`};
|
styled(FontAwesomeIcon)<{ $alarm: boolean }>`
|
||||||
`, isEqual);
|
${(props) => (props.$alarm ? tw`text-red-400` : tw`text-neutral-500`)};
|
||||||
|
`,
|
||||||
|
isEqual
|
||||||
|
);
|
||||||
|
|
||||||
const IconDescription = styled.p<{ $alarm: boolean }>`
|
const IconDescription = styled.p<{ $alarm: boolean }>`
|
||||||
${tw`text-sm ml-2`};
|
${tw`text-sm ml-2`};
|
||||||
${props => props.$alarm ? tw`text-white` : tw`text-neutral-400`};
|
${(props) => (props.$alarm ? tw`text-white` : tw`text-neutral-400`)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`
|
const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`
|
||||||
|
@ -31,7 +34,12 @@ const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | unde
|
||||||
${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`};
|
${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`};
|
||||||
height: calc(100% - 0.5rem);
|
height: calc(100% - 0.5rem);
|
||||||
|
|
||||||
${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)};
|
${({ $status }) =>
|
||||||
|
!$status || $status === 'offline'
|
||||||
|
? tw`bg-red-500`
|
||||||
|
: $status === 'running'
|
||||||
|
? tw`bg-green-500`
|
||||||
|
: tw`bg-yellow-500`};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .status-bar {
|
&:hover .status-bar {
|
||||||
|
@ -41,16 +49,17 @@ const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | unde
|
||||||
|
|
||||||
export default ({ server, className }: { server: Server; className?: string }) => {
|
export default ({ server, className }: { server: Server; className?: string }) => {
|
||||||
const interval = useRef<number>(null);
|
const interval = useRef<number>(null);
|
||||||
const [ isSuspended, setIsSuspended ] = useState(server.status === 'suspended');
|
const [isSuspended, setIsSuspended] = useState(server.status === 'suspended');
|
||||||
const [ stats, setStats ] = useState<ServerStats | null>(null);
|
const [stats, setStats] = useState<ServerStats | null>(null);
|
||||||
|
|
||||||
const getStats = () => getServerResourceUsage(server.uuid)
|
const getStats = () =>
|
||||||
.then(data => setStats(data))
|
getServerResourceUsage(server.uuid)
|
||||||
.catch(error => console.error(error));
|
.then((data) => setStats(data))
|
||||||
|
.catch((error) => console.error(error));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsSuspended(stats?.isSuspended || server.status === 'suspended');
|
setIsSuspended(stats?.isSuspended || server.status === 'suspended');
|
||||||
}, [ stats?.isSuspended, server.status ]);
|
}, [stats?.isSuspended, server.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Don't waste a HTTP request if there is nothing important to show to the user because
|
// Don't waste a HTTP request if there is nothing important to show to the user because
|
||||||
|
@ -65,11 +74,11 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||||
return () => {
|
return () => {
|
||||||
interval.current && clearInterval(interval.current);
|
interval.current && clearInterval(interval.current);
|
||||||
};
|
};
|
||||||
}, [ isSuspended ]);
|
}, [isSuspended]);
|
||||||
|
|
||||||
const alarms = { cpu: false, memory: false, disk: false };
|
const alarms = { cpu: false, memory: false, disk: false };
|
||||||
if (stats) {
|
if (stats) {
|
||||||
alarms.cpu = server.limits.cpu === 0 ? false : (stats.cpuUsagePercent >= (server.limits.cpu * 0.9));
|
alarms.cpu = server.limits.cpu === 0 ? false : stats.cpuUsagePercent >= server.limits.cpu * 0.9;
|
||||||
alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
|
alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
|
||||||
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
|
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
|
||||||
}
|
}
|
||||||
|
@ -82,60 +91,57 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||||
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
|
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
|
||||||
<div css={tw`flex items-center col-span-12 sm:col-span-5 lg:col-span-6`}>
|
<div css={tw`flex items-center col-span-12 sm:col-span-5 lg:col-span-6`}>
|
||||||
<div className={'icon'} css={tw`mr-4`}>
|
<div className={'icon'} css={tw`mr-4`}>
|
||||||
<FontAwesomeIcon icon={faServer}/>
|
<FontAwesomeIcon icon={faServer} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p css={tw`text-lg break-words`}>{server.name}</p>
|
<p css={tw`text-lg break-words`}>{server.name}</p>
|
||||||
{!!server.description &&
|
{!!server.description && (
|
||||||
<p css={tw`text-sm text-neutral-300 break-words line-clamp-2`}>{server.description}</p>
|
<p css={tw`text-sm text-neutral-300 break-words line-clamp-2`}>{server.description}</p>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex-1 ml-4 lg:block lg:col-span-2 hidden`}>
|
<div css={tw`flex-1 ml-4 lg:block lg:col-span-2 hidden`}>
|
||||||
<div css={tw`flex justify-center`}>
|
<div css={tw`flex justify-center`}>
|
||||||
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
|
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`} />
|
||||||
<p css={tw`text-sm text-neutral-400 ml-2`}>
|
<p css={tw`text-sm text-neutral-400 ml-2`}>
|
||||||
{
|
{server.allocations
|
||||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
.filter((alloc) => alloc.isDefault)
|
||||||
|
.map((allocation) => (
|
||||||
<React.Fragment key={allocation.ip + allocation.port.toString()}>
|
<React.Fragment key={allocation.ip + allocation.port.toString()}>
|
||||||
{allocation.alias || ip(allocation.ip)}:{allocation.port}
|
{allocation.alias || ip(allocation.ip)}:{allocation.port}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))}
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>
|
<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>
|
||||||
{(!stats || isSuspended) ?
|
{!stats || isSuspended ? (
|
||||||
isSuspended ?
|
isSuspended ? (
|
||||||
<div css={tw`flex-1 text-center`}>
|
<div css={tw`flex-1 text-center`}>
|
||||||
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
|
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
|
||||||
{server.status === 'suspended' ? 'Suspended' : 'Connection Error'}
|
{server.status === 'suspended' ? 'Suspended' : 'Connection Error'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
:
|
) : server.isTransferring || server.status ? (
|
||||||
(server.isTransferring || server.status) ?
|
<div css={tw`flex-1 text-center`}>
|
||||||
<div css={tw`flex-1 text-center`}>
|
<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
|
||||||
<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
|
{server.isTransferring
|
||||||
{server.isTransferring ?
|
? 'Transferring'
|
||||||
'Transferring'
|
: server.status === 'installing'
|
||||||
:
|
? 'Installing'
|
||||||
server.status === 'installing' ? 'Installing' : (
|
: server.status === 'restoring_backup'
|
||||||
server.status === 'restoring_backup' ?
|
? 'Restoring Backup'
|
||||||
'Restoring Backup'
|
: 'Unavailable'}
|
||||||
:
|
</span>
|
||||||
'Unavailable'
|
</div>
|
||||||
)
|
) : (
|
||||||
}
|
<Spinner size={'small'} />
|
||||||
</span>
|
)
|
||||||
</div>
|
) : (
|
||||||
:
|
|
||||||
<Spinner size={'small'}/>
|
|
||||||
:
|
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||||
<div css={tw`flex justify-center`}>
|
<div css={tw`flex justify-center`}>
|
||||||
<Icon icon={faMicrochip} $alarm={alarms.cpu}/>
|
<Icon icon={faMicrochip} $alarm={alarms.cpu} />
|
||||||
<IconDescription $alarm={alarms.cpu}>
|
<IconDescription $alarm={alarms.cpu}>
|
||||||
{stats.cpuUsagePercent.toFixed(2)} %
|
{stats.cpuUsagePercent.toFixed(2)} %
|
||||||
</IconDescription>
|
</IconDescription>
|
||||||
|
@ -144,7 +150,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||||
<div css={tw`flex justify-center`}>
|
<div css={tw`flex justify-center`}>
|
||||||
<Icon icon={faMemory} $alarm={alarms.memory}/>
|
<Icon icon={faMemory} $alarm={alarms.memory} />
|
||||||
<IconDescription $alarm={alarms.memory}>
|
<IconDescription $alarm={alarms.memory}>
|
||||||
{bytesToString(stats.memoryUsageInBytes)}
|
{bytesToString(stats.memoryUsageInBytes)}
|
||||||
</IconDescription>
|
</IconDescription>
|
||||||
|
@ -153,7 +159,7 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||||
<div css={tw`flex justify-center`}>
|
<div css={tw`flex justify-center`}>
|
||||||
<Icon icon={faHdd} $alarm={alarms.disk}/>
|
<Icon icon={faHdd} $alarm={alarms.disk} />
|
||||||
<IconDescription $alarm={alarms.disk}>
|
<IconDescription $alarm={alarms.disk}>
|
||||||
{bytesToString(stats.diskUsageInBytes)}
|
{bytesToString(stats.diskUsageInBytes)}
|
||||||
</IconDescription>
|
</IconDescription>
|
||||||
|
@ -161,9 +167,9 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
||||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {diskLimit}</p>
|
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {diskLimit}</p>
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={'status-bar'}/>
|
<div className={'status-bar'} />
|
||||||
</StatusIndicatorBox>
|
</StatusIndicatorBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,53 +16,57 @@ import useLocationHash from '@/plugins/useLocationHash';
|
||||||
export default () => {
|
export default () => {
|
||||||
const { hash } = useLocationHash();
|
const { hash } = useLocationHash();
|
||||||
const { clearAndAddHttpError } = useFlashKey('account');
|
const { clearAndAddHttpError } = useFlashKey('account');
|
||||||
const [ filters, setFilters ] = useState<ActivityLogFilters>({ page: 1, sorts: { timestamp: -1 } });
|
const [filters, setFilters] = useState<ActivityLogFilters>({ page: 1, sorts: { timestamp: -1 } });
|
||||||
const { data, isValidating, error } = useActivityLogs(filters, {
|
const { data, isValidating, error } = useActivityLogs(filters, {
|
||||||
revalidateOnMount: true,
|
revalidateOnMount: true,
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilters(value => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
|
setFilters((value) => ({ ...value, filters: { ip: hash.ip, event: hash.event } }));
|
||||||
}, [ hash ]);
|
}, [hash]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearAndAddHttpError(error);
|
clearAndAddHttpError(error);
|
||||||
}, [ error ]);
|
}, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={'Account Activity Log'}>
|
<PageContentBlock title={'Account Activity Log'}>
|
||||||
<FlashMessageRender byKey={'account'}/>
|
<FlashMessageRender byKey={'account'} />
|
||||||
{(filters.filters?.event || filters.filters?.ip) &&
|
{(filters.filters?.event || filters.filters?.ip) && (
|
||||||
<div className={'flex justify-end mb-2'}>
|
<div className={'flex justify-end mb-2'}>
|
||||||
<Link
|
<Link
|
||||||
to={'#'}
|
to={'#'}
|
||||||
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
|
className={classNames(btnStyles.button, btnStyles.text, 'w-full sm:w-auto')}
|
||||||
onClick={() => setFilters(value => ({ ...value, filters: {} }))}
|
onClick={() => setFilters((value) => ({ ...value, filters: {} }))}
|
||||||
>
|
>
|
||||||
Clear Filters <XCircleIcon className={'w-4 h-4 ml-2'}/>
|
Clear Filters <XCircleIcon className={'w-4 h-4 ml-2'} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{!data && isValidating ?
|
{!data && isValidating ? (
|
||||||
<Spinner centered/>
|
<Spinner centered />
|
||||||
:
|
) : (
|
||||||
<div className={'bg-gray-700'}>
|
<div className={'bg-gray-700'}>
|
||||||
{data?.items.map((activity) => (
|
{data?.items.map((activity) => (
|
||||||
<ActivityLogEntry key={activity.timestamp.toString() + activity.event} activity={activity}>
|
<ActivityLogEntry key={activity.timestamp.toString() + activity.event} activity={activity}>
|
||||||
{typeof activity.properties.useragent === 'string' &&
|
{typeof activity.properties.useragent === 'string' && (
|
||||||
<Tooltip content={activity.properties.useragent} placement={'top'}>
|
<Tooltip content={activity.properties.useragent} placement={'top'}>
|
||||||
<span><DesktopComputerIcon/></span>
|
<span>
|
||||||
|
<DesktopComputerIcon />
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
)}
|
||||||
</ActivityLogEntry>
|
</ActivityLogEntry>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{data && <PaginationFooter
|
{data && (
|
||||||
pagination={data.pagination}
|
<PaginationFooter
|
||||||
onPageSelect={page => setFilters(value => ({ ...value, page }))}
|
pagination={data.pagination}
|
||||||
/>}
|
onPageSelect={(page) => setFilters((value) => ({ ...value, page }))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,23 +7,21 @@ import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp);
|
const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{visible && (
|
{visible &&
|
||||||
isEnabled ?
|
(isEnabled ? (
|
||||||
<DisableTwoFactorModal visible={visible} onModalDismissed={() => setVisible(false)}/>
|
<DisableTwoFactorModal visible={visible} onModalDismissed={() => setVisible(false)} />
|
||||||
:
|
) : (
|
||||||
<SetupTwoFactorModal visible={visible} onModalDismissed={() => setVisible(false)}/>
|
<SetupTwoFactorModal visible={visible} onModalDismissed={() => setVisible(false)} />
|
||||||
)}
|
))}
|
||||||
<p css={tw`text-sm`}>
|
<p css={tw`text-sm`}>
|
||||||
{isEnabled ?
|
{isEnabled
|
||||||
'Two-factor authentication is currently enabled on your account.'
|
? 'Two-factor authentication is currently enabled on your account.'
|
||||||
:
|
: 'You do not currently have two-factor authentication enabled on your account. Click the button below to begin configuring it.'}
|
||||||
'You do not currently have two-factor authentication enabled on your account. Click the button below to begin configuring it.'
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Button color={'red'} isSecondary onClick={() => setVisible(true)}>
|
<Button color={'red'} isSecondary onClick={() => setVisible(true)}>
|
||||||
|
|
|
@ -19,10 +19,12 @@ interface Values {
|
||||||
allowedIps: string;
|
allowedIps: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomTextarea = styled(Textarea)`${tw`h-32`}`;
|
const CustomTextarea = styled(Textarea)`
|
||||||
|
${tw`h-32`}
|
||||||
|
`;
|
||||||
|
|
||||||
export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||||
const [ apiKey, setApiKey ] = useState('');
|
const [apiKey, setApiKey] = useState('');
|
||||||
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
|
||||||
const submit = (values: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
const submit = (values: Values, { setSubmitting, resetForm }: FormikHelpers<Values>) => {
|
||||||
|
@ -34,7 +36,7 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||||
setApiKey(`${key.identifier}${secretToken}`);
|
setApiKey(`${key.identifier}${secretToken}`);
|
||||||
onKeyCreated(key);
|
onKeyCreated(key);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
addError({ key: 'account', message: httpErrorToHuman(error) });
|
addError({ key: 'account', message: httpErrorToHuman(error) });
|
||||||
|
@ -44,11 +46,7 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ApiKeyModal
|
<ApiKeyModal visible={apiKey.length > 0} onModalDismissed={() => setApiKey('')} apiKey={apiKey} />
|
||||||
visible={apiKey.length > 0}
|
|
||||||
onModalDismissed={() => setApiKey('')}
|
|
||||||
apiKey={apiKey}
|
|
||||||
/>
|
|
||||||
<Formik
|
<Formik
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
initialValues={{ description: '', allowedIps: '' }}
|
initialValues={{ description: '', allowedIps: '' }}
|
||||||
|
@ -59,21 +57,23 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<SpinnerOverlay visible={isSubmitting}/>
|
<SpinnerOverlay visible={isSubmitting} />
|
||||||
<FormikFieldWrapper
|
<FormikFieldWrapper
|
||||||
label={'Description'}
|
label={'Description'}
|
||||||
name={'description'}
|
name={'description'}
|
||||||
description={'A description of this API key.'}
|
description={'A description of this API key.'}
|
||||||
css={tw`mb-6`}
|
css={tw`mb-6`}
|
||||||
>
|
>
|
||||||
<Field name={'description'} as={Input}/>
|
<Field name={'description'} as={Input} />
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
<FormikFieldWrapper
|
<FormikFieldWrapper
|
||||||
label={'Allowed IPs'}
|
label={'Allowed IPs'}
|
||||||
name={'allowedIps'}
|
name={'allowedIps'}
|
||||||
description={'Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line.'}
|
description={
|
||||||
|
'Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line.'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Field name={'allowedIps'} as={CustomTextarea}/>
|
<Field name={'allowedIps'} as={CustomTextarea} />
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
<div css={tw`flex justify-end mt-6`}>
|
<div css={tw`flex justify-end mt-6`}>
|
||||||
<Button>Create</Button>
|
<Button>Create</Button>
|
||||||
|
|
|
@ -27,7 +27,7 @@ const DisableTwoFactorModal = () => {
|
||||||
updateUserData({ useTotp: false });
|
updateUserData({ useTotp: false });
|
||||||
dismiss();
|
dismiss();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
||||||
|
@ -48,13 +48,15 @@ const DisableTwoFactorModal = () => {
|
||||||
>
|
>
|
||||||
{({ isValid }) => (
|
{({ isValid }) => (
|
||||||
<Form className={'mb-0'}>
|
<Form className={'mb-0'}>
|
||||||
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'}/>
|
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'} />
|
||||||
<Field
|
<Field
|
||||||
id={'password'}
|
id={'password'}
|
||||||
name={'password'}
|
name={'password'}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
label={'Current Password'}
|
label={'Current Password'}
|
||||||
description={'In order to disable two-factor authentication you will need to provide your account password.'}
|
description={
|
||||||
|
'In order to disable two-factor authentication you will need to provide your account password.'
|
||||||
|
}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div css={tw`mt-6 text-right`}>
|
<div css={tw`mt-6 text-right`}>
|
||||||
|
|
|
@ -19,8 +19,8 @@ interface Values {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SetupTwoFactorModal = () => {
|
const SetupTwoFactorModal = () => {
|
||||||
const [ token, setToken ] = useState<TwoFactorTokenData | null>(null);
|
const [token, setToken] = useState<TwoFactorTokenData | null>(null);
|
||||||
const [ recoveryTokens, setRecoveryTokens ] = useState<string[]>([]);
|
const [recoveryTokens, setRecoveryTokens] = useState<string[]>([]);
|
||||||
|
|
||||||
const { dismiss, setPropOverrides } = useContext(ModalContext);
|
const { dismiss, setPropOverrides } = useContext(ModalContext);
|
||||||
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
|
const updateUserData = useStoreActions((actions: Actions<ApplicationStore>) => actions.user.updateUserData);
|
||||||
|
@ -29,31 +29,31 @@ const SetupTwoFactorModal = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getTwoFactorTokenData()
|
getTwoFactorTokenData()
|
||||||
.then(setToken)
|
.then(setToken)
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const submit = ({ code }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
setPropOverrides(state => ({ ...state, showSpinnerOverlay: true }));
|
setPropOverrides((state) => ({ ...state, showSpinnerOverlay: true }));
|
||||||
enableAccountTwoFactor(code)
|
enableAccountTwoFactor(code)
|
||||||
.then(tokens => {
|
.then((tokens) => {
|
||||||
setRecoveryTokens(tokens);
|
setRecoveryTokens(tokens);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
clearAndAddHttpError({ error, key: 'account:two-factor' });
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setPropOverrides(state => ({ ...state, showSpinnerOverlay: false }));
|
setPropOverrides((state) => ({ ...state, showSpinnerOverlay: false }));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPropOverrides(state => ({
|
setPropOverrides((state) => ({
|
||||||
...state,
|
...state,
|
||||||
closeOnEscape: !recoveryTokens.length,
|
closeOnEscape: !recoveryTokens.length,
|
||||||
closeOnBackground: !recoveryTokens.length,
|
closeOnBackground: !recoveryTokens.length,
|
||||||
|
@ -64,7 +64,7 @@ const SetupTwoFactorModal = () => {
|
||||||
updateUserData({ useTotp: true });
|
updateUserData({ useTotp: true });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [ recoveryTokens ]);
|
}, [recoveryTokens]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
|
@ -76,20 +76,24 @@ const SetupTwoFactorModal = () => {
|
||||||
.matches(/^(\d){6}$/, 'Authenticator code must be 6 digits.'),
|
.matches(/^(\d){6}$/, 'Authenticator code must be 6 digits.'),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{recoveryTokens.length > 0 ?
|
{recoveryTokens.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<h2 css={tw`text-2xl mb-4`}>Two-factor authentication enabled</h2>
|
<h2 css={tw`text-2xl mb-4`}>Two-factor authentication enabled</h2>
|
||||||
<p css={tw`text-neutral-300`}>
|
<p css={tw`text-neutral-300`}>
|
||||||
Two-factor authentication has been enabled on your account. Should you lose access to
|
Two-factor authentication has been enabled on your account. Should you lose access to your
|
||||||
your authenticator device, you'll need to use one of the codes displayed below in order to access your
|
authenticator device, you'll need to use one of the codes displayed below in order to
|
||||||
account.
|
access your account.
|
||||||
</p>
|
</p>
|
||||||
<p css={tw`text-neutral-300 mt-4`}>
|
<p css={tw`text-neutral-300 mt-4`}>
|
||||||
<strong>These codes will not be displayed again.</strong> Please take note of them now
|
<strong>These codes will not be displayed again.</strong> Please take note of them now by
|
||||||
by storing them in a secure repository such as a password manager.
|
storing them in a secure repository such as a password manager.
|
||||||
</p>
|
</p>
|
||||||
<pre css={tw`text-sm mt-4 rounded font-mono bg-neutral-900 p-4`}>
|
<pre css={tw`text-sm mt-4 rounded font-mono bg-neutral-900 p-4`}>
|
||||||
{recoveryTokens.map(token => <code key={token} css={tw`block mb-1`}>{token}</code>)}
|
{recoveryTokens.map((token) => (
|
||||||
|
<code key={token} css={tw`block mb-1`}>
|
||||||
|
{token}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
</pre>
|
</pre>
|
||||||
<div css={tw`text-right`}>
|
<div css={tw`text-right`}>
|
||||||
<Button css={tw`mt-6`} onClick={dismiss}>
|
<Button css={tw`mt-6`} onClick={dismiss}>
|
||||||
|
@ -97,24 +101,26 @@ const SetupTwoFactorModal = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
:
|
) : (
|
||||||
<Form css={tw`mb-0`}>
|
<Form css={tw`mb-0`}>
|
||||||
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'}/>
|
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'} />
|
||||||
<div css={tw`flex flex-wrap`}>
|
<div css={tw`flex flex-wrap`}>
|
||||||
<div css={tw`w-full md:flex-1`}>
|
<div css={tw`w-full md:flex-1`}>
|
||||||
<div css={tw`w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto`}>
|
<div css={tw`w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto`}>
|
||||||
{!token ?
|
{!token ? (
|
||||||
<img
|
<img
|
||||||
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
|
src={
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
||||||
|
}
|
||||||
css={tw`w-64 h-64 rounded`}
|
css={tw`w-64 h-64 rounded`}
|
||||||
/>
|
/>
|
||||||
:
|
) : (
|
||||||
<QRCode
|
<QRCode
|
||||||
renderAs={'svg'}
|
renderAs={'svg'}
|
||||||
value={token.image_url_data}
|
value={token.image_url_data}
|
||||||
css={tw`w-full h-full shadow-none rounded-none`}
|
css={tw`w-full h-full shadow-none rounded-none`}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col`}>
|
<div css={tw`w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col`}>
|
||||||
|
@ -124,20 +130,20 @@ const SetupTwoFactorModal = () => {
|
||||||
name={'code'}
|
name={'code'}
|
||||||
type={'text'}
|
type={'text'}
|
||||||
title={'Code From Authenticator'}
|
title={'Code From Authenticator'}
|
||||||
description={'Enter the code from your authenticator device after scanning the QR image.'}
|
description={
|
||||||
|
'Enter the code from your authenticator device after scanning the QR image.'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{token &&
|
{token && (
|
||||||
<div css={tw`mt-4 pt-4 border-t border-neutral-500 text-neutral-200`}>
|
<div css={tw`mt-4 pt-4 border-t border-neutral-500 text-neutral-200`}>
|
||||||
Alternatively, enter the following token into your authenticator application:
|
Alternatively, enter the following token into your authenticator application:
|
||||||
<CopyOnClick text={token.secret}>
|
<CopyOnClick text={token.secret}>
|
||||||
<div css={tw`text-sm bg-neutral-900 rounded mt-2 py-2 px-4 font-mono`}>
|
<div css={tw`text-sm bg-neutral-900 rounded mt-2 py-2 px-4 font-mono`}>
|
||||||
<code css={tw`font-mono`}>
|
<code css={tw`font-mono`}>{token.secret}</code>
|
||||||
{token.secret}
|
</div>
|
||||||
</code>
|
</CopyOnClick>
|
||||||
</div>
|
</div>
|
||||||
</CopyOnClick>
|
)}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6 md:mt-0 text-right`}>
|
<div css={tw`mt-6 md:mt-0 text-right`}>
|
||||||
<Button>Setup</Button>
|
<Button>Setup</Button>
|
||||||
|
@ -145,7 +151,7 @@ const SetupTwoFactorModal = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,17 +29,21 @@ export default () => {
|
||||||
clearFlashes('account:email');
|
clearFlashes('account:email');
|
||||||
|
|
||||||
updateEmail({ ...values })
|
updateEmail({ ...values })
|
||||||
.then(() => addFlash({
|
.then(() =>
|
||||||
type: 'success',
|
addFlash({
|
||||||
key: 'account:email',
|
type: 'success',
|
||||||
message: 'Your primary email has been updated.',
|
key: 'account:email',
|
||||||
}))
|
message: 'Your primary email has been updated.',
|
||||||
.catch(error => addFlash({
|
})
|
||||||
type: 'error',
|
)
|
||||||
key: 'account:email',
|
.catch((error) =>
|
||||||
title: 'Error',
|
addFlash({
|
||||||
message: httpErrorToHuman(error),
|
type: 'error',
|
||||||
}))
|
key: 'account:email',
|
||||||
|
title: 'Error',
|
||||||
|
message: httpErrorToHuman(error),
|
||||||
|
})
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
resetForm();
|
resetForm();
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
|
@ -47,39 +51,28 @@ export default () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik onSubmit={submit} validationSchema={schema} initialValues={{ email: user!.email, password: '' }}>
|
||||||
onSubmit={submit}
|
{({ isSubmitting, isValid }) => (
|
||||||
validationSchema={schema}
|
<React.Fragment>
|
||||||
initialValues={{ email: user!.email, password: '' }}
|
<SpinnerOverlay size={'large'} visible={isSubmitting} />
|
||||||
>
|
<Form css={tw`m-0`}>
|
||||||
{
|
<Field id={'current_email'} type={'email'} name={'email'} label={'Email'} />
|
||||||
({ isSubmitting, isValid }) => (
|
<div css={tw`mt-6`}>
|
||||||
<React.Fragment>
|
|
||||||
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
|
|
||||||
<Form css={tw`m-0`}>
|
|
||||||
<Field
|
<Field
|
||||||
id={'current_email'}
|
id={'confirm_password'}
|
||||||
type={'email'}
|
type={'password'}
|
||||||
name={'email'}
|
name={'password'}
|
||||||
label={'Email'}
|
label={'Confirm Password'}
|
||||||
/>
|
/>
|
||||||
<div css={tw`mt-6`}>
|
</div>
|
||||||
<Field
|
<div css={tw`mt-6`}>
|
||||||
id={'confirm_password'}
|
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
||||||
type={'password'}
|
Update Email
|
||||||
name={'password'}
|
</Button>
|
||||||
label={'Confirm Password'}
|
</div>
|
||||||
/>
|
</Form>
|
||||||
</div>
|
</React.Fragment>
|
||||||
<div css={tw`mt-6`}>
|
)}
|
||||||
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
|
||||||
Update Email
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,9 +19,13 @@ interface Values {
|
||||||
const schema = Yup.object().shape({
|
const schema = Yup.object().shape({
|
||||||
current: Yup.string().min(1).required('You must provide your current password.'),
|
current: Yup.string().min(1).required('You must provide your current password.'),
|
||||||
password: Yup.string().min(8).required(),
|
password: Yup.string().min(8).required(),
|
||||||
confirmPassword: Yup.string().test('password', 'Password confirmation does not match the password you entered.', function (value) {
|
confirmPassword: Yup.string().test(
|
||||||
return value === this.parent.password;
|
'password',
|
||||||
}),
|
'Password confirmation does not match the password you entered.',
|
||||||
|
function (value) {
|
||||||
|
return value === this.parent.password;
|
||||||
|
}
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
@ -39,12 +43,14 @@ export default () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.location = '/auth/login';
|
window.location = '/auth/login';
|
||||||
})
|
})
|
||||||
.catch(error => addFlash({
|
.catch((error) =>
|
||||||
key: 'account:password',
|
addFlash({
|
||||||
type: 'error',
|
key: 'account:password',
|
||||||
title: 'Error',
|
type: 'error',
|
||||||
message: httpErrorToHuman(error),
|
title: 'Error',
|
||||||
}))
|
message: httpErrorToHuman(error),
|
||||||
|
})
|
||||||
|
)
|
||||||
.then(() => setSubmitting(false));
|
.then(() => setSubmitting(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,43 +61,43 @@ export default () => {
|
||||||
validationSchema={schema}
|
validationSchema={schema}
|
||||||
initialValues={{ current: '', password: '', confirmPassword: '' }}
|
initialValues={{ current: '', password: '', confirmPassword: '' }}
|
||||||
>
|
>
|
||||||
{
|
{({ isSubmitting, isValid }) => (
|
||||||
({ isSubmitting, isValid }) => (
|
<React.Fragment>
|
||||||
<React.Fragment>
|
<SpinnerOverlay size={'large'} visible={isSubmitting} />
|
||||||
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
|
<Form css={tw`m-0`}>
|
||||||
<Form css={tw`m-0`}>
|
<Field
|
||||||
|
id={'current_password'}
|
||||||
|
type={'password'}
|
||||||
|
name={'current'}
|
||||||
|
label={'Current Password'}
|
||||||
|
/>
|
||||||
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Field
|
||||||
id={'current_password'}
|
id={'new_password'}
|
||||||
type={'password'}
|
type={'password'}
|
||||||
name={'current'}
|
name={'password'}
|
||||||
label={'Current Password'}
|
label={'New Password'}
|
||||||
|
description={
|
||||||
|
'Your new password should be at least 8 characters in length and unique to this website.'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div css={tw`mt-6`}>
|
</div>
|
||||||
<Field
|
<div css={tw`mt-6`}>
|
||||||
id={'new_password'}
|
<Field
|
||||||
type={'password'}
|
id={'confirm_new_password'}
|
||||||
name={'password'}
|
type={'password'}
|
||||||
label={'New Password'}
|
name={'confirmPassword'}
|
||||||
description={'Your new password should be at least 8 characters in length and unique to this website.'}
|
label={'Confirm New Password'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
<Field
|
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
||||||
id={'confirm_new_password'}
|
Update Password
|
||||||
type={'password'}
|
</Button>
|
||||||
name={'confirmPassword'}
|
</div>
|
||||||
label={'Confirm New Password'}
|
</Form>
|
||||||
/>
|
</React.Fragment>
|
||||||
</div>
|
)}
|
||||||
<div css={tw`mt-6`}>
|
|
||||||
<Button size={'small'} disabled={isSubmitting || !isValid}>
|
|
||||||
Update Password
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Formik>
|
</Formik>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,10 +6,10 @@ import SearchModal from '@/components/dashboard/search/SearchModal';
|
||||||
import Tooltip from '@/components/elements/tooltip/Tooltip';
|
import Tooltip from '@/components/elements/tooltip/Tooltip';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
useEventListener('keydown', (e: KeyboardEvent) => {
|
useEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
if ([ 'input', 'textarea' ].indexOf(((e.target as HTMLElement).tagName || 'input').toLowerCase()) < 0) {
|
if (['input', 'textarea'].indexOf(((e.target as HTMLElement).tagName || 'input').toLowerCase()) < 0) {
|
||||||
if (!visible && e.metaKey && e.key.toLowerCase() === '/') {
|
if (!visible && e.metaKey && e.key.toLowerCase() === '/') {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}
|
}
|
||||||
|
@ -18,16 +18,10 @@ export default () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{visible &&
|
{visible && <SearchModal appear visible={visible} onDismissed={() => setVisible(false)} />}
|
||||||
<SearchModal
|
|
||||||
appear
|
|
||||||
visible={visible}
|
|
||||||
onDismissed={() => setVisible(false)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<Tooltip placement={'bottom'} content={'Search'}>
|
<Tooltip placement={'bottom'} content={'Search'}>
|
||||||
<div className={'navigation-link'} onClick={() => setVisible(true)}>
|
<div className={'navigation-link'} onClick={() => setVisible(true)}>
|
||||||
<FontAwesomeIcon icon={faSearch}/>
|
<FontAwesomeIcon icon={faSearch} />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -40,24 +40,26 @@ const SearchWatcher = () => {
|
||||||
if (values.term.length >= 3) {
|
if (values.term.length >= 3) {
|
||||||
submitForm();
|
submitForm();
|
||||||
}
|
}
|
||||||
}, [ values.term ]);
|
}, [values.term]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ ...props }: Props) => {
|
export default ({ ...props }: Props) => {
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
const isAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
const isAdmin = useStoreState((state) => state.user.data!.rootAdmin);
|
||||||
const [ servers, setServers ] = useState<Server[]>([]);
|
const [servers, setServers] = useState<Server[]>([]);
|
||||||
const { clearAndAddHttpError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { clearAndAddHttpError, clearFlashes } = useStoreActions(
|
||||||
|
(actions: Actions<ApplicationStore>) => actions.flashes
|
||||||
|
);
|
||||||
|
|
||||||
const search = debounce(({ term }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
const search = debounce(({ term }: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
clearFlashes('search');
|
clearFlashes('search');
|
||||||
|
|
||||||
// if (ref.current) ref.current.focus();
|
// if (ref.current) ref.current.focus();
|
||||||
getServers({ query: term, type: isAdmin ? 'admin-all' : undefined })
|
getServers({ query: term, type: isAdmin ? 'admin-all' : undefined })
|
||||||
.then(servers => setServers(servers.items.filter((_, index) => index < 5)))
|
.then((servers) => setServers(servers.items.filter((_, index) => index < 5)))
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
clearAndAddHttpError({ key: 'search', error });
|
clearAndAddHttpError({ key: 'search', error });
|
||||||
})
|
})
|
||||||
|
@ -69,10 +71,10 @@ export default ({ ...props }: Props) => {
|
||||||
if (props.visible) {
|
if (props.visible) {
|
||||||
if (ref.current) ref.current.focus();
|
if (ref.current) ref.current.focus();
|
||||||
}
|
}
|
||||||
}, [ props.visible ]);
|
}, [props.visible]);
|
||||||
|
|
||||||
// Formik does not support an innerRef on custom components.
|
// Formik does not support an innerRef on custom components.
|
||||||
const InputWithRef = (props: any) => <Input autoFocus {...props} ref={ref}/>;
|
const InputWithRef = (props: any) => <Input autoFocus {...props} ref={ref} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
|
@ -90,16 +92,15 @@ export default ({ ...props }: Props) => {
|
||||||
label={'Search term'}
|
label={'Search term'}
|
||||||
description={'Enter a server name, uuid, or allocation to begin searching.'}
|
description={'Enter a server name, uuid, or allocation to begin searching.'}
|
||||||
>
|
>
|
||||||
<SearchWatcher/>
|
<SearchWatcher />
|
||||||
<InputSpinner visible={isSubmitting}>
|
<InputSpinner visible={isSubmitting}>
|
||||||
<Field as={InputWithRef} name={'term'}/>
|
<Field as={InputWithRef} name={'term'} />
|
||||||
</InputSpinner>
|
</InputSpinner>
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
</Form>
|
</Form>
|
||||||
{servers.length > 0 &&
|
{servers.length > 0 && (
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
{
|
{servers.map((server) => (
|
||||||
servers.map(server => (
|
|
||||||
<ServerResult
|
<ServerResult
|
||||||
key={server.uuid}
|
key={server.uuid}
|
||||||
to={`/server/${server.id}`}
|
to={`/server/${server.id}`}
|
||||||
|
@ -108,11 +109,13 @@ export default ({ ...props }: Props) => {
|
||||||
<div css={tw`flex-1 mr-4`}>
|
<div css={tw`flex-1 mr-4`}>
|
||||||
<p css={tw`text-sm`}>{server.name}</p>
|
<p css={tw`text-sm`}>{server.name}</p>
|
||||||
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
<p css={tw`mt-1 text-xs text-neutral-400`}>
|
||||||
{
|
{server.allocations
|
||||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
.filter((alloc) => alloc.isDefault)
|
||||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || ip(allocation.ip)}:{allocation.port}</span>
|
.map((allocation) => (
|
||||||
))
|
<span key={allocation.ip + allocation.port.toString()}>
|
||||||
}
|
{allocation.alias || ip(allocation.ip)}:{allocation.port}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`flex-none text-right`}>
|
<div css={tw`flex-none text-right`}>
|
||||||
|
@ -121,10 +124,9 @@ export default ({ ...props }: Props) => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ServerResult>
|
</ServerResult>
|
||||||
))
|
))}
|
||||||
}
|
</div>
|
||||||
</div>
|
)}
|
||||||
}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
|
|
|
@ -22,43 +22,40 @@ export default () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearAndAddHttpError(error);
|
clearAndAddHttpError(error);
|
||||||
}, [ error ]);
|
}, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={'Account API'}>
|
<PageContentBlock title={'Account API'}>
|
||||||
<FlashMessageRender byKey={'account'}/>
|
<FlashMessageRender byKey={'account'} />
|
||||||
<div css={tw`md:flex flex-nowrap my-10`}>
|
<div css={tw`md:flex flex-nowrap my-10`}>
|
||||||
<ContentBox title={'Add SSH Key'} css={tw`flex-none w-full md:w-1/2`}>
|
<ContentBox title={'Add SSH Key'} css={tw`flex-none w-full md:w-1/2`}>
|
||||||
<CreateSSHKeyForm/>
|
<CreateSSHKeyForm />
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
<ContentBox title={'SSH Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
|
<ContentBox title={'SSH Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
|
||||||
<SpinnerOverlay visible={!data && isValidating}/>
|
<SpinnerOverlay visible={!data && isValidating} />
|
||||||
{
|
{!data || !data.length ? (
|
||||||
!data || !data.length ?
|
<p css={tw`text-center text-sm`}>
|
||||||
<p css={tw`text-center text-sm`}>
|
{!data ? 'Loading...' : 'No SSH Keys exist for this account.'}
|
||||||
{!data ? 'Loading...' : 'No SSH Keys exist for this account.'}
|
</p>
|
||||||
</p>
|
) : (
|
||||||
:
|
data.map((key, index) => (
|
||||||
data.map((key, index) => (
|
<GreyRowBox
|
||||||
<GreyRowBox
|
key={key.fingerprint}
|
||||||
key={key.fingerprint}
|
css={[tw`bg-neutral-600 flex space-x-4 items-center`, index > 0 && tw`mt-2`]}
|
||||||
css={[ tw`bg-neutral-600 flex space-x-4 items-center`, index > 0 && tw`mt-2` ]}
|
>
|
||||||
>
|
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`} />
|
||||||
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`}/>
|
<div css={tw`flex-1`}>
|
||||||
<div css={tw`flex-1`}>
|
<p css={tw`text-sm break-words font-medium`}>{key.name}</p>
|
||||||
<p css={tw`text-sm break-words font-medium`}>{key.name}</p>
|
<p css={tw`text-xs mt-1 font-mono truncate`}>SHA256:{key.fingerprint}</p>
|
||||||
<p css={tw`text-xs mt-1 font-mono truncate`}>
|
<p css={tw`text-xs mt-1 text-neutral-300 uppercase`}>
|
||||||
SHA256:{key.fingerprint}
|
Added on:
|
||||||
</p>
|
{format(key.createdAt, 'MMM do, yyyy HH:mm')}
|
||||||
<p css={tw`text-xs mt-1 text-neutral-300 uppercase`}>
|
</p>
|
||||||
Added on:
|
</div>
|
||||||
{format(key.createdAt, 'MMM do, yyyy HH:mm')}
|
<DeleteSSHKeyButton name={key.name} fingerprint={key.fingerprint} />
|
||||||
</p>
|
</GreyRowBox>
|
||||||
</div>
|
))
|
||||||
<DeleteSSHKeyButton name={key.name} fingerprint={key.fingerprint} />
|
)}
|
||||||
</GreyRowBox>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
</div>
|
</div>
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
|
|
|
@ -15,7 +15,9 @@ interface Values {
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomTextarea = styled(Textarea)`${tw`h-32`}`;
|
const CustomTextarea = styled(Textarea)`
|
||||||
|
${tw`h-32`}
|
||||||
|
`;
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { clearAndAddHttpError } = useFlashKey('account');
|
const { clearAndAddHttpError } = useFlashKey('account');
|
||||||
|
@ -45,16 +47,16 @@ export default () => {
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
<SpinnerOverlay visible={isSubmitting}/>
|
<SpinnerOverlay visible={isSubmitting} />
|
||||||
<FormikFieldWrapper label={'SSH Key Name'} name={'name'} css={tw`mb-6`}>
|
<FormikFieldWrapper label={'SSH Key Name'} name={'name'} css={tw`mb-6`}>
|
||||||
<Field name={'name'} as={Input}/>
|
<Field name={'name'} as={Input} />
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
<FormikFieldWrapper
|
<FormikFieldWrapper
|
||||||
label={'Public Key'}
|
label={'Public Key'}
|
||||||
name={'publicKey'}
|
name={'publicKey'}
|
||||||
description={'Enter your public SSH key.'}
|
description={'Enter your public SSH key.'}
|
||||||
>
|
>
|
||||||
<Field name={'publicKey'} as={CustomTextarea}/>
|
<Field name={'publicKey'} as={CustomTextarea} />
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
<div css={tw`flex justify-end mt-6`}>
|
<div css={tw`flex justify-end mt-6`}>
|
||||||
<Button>Save</Button>
|
<Button>Save</Button>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Code from '@/components/elements/Code';
|
||||||
|
|
||||||
export default ({ name, fingerprint }: { name: string; fingerprint: string }) => {
|
export default ({ name, fingerprint }: { name: string; fingerprint: string }) => {
|
||||||
const { clearAndAddHttpError } = useFlashKey('account');
|
const { clearAndAddHttpError } = useFlashKey('account');
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const { mutate } = useSSHKeys();
|
const { mutate } = useSSHKeys();
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
|
@ -18,11 +18,10 @@ export default ({ name, fingerprint }: { name: string; fingerprint: string }) =>
|
||||||
Promise.all([
|
Promise.all([
|
||||||
mutate((data) => data?.filter((value) => value.fingerprint !== fingerprint), false),
|
mutate((data) => data?.filter((value) => value.fingerprint !== fingerprint), false),
|
||||||
deleteSSHKey(fingerprint),
|
deleteSSHKey(fingerprint),
|
||||||
])
|
]).catch((error) => {
|
||||||
.catch((error) => {
|
mutate(undefined, true).catch(console.error);
|
||||||
mutate(undefined, true).catch(console.error);
|
clearAndAddHttpError(error);
|
||||||
clearAndAddHttpError(error);
|
});
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -3,14 +3,14 @@ import { Redirect, Route, RouteProps } from 'react-router';
|
||||||
import { useStoreState } from '@/state/hooks';
|
import { useStoreState } from '@/state/hooks';
|
||||||
|
|
||||||
export default ({ children, ...props }: Omit<RouteProps, 'render'>) => {
|
export default ({ children, ...props }: Omit<RouteProps, 'render'>) => {
|
||||||
const isAuthenticated = useStoreState(state => !!state.user.data?.uuid);
|
const isAuthenticated = useStoreState((state) => !!state.user.data?.uuid);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Route
|
<Route
|
||||||
{...props}
|
{...props}
|
||||||
render={({ location }) => (
|
render={({ location }) =>
|
||||||
isAuthenticated ? children : <Redirect to={{ pathname: '/auth/login', state: { from: location } }}/>
|
isAuthenticated ? children : <Redirect to={{ pathname: '/auth/login', state: { from: location } }} />
|
||||||
)}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,88 +12,103 @@ interface Props {
|
||||||
|
|
||||||
const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
||||||
${tw`relative inline-block rounded p-2 uppercase tracking-wide text-sm transition-all duration-150 border`};
|
${tw`relative inline-block rounded p-2 uppercase tracking-wide text-sm transition-all duration-150 border`};
|
||||||
|
|
||||||
${props => ((!props.isSecondary && !props.color) || props.color === 'primary') && css<Props>`
|
${(props) =>
|
||||||
${props => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
|
((!props.isSecondary && !props.color) || props.color === 'primary') &&
|
||||||
|
css<Props>`
|
||||||
&:hover:not(:disabled) {
|
${(props) => !props.isSecondary && tw`bg-primary-500 border-primary-600 border text-primary-50`};
|
||||||
${tw`bg-primary-600 border-primary-700`};
|
|
||||||
}
|
&:hover:not(:disabled) {
|
||||||
`};
|
${tw`bg-primary-600 border-primary-700`};
|
||||||
|
}
|
||||||
${props => props.color === 'grey' && css`
|
`};
|
||||||
${tw`border-neutral-600 bg-neutral-500 text-neutral-50`};
|
|
||||||
|
${(props) =>
|
||||||
&:hover:not(:disabled) {
|
props.color === 'grey' &&
|
||||||
${tw`bg-neutral-600 border-neutral-700`};
|
css`
|
||||||
}
|
${tw`border-neutral-600 bg-neutral-500 text-neutral-50`};
|
||||||
`};
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
${props => props.color === 'green' && css<Props>`
|
${tw`bg-neutral-600 border-neutral-700`};
|
||||||
${tw`border-green-600 bg-green-500 text-green-50`};
|
}
|
||||||
|
`};
|
||||||
&:hover:not(:disabled) {
|
|
||||||
${tw`bg-green-600 border-green-700`};
|
${(props) =>
|
||||||
}
|
props.color === 'green' &&
|
||||||
|
css<Props>`
|
||||||
${props => props.isSecondary && css`
|
${tw`border-green-600 bg-green-500 text-green-50`};
|
||||||
&:active:not(:disabled) {
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
${tw`bg-green-600 border-green-700`};
|
${tw`bg-green-600 border-green-700`};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.isSecondary &&
|
||||||
|
css`
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
${tw`bg-green-600 border-green-700`};
|
||||||
|
}
|
||||||
|
`};
|
||||||
`};
|
`};
|
||||||
`};
|
|
||||||
|
${(props) =>
|
||||||
${props => props.color === 'red' && css<Props>`
|
props.color === 'red' &&
|
||||||
${tw`border-red-600 bg-red-500 text-red-50`};
|
css<Props>`
|
||||||
|
${tw`border-red-600 bg-red-500 text-red-50`};
|
||||||
&:hover:not(:disabled) {
|
|
||||||
${tw`bg-red-600 border-red-700`};
|
&:hover:not(:disabled) {
|
||||||
}
|
|
||||||
|
|
||||||
${props => props.isSecondary && css`
|
|
||||||
&:active:not(:disabled) {
|
|
||||||
${tw`bg-red-600 border-red-700`};
|
${tw`bg-red-600 border-red-700`};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.isSecondary &&
|
||||||
|
css`
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
${tw`bg-red-600 border-red-700`};
|
||||||
|
}
|
||||||
|
`};
|
||||||
`};
|
`};
|
||||||
`};
|
|
||||||
|
${(props) => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
|
||||||
${props => props.size === 'xsmall' && tw`px-2 py-1 text-xs`};
|
${(props) => (!props.size || props.size === 'small') && tw`px-4 py-2`};
|
||||||
${props => (!props.size || props.size === 'small') && tw`px-4 py-2`};
|
${(props) => props.size === 'large' && tw`p-4 text-sm`};
|
||||||
${props => props.size === 'large' && tw`p-4 text-sm`};
|
${(props) => props.size === 'xlarge' && tw`p-4 w-full`};
|
||||||
${props => props.size === 'xlarge' && tw`p-4 w-full`};
|
|
||||||
|
${(props) =>
|
||||||
${props => props.isSecondary && css<Props>`
|
props.isSecondary &&
|
||||||
${tw`border-neutral-600 bg-transparent text-neutral-200`};
|
css<Props>`
|
||||||
|
${tw`border-neutral-600 bg-transparent text-neutral-200`};
|
||||||
&:hover:not(:disabled) {
|
|
||||||
${tw`border-neutral-500 text-neutral-100`};
|
&:hover:not(:disabled) {
|
||||||
${props => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
|
${tw`border-neutral-500 text-neutral-100`};
|
||||||
${props => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`};
|
${(props) => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
|
||||||
${props => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
|
${(props) => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`};
|
||||||
}
|
${(props) => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
|
||||||
`};
|
}
|
||||||
|
`};
|
||||||
&:disabled { opacity: 0.55; cursor: default }
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type ComponentProps = Omit<JSX.IntrinsicElements['button'], 'ref' | keyof Props> & Props;
|
type ComponentProps = Omit<JSX.IntrinsicElements['button'], 'ref' | keyof Props> & Props;
|
||||||
|
|
||||||
const Button: React.FC<ComponentProps> = ({ children, isLoading, ...props }) => (
|
const Button: React.FC<ComponentProps> = ({ children, isLoading, ...props }) => (
|
||||||
<ButtonStyle {...props}>
|
<ButtonStyle {...props}>
|
||||||
{isLoading &&
|
{isLoading && (
|
||||||
<div css={tw`flex absolute justify-center items-center w-full h-full left-0 top-0`}>
|
<div css={tw`flex absolute justify-center items-center w-full h-full left-0 top-0`}>
|
||||||
<Spinner size={'small'}/>
|
<Spinner size={'small'} />
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<span css={isLoading ? tw`text-transparent` : undefined}>
|
<span css={isLoading ? tw`text-transparent` : undefined}>{children}</span>
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
</ButtonStyle>
|
</ButtonStyle>
|
||||||
);
|
);
|
||||||
|
|
||||||
type LinkProps = Omit<JSX.IntrinsicElements['a'], 'ref' | keyof Props> & Props;
|
type LinkProps = Omit<JSX.IntrinsicElements['a'], 'ref' | keyof Props> & Props;
|
||||||
|
|
||||||
const LinkButton: React.FC<LinkProps> = props => <ButtonStyle as={'a'} {...props}/>;
|
const LinkButton: React.FC<LinkProps> = (props) => <ButtonStyle as={'a'} {...props} />;
|
||||||
|
|
||||||
export { LinkButton, ButtonStyle };
|
export { LinkButton, ButtonStyle };
|
||||||
export default Button;
|
export default Button;
|
||||||
|
|
|
@ -14,12 +14,9 @@ const Can = ({ action, matchAny = false, renderOnError, children }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{(matchAny && can.filter((p) => p).length > 0) || (!matchAny && can.every((p) => p))
|
||||||
((matchAny && can.filter(p => p).length > 0) || (!matchAny && can.every(p => p))) ?
|
? children
|
||||||
children
|
: renderOnError}
|
||||||
:
|
|
||||||
renderOnError
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,7 @@ const Checkbox = ({ name, value, className, ...props }: Props & InputProps) => (
|
||||||
type={'checkbox'}
|
type={'checkbox'}
|
||||||
checked={(field.value || []).includes(value)}
|
checked={(field.value || []).includes(value)}
|
||||||
onClick={() => form.setFieldTouched(field.name, true)}
|
onClick={() => form.setFieldTouched(field.name, true)}
|
||||||
onChange={e => {
|
onChange={(e) => {
|
||||||
const set = new Set(field.value);
|
const set = new Set(field.value);
|
||||||
set.has(value) ? set.delete(value) : set.add(value);
|
set.has(value) ? set.delete(value) : set.add(value);
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ const EditorContainer = styled.div`
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
color: #CBCCC6;
|
color: #cbccc6;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
|
@ -144,7 +144,7 @@ const findModeByFilename = (filename: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => {
|
export default ({ style, initialContent, filename, mode, fetchContent, onContentSaved, onModeChanged }: Props) => {
|
||||||
const [ editor, setEditor ] = useState<CodeMirror.Editor>();
|
const [editor, setEditor] = useState<CodeMirror.Editor>();
|
||||||
|
|
||||||
const ref = useCallback((node) => {
|
const ref = useCallback((node) => {
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
@ -173,7 +173,7 @@ export default ({ style, initialContent, filename, mode, fetchContent, onContent
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
autoCloseBrackets: true,
|
autoCloseBrackets: true,
|
||||||
matchBrackets: true,
|
matchBrackets: true,
|
||||||
gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ],
|
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
});
|
});
|
||||||
|
|
||||||
setEditor(e);
|
setEditor(e);
|
||||||
|
@ -185,15 +185,15 @@ export default ({ style, initialContent, filename, mode, fetchContent, onContent
|
||||||
}
|
}
|
||||||
|
|
||||||
onModeChanged(findModeByFilename(filename)?.mime || 'text/plain');
|
onModeChanged(findModeByFilename(filename)?.mime || 'text/plain');
|
||||||
}, [ filename ]);
|
}, [filename]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editor && editor.setOption('mode', mode);
|
editor && editor.setOption('mode', mode);
|
||||||
}, [ editor, mode ]);
|
}, [editor, mode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editor && editor.setValue(initialContent || '');
|
editor && editor.setValue(initialContent || '');
|
||||||
}, [ editor, initialContent ]);
|
}, [editor, initialContent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
@ -207,11 +207,11 @@ export default ({ style, initialContent, filename, mode, fetchContent, onContent
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchContent(() => Promise.resolve(editor.getValue()));
|
fetchContent(() => Promise.resolve(editor.getValue()));
|
||||||
}, [ editor, fetchContent, onContentSaved ]);
|
}, [editor, fetchContent, onContentSaved]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorContainer style={style}>
|
<EditorContainer style={style}>
|
||||||
<textarea ref={ref}/>
|
<textarea ref={ref} />
|
||||||
</EditorContainer>
|
</EditorContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,9 +17,7 @@ const ConfirmationModal: React.FC<Props> = ({ title, children, buttonText, onCon
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
|
<h2 css={tw`text-2xl mb-6`}>{title}</h2>
|
||||||
<div css={tw`text-neutral-300`}>
|
<div css={tw`text-neutral-300`}>{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<div css={tw`flex flex-wrap items-center justify-end mt-8`}>
|
<div css={tw`flex flex-wrap items-center justify-end mt-8`}>
|
||||||
<Button isSecondary onClick={() => dismiss()} css={tw`w-full sm:w-auto border-transparent`}>
|
<Button isSecondary onClick={() => dismiss()} css={tw`w-full sm:w-auto border-transparent`}>
|
||||||
Cancel
|
Cancel
|
||||||
|
@ -34,6 +32,6 @@ const ConfirmationModal: React.FC<Props> = ({ title, children, buttonText, onCon
|
||||||
|
|
||||||
ConfirmationModal.displayName = 'ConfirmationModal';
|
ConfirmationModal.displayName = 'ConfirmationModal';
|
||||||
|
|
||||||
export default asModal<Props>(props => ({
|
export default asModal<Props>((props) => ({
|
||||||
showSpinnerOverlay: props.showSpinnerOverlay,
|
showSpinnerOverlay: props.showSpinnerOverlay,
|
||||||
}))(ConfirmationModal);
|
}))(ConfirmationModal);
|
||||||
|
|
|
@ -3,29 +3,23 @@ import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
type Props = Readonly<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
type Props = Readonly<
|
||||||
title?: string;
|
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||||
borderColor?: string;
|
title?: string;
|
||||||
showFlashes?: string | boolean;
|
borderColor?: string;
|
||||||
showLoadingOverlay?: boolean;
|
showFlashes?: string | boolean;
|
||||||
}>;
|
showLoadingOverlay?: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
const ContentBox = ({ title, borderColor, showFlashes, showLoadingOverlay, children, ...props }: Props) => (
|
const ContentBox = ({ title, borderColor, showFlashes, showLoadingOverlay, children, ...props }: Props) => (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{title && <h2 css={tw`text-neutral-300 mb-4 px-4 text-2xl`}>{title}</h2>}
|
{title && <h2 css={tw`text-neutral-300 mb-4 px-4 text-2xl`}>{title}</h2>}
|
||||||
{showFlashes &&
|
{showFlashes && (
|
||||||
<FlashMessageRender
|
<FlashMessageRender byKey={typeof showFlashes === 'string' ? showFlashes : undefined} css={tw`mb-4`} />
|
||||||
byKey={typeof showFlashes === 'string' ? showFlashes : undefined}
|
)}
|
||||||
css={tw`mb-4`}
|
<div css={[tw`bg-neutral-700 p-4 rounded shadow-lg relative`, !!borderColor && tw`border-t-4`]}>
|
||||||
/>
|
<SpinnerOverlay visible={showLoadingOverlay || false} />
|
||||||
}
|
|
||||||
<div
|
|
||||||
css={[
|
|
||||||
tw`bg-neutral-700 p-4 rounded shadow-lg relative`,
|
|
||||||
!!borderColor && tw`border-t-4`,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SpinnerOverlay visible={showLoadingOverlay || false}/>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,7 +20,7 @@ const Toast = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CopyOnClick: React.FC<{ text: any }> = ({ text, children }) => {
|
const CopyOnClick: React.FC<{ text: any }> = ({ text, children }) => {
|
||||||
const [ copied, setCopied ] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!copied) return;
|
if (!copied) return;
|
||||||
|
@ -32,7 +32,7 @@ const CopyOnClick: React.FC<{ text: any }> = ({ text, children }) => {
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
};
|
};
|
||||||
}, [ copied ]);
|
}, [copied]);
|
||||||
|
|
||||||
const onCopy = useCallback(() => {
|
const onCopy = useCallback(() => {
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
|
@ -42,15 +42,15 @@ const CopyOnClick: React.FC<{ text: any }> = ({ text, children }) => {
|
||||||
<>
|
<>
|
||||||
<SwitchTransition>
|
<SwitchTransition>
|
||||||
<Fade timeout={250} key={copied ? 'visible' : 'invisible'}>
|
<Fade timeout={250} key={copied ? 'visible' : 'invisible'}>
|
||||||
{copied ?
|
{copied ? (
|
||||||
<Toast>
|
<Toast>
|
||||||
<div>
|
<div>
|
||||||
<p>Copied "{text}" to clipboard.</p>
|
<p>Copied "{text}" to clipboard.</p>
|
||||||
</div>
|
</div>
|
||||||
</Toast>
|
</Toast>
|
||||||
:
|
) : (
|
||||||
<></>
|
<></>
|
||||||
}
|
)}
|
||||||
</Fade>
|
</Fade>
|
||||||
</SwitchTransition>
|
</SwitchTransition>
|
||||||
<CopyToClipboard onCopy={onCopy} text={text} options={{ debug: true }} css={tw`cursor-pointer`}>
|
<CopyToClipboard onCopy={onCopy} text={text} options={{ debug: true }} css={tw`cursor-pointer`}>
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const DropdownButtonRow = styled.button<{ danger?: boolean }>`
|
||||||
transition: 150ms all ease;
|
transition: 150ms all ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
${props => props.danger ? tw`text-red-700 bg-red-100` : tw`text-neutral-700 bg-neutral-100`};
|
${(props) => (props.danger ? tw`text-red-700 bg-red-100` : tw`text-neutral-700 bg-neutral-100`)};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -30,11 +30,11 @@ class DropdownMenu extends React.PureComponent<Props, State> {
|
||||||
visible: false,
|
visible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
this.removeListeners();
|
this.removeListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
|
||||||
const menu = this.menu.current;
|
const menu = this.menu.current;
|
||||||
|
|
||||||
if (this.state.visible && !prevState.visible && menu) {
|
if (this.state.visible && !prevState.visible && menu) {
|
||||||
|
@ -76,19 +76,20 @@ class DropdownMenu extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
triggerMenu = (posX: number) => this.setState(s => ({
|
triggerMenu = (posX: number) =>
|
||||||
posX: !s.visible ? posX : s.posX,
|
this.setState((s) => ({
|
||||||
visible: !s.visible,
|
posX: !s.visible ? posX : s.posX,
|
||||||
}));
|
visible: !s.visible,
|
||||||
|
}));
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{this.props.renderToggle(this.onClickHandler)}
|
{this.props.renderToggle(this.onClickHandler)}
|
||||||
<Fade timeout={150} in={this.state.visible} unmountOnExit>
|
<Fade timeout={150} in={this.state.visible} unmountOnExit>
|
||||||
<div
|
<div
|
||||||
ref={this.menu}
|
ref={this.menu}
|
||||||
onClick={e => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.setState({ visible: false });
|
this.setState({ visible: false });
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -13,26 +13,27 @@ class ErrorBoundary extends React.Component<{}, State> {
|
||||||
hasError: false,
|
hasError: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
static getDerivedStateFromError () {
|
static getDerivedStateFromError() {
|
||||||
return { hasError: true };
|
return { hasError: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch (error: Error) {
|
componentDidCatch(error: Error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
return this.state.hasError ?
|
return this.state.hasError ? (
|
||||||
<div css={tw`flex items-center justify-center w-full my-4`}>
|
<div css={tw`flex items-center justify-center w-full my-4`}>
|
||||||
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
|
<div css={tw`flex items-center bg-neutral-900 rounded p-3 text-red-500`}>
|
||||||
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`}/>
|
<Icon icon={faExclamationTriangle} css={tw`h-4 w-auto mr-2`} />
|
||||||
<p css={tw`text-sm text-neutral-100`}>
|
<p css={tw`text-sm text-neutral-100`}>
|
||||||
An error was encountered by the application while rendering this view. Try refreshing the page.
|
An error was encountered by the application while rendering this view. Try refreshing the page.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
:
|
) : (
|
||||||
this.props.children;
|
this.props.children
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,25 +8,29 @@ interface Props extends Omit<CSSTransitionProps, 'timeout' | 'classNames'> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div<{ timeout: number }>`
|
const Container = styled.div<{ timeout: number }>`
|
||||||
.fade-enter, .fade-exit, .fade-appear {
|
.fade-enter,
|
||||||
|
.fade-exit,
|
||||||
|
.fade-appear {
|
||||||
will-change: opacity;
|
will-change: opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter, .fade-appear {
|
.fade-enter,
|
||||||
|
.fade-appear {
|
||||||
${tw`opacity-0`};
|
${tw`opacity-0`};
|
||||||
|
|
||||||
&.fade-enter-active, &.fade-appear-active {
|
&.fade-enter-active,
|
||||||
|
&.fade-appear-active {
|
||||||
${tw`opacity-100 transition-opacity ease-in`};
|
${tw`opacity-100 transition-opacity ease-in`};
|
||||||
transition-duration: ${props => props.timeout}ms;
|
transition-duration: ${(props) => props.timeout}ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-exit {
|
.fade-exit {
|
||||||
${tw`opacity-100`};
|
${tw`opacity-100`};
|
||||||
|
|
||||||
&.fade-exit-active {
|
&.fade-exit-active {
|
||||||
${tw`opacity-0 transition-opacity ease-in`};
|
${tw`opacity-0 transition-opacity ease-in`};
|
||||||
transition-duration: ${props => props.timeout}ms;
|
transition-duration: ${(props) => props.timeout}ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -13,14 +13,16 @@ interface OwnProps {
|
||||||
|
|
||||||
type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>;
|
type Props = OwnProps & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'name'>;
|
||||||
|
|
||||||
const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, label, description, validate, ...props }, ref) => (
|
const Field = forwardRef<HTMLInputElement, Props>(
|
||||||
<FormikField innerRef={ref} name={name} validate={validate}>
|
({ id, name, light = false, label, description, validate, ...props }, ref) => (
|
||||||
{
|
<FormikField innerRef={ref} name={name} validate={validate}>
|
||||||
({ field, form: { errors, touched } }: FieldProps) => (
|
{({ field, form: { errors, touched } }: FieldProps) => (
|
||||||
<div>
|
<div>
|
||||||
{label &&
|
{label && (
|
||||||
<Label htmlFor={id} isLight={light}>{label}</Label>
|
<Label htmlFor={id} isLight={light}>
|
||||||
}
|
{label}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
{...field}
|
{...field}
|
||||||
|
@ -28,18 +30,19 @@ const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, la
|
||||||
isLight={light}
|
isLight={light}
|
||||||
hasError={!!(touched[field.name] && errors[field.name])}
|
hasError={!!(touched[field.name] && errors[field.name])}
|
||||||
/>
|
/>
|
||||||
{touched[field.name] && errors[field.name] ?
|
{touched[field.name] && errors[field.name] ? (
|
||||||
<p className={'input-help error'}>
|
<p className={'input-help error'}>
|
||||||
{(errors[field.name] as string).charAt(0).toUpperCase() + (errors[field.name] as string).slice(1)}
|
{(errors[field.name] as string).charAt(0).toUpperCase() +
|
||||||
|
(errors[field.name] as string).slice(1)}
|
||||||
</p>
|
</p>
|
||||||
:
|
) : description ? (
|
||||||
description ? <p className={'input-help'}>{description}</p> : null
|
<p className={'input-help'}>{description}</p>
|
||||||
}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
</FormikField>
|
||||||
</FormikField>
|
)
|
||||||
));
|
);
|
||||||
Field.displayName = 'Field';
|
Field.displayName = 'Field';
|
||||||
|
|
||||||
export default Field;
|
export default Field;
|
||||||
|
|
|
@ -15,17 +15,15 @@ interface Props {
|
||||||
|
|
||||||
const FormikFieldWrapper = ({ id, name, label, className, description, validate, children }: Props) => (
|
const FormikFieldWrapper = ({ id, name, label, className, description, validate, children }: Props) => (
|
||||||
<Field name={name} validate={validate}>
|
<Field name={name} validate={validate}>
|
||||||
{
|
{({ field, form: { errors, touched } }: FieldProps) => (
|
||||||
({ field, form: { errors, touched } }: FieldProps) => (
|
<div className={`${className} ${touched[field.name] && errors[field.name] ? 'has-error' : undefined}`}>
|
||||||
<div className={`${className} ${(touched[field.name] && errors[field.name]) ? 'has-error' : undefined}`}>
|
{label && <Label htmlFor={id}>{label}</Label>}
|
||||||
{label && <Label htmlFor={id}>{label}</Label>}
|
{children}
|
||||||
{children}
|
<InputError errors={errors} touched={touched} name={field.name}>
|
||||||
<InputError errors={errors} touched={touched} name={field.name}>
|
{description || null}
|
||||||
{description || null}
|
</InputError>
|
||||||
</InputError>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import tw from 'twin.macro';
|
||||||
|
|
||||||
export default styled.div<{ $hoverable?: boolean }>`
|
export default styled.div<{ $hoverable?: boolean }>`
|
||||||
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150 overflow-hidden`};
|
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150 overflow-hidden`};
|
||||||
|
|
||||||
${props => props.$hoverable !== false && tw`hover:border-neutral-500`};
|
${(props) => props.$hoverable !== false && tw`hover:border-neutral-500`};
|
||||||
|
|
||||||
& .icon {
|
& .icon {
|
||||||
${tw`rounded-full bg-neutral-500 p-3`};
|
${tw`rounded-full bg-neutral-500 p-3`};
|
||||||
|
|
|
@ -9,9 +9,9 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Icon = ({ icon, className, style }: Props) => {
|
const Icon = ({ icon, className, style }: Props) => {
|
||||||
let [ width, height, , , paths ] = icon.icon;
|
let [width, height, , , paths] = icon.icon;
|
||||||
|
|
||||||
paths = Array.isArray(paths) ? paths : [ paths ];
|
paths = Array.isArray(paths) ? paths : [paths];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -22,7 +22,7 @@ const Icon = ({ icon, className, style }: Props) => {
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{paths.map((path, index) => (
|
{paths.map((path, index) => (
|
||||||
<path key={`svg_path_${index}`} d={path}/>
|
<path key={`svg_path_${index}`} d={path} />
|
||||||
))}
|
))}
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,9 +7,11 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const light = css<Props>`
|
const light = css<Props>`
|
||||||
${tw`bg-white border-neutral-200 text-neutral-800`};
|
${tw`bg-white border-neutral-200 text-neutral-800`};
|
||||||
&:focus { ${tw`border-primary-400`} }
|
&:focus {
|
||||||
|
${tw`border-primary-400`}
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
${tw`bg-neutral-100 border-neutral-200`};
|
${tw`bg-neutral-100 border-neutral-200`};
|
||||||
}
|
}
|
||||||
|
@ -40,43 +42,47 @@ const inputStyle = css<Props>`
|
||||||
${tw`appearance-none outline-none w-full min-w-0`};
|
${tw`appearance-none outline-none w-full min-w-0`};
|
||||||
${tw`p-3 border-2 rounded text-sm transition-all duration-150`};
|
${tw`p-3 border-2 rounded text-sm transition-all duration-150`};
|
||||||
${tw`bg-neutral-600 border-neutral-500 hover:border-neutral-400 text-neutral-200 shadow-none focus:ring-0`};
|
${tw`bg-neutral-600 border-neutral-500 hover:border-neutral-400 text-neutral-200 shadow-none focus:ring-0`};
|
||||||
|
|
||||||
& + .input-help {
|
& + .input-help {
|
||||||
${tw`mt-1 text-xs`};
|
${tw`mt-1 text-xs`};
|
||||||
${props => props.hasError ? tw`text-red-200` : tw`text-neutral-200`};
|
${(props) => (props.hasError ? tw`text-red-200` : tw`text-neutral-200`)};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:required, &:invalid {
|
&:required,
|
||||||
|
&:invalid {
|
||||||
${tw`shadow-none`};
|
${tw`shadow-none`};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled):not(:read-only):focus {
|
&:not(:disabled):not(:read-only):focus {
|
||||||
${tw`shadow-md border-primary-300 ring-2 ring-primary-400 ring-opacity-50`};
|
${tw`shadow-md border-primary-300 ring-2 ring-primary-400 ring-opacity-50`};
|
||||||
${props => props.hasError && tw`border-red-300 ring-red-200`};
|
${(props) => props.hasError && tw`border-red-300 ring-red-200`};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
${tw`opacity-75`};
|
${tw`opacity-75`};
|
||||||
}
|
}
|
||||||
|
|
||||||
${props => props.isLight && light};
|
${(props) => props.isLight && light};
|
||||||
${props => props.hasError && tw`text-red-100 border-red-400 hover:border-red-300`};
|
${(props) => props.hasError && tw`text-red-100 border-red-400 hover:border-red-300`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Input = styled.input<Props>`
|
const Input = styled.input<Props>`
|
||||||
&:not([type="checkbox"]):not([type="radio"]) {
|
&:not([type='checkbox']):not([type='radio']) {
|
||||||
${inputStyle};
|
${inputStyle};
|
||||||
}
|
}
|
||||||
|
|
||||||
&[type="checkbox"], &[type="radio"] {
|
&[type='checkbox'],
|
||||||
|
&[type='radio'] {
|
||||||
${checkboxStyle};
|
${checkboxStyle};
|
||||||
|
|
||||||
&[type="radio"] {
|
&[type='radio'] {
|
||||||
${tw`rounded-full`};
|
${tw`rounded-full`};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const Textarea = styled.textarea<Props>`${inputStyle}`;
|
const Textarea = styled.textarea<Props>`
|
||||||
|
${inputStyle}
|
||||||
|
`;
|
||||||
|
|
||||||
export { Textarea };
|
export { Textarea };
|
||||||
export default Input;
|
export default Input;
|
||||||
|
|
|
@ -10,19 +10,15 @@ interface Props {
|
||||||
children?: string | number | null | undefined;
|
children?: string | number | null | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputError = ({ errors, touched, name, children }: Props) => (
|
const InputError = ({ errors, touched, name, children }: Props) =>
|
||||||
touched[name] && errors[name] ?
|
touched[name] && errors[name] ? (
|
||||||
<p css={tw`text-xs text-red-400 pt-2`}>
|
<p css={tw`text-xs text-red-400 pt-2`}>
|
||||||
{typeof errors[name] === 'string' ?
|
{typeof errors[name] === 'string'
|
||||||
capitalize(errors[name] as string)
|
? capitalize(errors[name] as string)
|
||||||
:
|
: capitalize((errors[name] as unknown as string[])[0])}
|
||||||
capitalize((errors[name] as unknown as string[])[0])
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
:
|
) : (
|
||||||
<>
|
<>{children ? <p css={tw`text-xs text-neutral-400 pt-2`}>{children}</p> : null}</>
|
||||||
{children ? <p css={tw`text-xs text-neutral-400 pt-2`}>{children}</p> : null}
|
);
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default InputError;
|
export default InputError;
|
||||||
|
|
|
@ -7,19 +7,21 @@ import Select from '@/components/elements/Select';
|
||||||
|
|
||||||
const Container = styled.div<{ visible?: boolean }>`
|
const Container = styled.div<{ visible?: boolean }>`
|
||||||
${tw`relative`};
|
${tw`relative`};
|
||||||
|
|
||||||
${props => props.visible && css`
|
${(props) =>
|
||||||
& ${Select} {
|
props.visible &&
|
||||||
background-image: none;
|
css`
|
||||||
}
|
& ${Select} {
|
||||||
`};
|
background-image: none;
|
||||||
|
}
|
||||||
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const InputSpinner = ({ visible, children }: { visible: boolean, children: React.ReactNode }) => (
|
const InputSpinner = ({ visible, children }: { visible: boolean; children: React.ReactNode }) => (
|
||||||
<Container visible={visible}>
|
<Container visible={visible}>
|
||||||
<Fade appear unmountOnExit in={visible} timeout={150}>
|
<Fade appear unmountOnExit in={visible} timeout={150}>
|
||||||
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
|
<div css={tw`absolute right-0 h-full flex items-center justify-end pr-3`}>
|
||||||
<Spinner size={'small'}/>
|
<Spinner size={'small'} />
|
||||||
</div>
|
</div>
|
||||||
</Fade>
|
</Fade>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import tw from 'twin.macro';
|
||||||
|
|
||||||
const Label = styled.label<{ isLight?: boolean }>`
|
const Label = styled.label<{ isLight?: boolean }>`
|
||||||
${tw`block text-xs uppercase text-neutral-200 mb-1 sm:mb-2`};
|
${tw`block text-xs uppercase text-neutral-200 mb-1 sm:mb-2`};
|
||||||
${props => props.isLight && tw`text-neutral-700`};
|
${(props) => props.isLight && tw`text-neutral-700`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Label;
|
export default Label;
|
||||||
|
|
|
@ -22,41 +22,55 @@ export interface ModalProps extends RequiredModalProps {
|
||||||
|
|
||||||
export const ModalMask = styled.div`
|
export const ModalMask = styled.div`
|
||||||
${tw`fixed z-50 overflow-auto flex w-full inset-0`};
|
${tw`fixed z-50 overflow-auto flex w-full inset-0`};
|
||||||
background: rgba(0, 0, 0, 0.70);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ModalContainer = styled.div<{ alignTop?: boolean }>`
|
const ModalContainer = styled.div<{ alignTop?: boolean }>`
|
||||||
max-width: 95%;
|
max-width: 95%;
|
||||||
max-height: calc(100vh - 8rem);
|
max-height: calc(100vh - 8rem);
|
||||||
${breakpoint('md')`max-width: 75%`};
|
${breakpoint('md')`max-width: 75%`};
|
||||||
${breakpoint('lg')`max-width: 50%`};
|
${breakpoint('lg')`max-width: 50%`};
|
||||||
|
|
||||||
${tw`relative flex flex-col w-full m-auto`};
|
${tw`relative flex flex-col w-full m-auto`};
|
||||||
${props => props.alignTop && css`
|
${(props) =>
|
||||||
margin-top: 20%;
|
props.alignTop &&
|
||||||
${breakpoint('md')`margin-top: 10%`};
|
css`
|
||||||
`};
|
margin-top: 20%;
|
||||||
|
${breakpoint('md')`margin-top: 10%`};
|
||||||
|
`};
|
||||||
|
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
|
|
||||||
& > .close-icon {
|
& > .close-icon {
|
||||||
${tw`absolute right-0 p-2 text-white cursor-pointer opacity-50 transition-all duration-150 ease-linear hover:opacity-100`};
|
${tw`absolute right-0 p-2 text-white cursor-pointer opacity-50 transition-all duration-150 ease-linear hover:opacity-100`};
|
||||||
top: -2.5rem;
|
top: -2.5rem;
|
||||||
|
|
||||||
&:hover {${tw`transform rotate-90`}}
|
&:hover {
|
||||||
|
${tw`transform rotate-90`}
|
||||||
|
}
|
||||||
|
|
||||||
& > svg {
|
& > svg {
|
||||||
${tw`w-6 h-6`};
|
${tw`w-6 h-6`};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinnerOverlay, top = true, closeOnBackground = true, closeOnEscape = true, onDismissed, children }) => {
|
const Modal: React.FC<ModalProps> = ({
|
||||||
const [ render, setRender ] = useState(visible);
|
visible,
|
||||||
|
appear,
|
||||||
|
dismissable,
|
||||||
|
showSpinnerOverlay,
|
||||||
|
top = true,
|
||||||
|
closeOnBackground = true,
|
||||||
|
closeOnEscape = true,
|
||||||
|
onDismissed,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [render, setRender] = useState(visible);
|
||||||
|
|
||||||
const isDismissable = useMemo(() => {
|
const isDismissable = useMemo(() => {
|
||||||
return (dismissable || true) && !(showSpinnerOverlay || false);
|
return (dismissable || true) && !(showSpinnerOverlay || false);
|
||||||
}, [ dismissable, showSpinnerOverlay ]);
|
}, [dismissable, showSpinnerOverlay]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDismissable || !closeOnEscape) return;
|
if (!isDismissable || !closeOnEscape) return;
|
||||||
|
@ -69,22 +83,16 @@ const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinner
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handler);
|
window.removeEventListener('keydown', handler);
|
||||||
};
|
};
|
||||||
}, [ isDismissable, closeOnEscape, render ]);
|
}, [isDismissable, closeOnEscape, render]);
|
||||||
|
|
||||||
useEffect(() => setRender(visible), [ visible ]);
|
useEffect(() => setRender(visible), [visible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fade
|
<Fade in={render} timeout={150} appear={appear || true} unmountOnExit onExited={() => onDismissed()}>
|
||||||
in={render}
|
|
||||||
timeout={150}
|
|
||||||
appear={appear || true}
|
|
||||||
unmountOnExit
|
|
||||||
onExited={() => onDismissed()}
|
|
||||||
>
|
|
||||||
<ModalMask
|
<ModalMask
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onContextMenu={e => e.stopPropagation()}
|
onContextMenu={(e) => e.stopPropagation()}
|
||||||
onMouseDown={e => {
|
onMouseDown={(e) => {
|
||||||
if (isDismissable && closeOnBackground) {
|
if (isDismissable && closeOnBackground) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
|
@ -94,29 +102,36 @@ const Modal: React.FC<ModalProps> = ({ visible, appear, dismissable, showSpinner
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContainer alignTop={top}>
|
<ModalContainer alignTop={top}>
|
||||||
{isDismissable &&
|
{isDismissable && (
|
||||||
<div className={'close-icon'} onClick={() => setRender(false)}>
|
<div className={'close-icon'} onClick={() => setRender(false)}>
|
||||||
<svg xmlns={'http://www.w3.org/2000/svg'} fill={'none'} viewBox={'0 0 24 24'} stroke={'currentColor'}>
|
<svg
|
||||||
<path
|
xmlns={'http://www.w3.org/2000/svg'}
|
||||||
strokeLinecap={'round'}
|
fill={'none'}
|
||||||
strokeLinejoin={'round'}
|
viewBox={'0 0 24 24'}
|
||||||
strokeWidth={'2'}
|
stroke={'currentColor'}
|
||||||
d={'M6 18L18 6M6 6l12 12'}
|
>
|
||||||
/>
|
<path
|
||||||
</svg>
|
strokeLinecap={'round'}
|
||||||
</div>
|
strokeLinejoin={'round'}
|
||||||
}
|
strokeWidth={'2'}
|
||||||
{showSpinnerOverlay &&
|
d={'M6 18L18 6M6 6l12 12'}
|
||||||
<Fade timeout={150} appear in>
|
/>
|
||||||
<div
|
</svg>
|
||||||
css={tw`absolute w-full h-full rounded flex items-center justify-center`}
|
|
||||||
style={{ background: 'hsla(211, 10%, 53%, 0.35)', zIndex: 9999 }}
|
|
||||||
>
|
|
||||||
<Spinner/>
|
|
||||||
</div>
|
</div>
|
||||||
</Fade>
|
)}
|
||||||
}
|
{showSpinnerOverlay && (
|
||||||
<div css={tw`bg-neutral-800 p-3 sm:p-4 md:p-6 rounded shadow-md overflow-y-scroll transition-all duration-150`}>
|
<Fade timeout={150} appear in>
|
||||||
|
<div
|
||||||
|
css={tw`absolute w-full h-full rounded flex items-center justify-center`}
|
||||||
|
style={{ background: 'hsla(211, 10%, 53%, 0.35)', zIndex: 9999 }}
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
</Fade>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
css={tw`bg-neutral-800 p-3 sm:p-4 md:p-6 rounded shadow-md overflow-y-scroll transition-all duration-150`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
|
|
|
@ -15,15 +15,13 @@ const PageContentBlock: React.FC<PageContentBlockProps> = ({ title, showFlashKey
|
||||||
if (title) {
|
if (title) {
|
||||||
document.title = title;
|
document.title = title;
|
||||||
}
|
}
|
||||||
}, [ title ]);
|
}, [title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CSSTransition timeout={150} classNames={'fade'} appear in>
|
<CSSTransition timeout={150} classNames={'fade'} appear in>
|
||||||
<>
|
<>
|
||||||
<ContentContainer css={tw`my-4 sm:my-10`} className={className}>
|
<ContentContainer css={tw`my-4 sm:my-10`} className={className}>
|
||||||
{showFlashKey &&
|
{showFlashKey && <FlashMessageRender byKey={showFlashKey} css={tw`mb-4`} />}
|
||||||
<FlashMessageRender byKey={showFlashKey} css={tw`mb-4`}/>
|
|
||||||
}
|
|
||||||
{children}
|
{children}
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
<ContentContainer css={tw`mb-4`}>
|
<ContentContainer css={tw`mb-4`}>
|
||||||
|
@ -36,7 +34,7 @@ const PageContentBlock: React.FC<PageContentBlockProps> = ({ title, showFlashKey
|
||||||
>
|
>
|
||||||
Pterodactyl®
|
Pterodactyl®
|
||||||
</a>
|
</a>
|
||||||
© 2015 - {(new Date()).getFullYear()}
|
© 2015 - {new Date().getFullYear()}
|
||||||
</p>
|
</p>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -22,13 +22,13 @@ interface Props<T> {
|
||||||
|
|
||||||
const Block = styled(Button)`
|
const Block = styled(Button)`
|
||||||
${tw`p-0 w-10 h-10`}
|
${tw`p-0 w-10 h-10`}
|
||||||
|
|
||||||
&:not(:last-of-type) {
|
&:not(:last-of-type) {
|
||||||
${tw`mr-2`};
|
${tw`mr-2`};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Pagination<T> ({ data: { items, pagination }, onPageSelect, children }: Props<T>) {
|
function Pagination<T>({ data: { items, pagination }, onPageSelect, children }: Props<T>) {
|
||||||
const isFirstPage = pagination.currentPage === 1;
|
const isFirstPage = pagination.currentPage === 1;
|
||||||
const isLastPage = pagination.currentPage >= pagination.totalPages;
|
const isLastPage = pagination.currentPage >= pagination.totalPages;
|
||||||
|
|
||||||
|
@ -46,19 +46,14 @@ function Pagination<T> ({ data: { items, pagination }, onPageSelect, children }:
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children({ items, isFirstPage, isLastPage })}
|
{children({ items, isFirstPage, isLastPage })}
|
||||||
{(pages.length > 1) &&
|
{pages.length > 1 && (
|
||||||
<div css={tw`mt-4 flex justify-center`}>
|
<div css={tw`mt-4 flex justify-center`}>
|
||||||
{(pages[0] > 1 && !isFirstPage) &&
|
{pages[0] > 1 && !isFirstPage && (
|
||||||
<Block
|
<Block isSecondary color={'primary'} onClick={() => onPageSelect(1)}>
|
||||||
isSecondary
|
<FontAwesomeIcon icon={faAngleDoubleLeft} />
|
||||||
color={'primary'}
|
</Block>
|
||||||
onClick={() => onPageSelect(1)}
|
)}
|
||||||
>
|
{pages.map((i) => (
|
||||||
<FontAwesomeIcon icon={faAngleDoubleLeft}/>
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
pages.map(i => (
|
|
||||||
<Block
|
<Block
|
||||||
isSecondary={pagination.currentPage !== i}
|
isSecondary={pagination.currentPage !== i}
|
||||||
color={'primary'}
|
color={'primary'}
|
||||||
|
@ -67,19 +62,14 @@ function Pagination<T> ({ data: { items, pagination }, onPageSelect, children }:
|
||||||
>
|
>
|
||||||
{i}
|
{i}
|
||||||
</Block>
|
</Block>
|
||||||
))
|
))}
|
||||||
}
|
{pages[4] < pagination.totalPages && !isLastPage && (
|
||||||
{(pages[4] < pagination.totalPages && !isLastPage) &&
|
<Block isSecondary color={'primary'} onClick={() => onPageSelect(pagination.totalPages)}>
|
||||||
<Block
|
<FontAwesomeIcon icon={faAngleDoubleRight} />
|
||||||
isSecondary
|
</Block>
|
||||||
color={'primary'}
|
)}
|
||||||
onClick={() => onPageSelect(pagination.totalPages)}
|
</div>
|
||||||
>
|
)}
|
||||||
<FontAwesomeIcon icon={faAngleDoubleRight}/>
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,20 +11,17 @@ interface Props extends Omit<RouteProps, 'path'> {
|
||||||
|
|
||||||
export default ({ permission, children, ...props }: Props) => (
|
export default ({ permission, children, ...props }: Props) => (
|
||||||
<Route {...props}>
|
<Route {...props}>
|
||||||
{!permission ?
|
{!permission ? (
|
||||||
children
|
children
|
||||||
:
|
) : (
|
||||||
<Can
|
<Can
|
||||||
action={permission}
|
action={permission}
|
||||||
renderOnError={
|
renderOnError={
|
||||||
<ServerError
|
<ServerError title={'Access Denied'} message={'You do not have permission to access this page.'} />
|
||||||
title={'Access Denied'}
|
|
||||||
message={'You do not have permission to access this page.'}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Can>
|
</Can>
|
||||||
}
|
)}
|
||||||
</Route>
|
</Route>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,10 +14,10 @@ const BarFill = styled.div`
|
||||||
export default () => {
|
export default () => {
|
||||||
const interval = useRef<number>(null);
|
const interval = useRef<number>(null);
|
||||||
const timeout = useRef<number>(null);
|
const timeout = useRef<number>(null);
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const progress = useStoreState(state => state.progress.progress);
|
const progress = useStoreState((state) => state.progress.progress);
|
||||||
const continuous = useStoreState(state => state.progress.continuous);
|
const continuous = useStoreState((state) => state.progress.continuous);
|
||||||
const setProgress = useStoreActions(actions => actions.progress.setProgress);
|
const setProgress = useStoreActions((actions) => actions.progress.setProgress);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -33,7 +33,7 @@ export default () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
timeout.current = setTimeout(() => setProgress(undefined), 500);
|
timeout.current = setTimeout(() => setProgress(undefined), 500);
|
||||||
}
|
}
|
||||||
}, [ progress ]);
|
}, [progress]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!continuous) {
|
if (!continuous) {
|
||||||
|
@ -44,7 +44,7 @@ export default () => {
|
||||||
if (!progress || progress === 0) {
|
if (!progress || progress === 0) {
|
||||||
setProgress(randomInt(20, 30));
|
setProgress(randomInt(20, 30));
|
||||||
}
|
}
|
||||||
}, [ continuous ]);
|
}, [continuous]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (continuous) {
|
if (continuous) {
|
||||||
|
@ -56,18 +56,12 @@ export default () => {
|
||||||
interval.current = setTimeout(() => setProgress(progress + randomInt(1, 5)), 500);
|
interval.current = setTimeout(() => setProgress(progress + randomInt(1, 5)), 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [ progress, continuous ]);
|
}, [progress, continuous]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div css={tw`w-full fixed`} style={{ height: '2px' }}>
|
<div css={tw`w-full fixed`} style={{ height: '2px' }}>
|
||||||
<CSSTransition
|
<CSSTransition timeout={150} appear in={visible} unmountOnExit classNames={'fade'}>
|
||||||
timeout={150}
|
<BarFill style={{ width: progress === undefined ? '100%' : `${progress}%` }} />
|
||||||
appear
|
|
||||||
in={visible}
|
|
||||||
unmountOnExit
|
|
||||||
classNames={'fade'}
|
|
||||||
>
|
|
||||||
<BarFill style={{ width: progress === undefined ? '100%' : `${progress}%` }}/>
|
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -43,22 +43,22 @@ const ActionButton = styled(Button)`
|
||||||
const ScreenBlock = ({ title, image, message, onBack, onRetry }: ScreenBlockProps) => (
|
const ScreenBlock = ({ title, image, message, onBack, onRetry }: ScreenBlockProps) => (
|
||||||
<PageContentBlock>
|
<PageContentBlock>
|
||||||
<div css={tw`flex justify-center`}>
|
<div css={tw`flex justify-center`}>
|
||||||
<div css={tw`w-full sm:w-3/4 md:w-1/2 p-12 md:p-20 bg-neutral-100 rounded-lg shadow-lg text-center relative`}>
|
<div
|
||||||
{(typeof onBack === 'function' || typeof onRetry === 'function') &&
|
css={tw`w-full sm:w-3/4 md:w-1/2 p-12 md:p-20 bg-neutral-100 rounded-lg shadow-lg text-center relative`}
|
||||||
<div css={tw`absolute left-0 top-0 ml-4 mt-4`}>
|
>
|
||||||
<ActionButton
|
{(typeof onBack === 'function' || typeof onRetry === 'function') && (
|
||||||
onClick={() => onRetry ? onRetry() : (onBack ? onBack() : null)}
|
<div css={tw`absolute left-0 top-0 ml-4 mt-4`}>
|
||||||
className={onRetry ? 'hover:spin' : undefined}
|
<ActionButton
|
||||||
>
|
onClick={() => (onRetry ? onRetry() : onBack ? onBack() : null)}
|
||||||
<FontAwesomeIcon icon={onRetry ? faSyncAlt : faArrowLeft}/>
|
className={onRetry ? 'hover:spin' : undefined}
|
||||||
</ActionButton>
|
>
|
||||||
</div>
|
<FontAwesomeIcon icon={onRetry ? faSyncAlt : faArrowLeft} />
|
||||||
}
|
</ActionButton>
|
||||||
<img src={image} css={tw`w-2/3 h-auto select-none mx-auto`}/>
|
</div>
|
||||||
|
)}
|
||||||
|
<img src={image} css={tw`w-2/3 h-auto select-none mx-auto`} />
|
||||||
<h2 css={tw`mt-10 text-neutral-900 font-bold text-4xl`}>{title}</h2>
|
<h2 css={tw`mt-10 text-neutral-900 font-bold text-4xl`}>{title}</h2>
|
||||||
<p css={tw`text-sm text-neutral-700 mt-2`}>
|
<p css={tw`text-sm text-neutral-700 mt-2`}>{message}</p>
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
|
@ -66,10 +66,10 @@ const ScreenBlock = ({ title, image, message, onBack, onRetry }: ScreenBlockProp
|
||||||
|
|
||||||
type ServerErrorProps = (Omit<PropsWithBack, 'image' | 'title'> | Omit<PropsWithRetry, 'image' | 'title'>) & {
|
type ServerErrorProps = (Omit<PropsWithBack, 'image' | 'title'> | Omit<PropsWithRetry, 'image' | 'title'>) & {
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ServerError = ({ title, ...props }: ServerErrorProps) => (
|
const ServerError = ({ title, ...props }: ServerErrorProps) => (
|
||||||
<ScreenBlock title={title || 'Something went wrong'} image={ServerErrorSvg} {...props}/>
|
<ScreenBlock title={title || 'Something went wrong'} image={ServerErrorSvg} {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const NotFound = ({ title, message, onBack }: Partial<Pick<ScreenBlockProps, 'title' | 'message' | 'onBack'>>) => (
|
const NotFound = ({ title, message, onBack }: Partial<Pick<ScreenBlockProps, 'title' | 'message' | 'onBack'>>) => (
|
||||||
|
|
|
@ -8,7 +8,9 @@ interface Props {
|
||||||
const Select = styled.select<Props>`
|
const Select = styled.select<Props>`
|
||||||
${tw`shadow-none block p-3 pr-8 rounded border w-full text-sm transition-colors duration-150 ease-linear`};
|
${tw`shadow-none block p-3 pr-8 rounded border w-full text-sm transition-colors duration-150 ease-linear`};
|
||||||
|
|
||||||
&, &:hover:not(:disabled), &:focus {
|
&,
|
||||||
|
&:hover:not(:disabled),
|
||||||
|
&:focus {
|
||||||
${tw`outline-none`};
|
${tw`outline-none`};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,15 +24,18 @@ const Select = styled.select<Props>`
|
||||||
&::-ms-expand {
|
&::-ms-expand {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
${props => !props.hideDropdownArrow && css`
|
${(props) =>
|
||||||
${tw`bg-neutral-600 border-neutral-500 text-neutral-200`};
|
!props.hideDropdownArrow &&
|
||||||
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 ");
|
css`
|
||||||
|
${tw`bg-neutral-600 border-neutral-500 text-neutral-200`};
|
||||||
&:hover:not(:disabled), &:focus {
|
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 ");
|
||||||
${tw`border-neutral-400`};
|
|
||||||
}
|
&:hover:not(:disabled),
|
||||||
`};
|
&:focus {
|
||||||
|
${tw`border-neutral-400`};
|
||||||
|
}
|
||||||
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default Select;
|
export default Select;
|
||||||
|
|
|
@ -7,7 +7,7 @@ interface Props extends PageContentBlockProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServerContentBlock: React.FC<Props> = ({ title, children, ...props }) => {
|
const ServerContentBlock: React.FC<Props> = ({ title, children, ...props }) => {
|
||||||
const name = ServerContext.useStoreState(state => state.server.data!.name);
|
const name = ServerContext.useStoreState((state) => state.server.data!.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={`${name} | ${title}`} {...props}>
|
<PageContentBlock title={`${name} | ${title}`} {...props}>
|
||||||
|
|
|
@ -25,30 +25,30 @@ const SpinnerComponent = styled.div<Props>`
|
||||||
${tw`w-8 h-8`};
|
${tw`w-8 h-8`};
|
||||||
border-width: 3px;
|
border-width: 3px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: ${spin} 1s cubic-bezier(0.55, 0.25, 0.25, 0.70) infinite;
|
animation: ${spin} 1s cubic-bezier(0.55, 0.25, 0.25, 0.7) infinite;
|
||||||
|
|
||||||
${props => props.size === 'small' ? tw`w-4 h-4 border-2` : (props.size === 'large' ? css`
|
${(props) =>
|
||||||
${tw`w-16 h-16`};
|
props.size === 'small'
|
||||||
border-width: 6px;
|
? tw`w-4 h-4 border-2`
|
||||||
` : null)};
|
: props.size === 'large'
|
||||||
|
? css`
|
||||||
|
${tw`w-16 h-16`};
|
||||||
|
border-width: 6px;
|
||||||
|
`
|
||||||
|
: null};
|
||||||
|
|
||||||
border-color: ${props => !props.isBlue ? 'rgba(255, 255, 255, 0.2)' : 'hsla(212, 92%, 43%, 0.2)'};
|
border-color: ${(props) => (!props.isBlue ? 'rgba(255, 255, 255, 0.2)' : 'hsla(212, 92%, 43%, 0.2)')};
|
||||||
border-top-color: ${props => !props.isBlue ? 'rgb(255, 255, 255)' : 'hsl(212, 92%, 43%)'};
|
border-top-color: ${(props) => (!props.isBlue ? 'rgb(255, 255, 255)' : 'hsl(212, 92%, 43%)')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Spinner: Spinner = ({ centered, ...props }) => (
|
const Spinner: Spinner = ({ centered, ...props }) =>
|
||||||
centered ?
|
centered ? (
|
||||||
<div
|
<div css={[tw`flex justify-center items-center`, props.size === 'large' ? tw`m-20` : tw`m-6`]}>
|
||||||
css={[
|
<SpinnerComponent {...props} />
|
||||||
tw`flex justify-center items-center`,
|
|
||||||
props.size === 'large' ? tw`m-20` : tw`m-6`,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<SpinnerComponent {...props}/>
|
|
||||||
</div>
|
</div>
|
||||||
:
|
) : (
|
||||||
<SpinnerComponent {...props}/>
|
<SpinnerComponent {...props} />
|
||||||
);
|
);
|
||||||
Spinner.displayName = 'Spinner';
|
Spinner.displayName = 'Spinner';
|
||||||
|
|
||||||
Spinner.Size = {
|
Spinner.Size = {
|
||||||
|
@ -58,10 +58,8 @@ Spinner.Size = {
|
||||||
};
|
};
|
||||||
|
|
||||||
Spinner.Suspense = ({ children, centered = true, size = Spinner.Size.LARGE, ...props }) => (
|
Spinner.Suspense = ({ children, centered = true, size = Spinner.Size.LARGE, ...props }) => (
|
||||||
<Suspense fallback={<Spinner centered={centered} size={size} {...props}/>}>
|
<Suspense fallback={<Spinner centered={centered} size={size} {...props} />}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
{children}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
Spinner.Suspense.displayName = 'Spinner.Suspense';
|
Spinner.Suspense.displayName = 'Spinner.Suspense';
|
||||||
|
|
|
@ -19,7 +19,7 @@ const SpinnerOverlay: React.FC<Props> = ({ size, fixed, visible, backgroundOpaci
|
||||||
]}
|
]}
|
||||||
style={{ background: `rgba(0, 0, 0, ${backgroundOpacity || 0.45})` }}
|
style={{ background: `rgba(0, 0, 0, ${backgroundOpacity || 0.45})` }}
|
||||||
>
|
>
|
||||||
<Spinner size={size}/>
|
<Spinner size={size} />
|
||||||
{children && (typeof children === 'string' ? <p css={tw`mt-4 text-neutral-400`}>{children}</p> : children)}
|
{children && (typeof children === 'string' ? <p css={tw`mt-4 text-neutral-400`}>{children}</p> : children)}
|
||||||
</div>
|
</div>
|
||||||
</Fade>
|
</Fade>
|
||||||
|
|
|
@ -8,7 +8,8 @@ const SubNavigation = styled.div`
|
||||||
${tw`flex items-center text-sm mx-auto px-2`};
|
${tw`flex items-center text-sm mx-auto px-2`};
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
||||||
& > a, & > div {
|
& > a,
|
||||||
|
& > div {
|
||||||
${tw`inline-block py-3 px-4 text-neutral-300 no-underline whitespace-nowrap transition-all duration-150`};
|
${tw`inline-block py-3 px-4 text-neutral-300 no-underline whitespace-nowrap transition-all duration-150`};
|
||||||
|
|
||||||
&:not(:first-of-type) {
|
&:not(:first-of-type) {
|
||||||
|
@ -19,7 +20,8 @@ const SubNavigation = styled.div`
|
||||||
${tw`text-neutral-100`};
|
${tw`text-neutral-100`};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active, &.active {
|
&:active,
|
||||||
|
&.active {
|
||||||
${tw`text-neutral-100`};
|
${tw`text-neutral-100`};
|
||||||
box-shadow: inset 0 -2px ${theme`colors.cyan.600`.toString()};
|
box-shadow: inset 0 -2px ${theme`colors.cyan.600`.toString()};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Input from '@/components/elements/Input';
|
||||||
const ToggleContainer = styled.div`
|
const ToggleContainer = styled.div`
|
||||||
${tw`relative select-none w-12 leading-normal`};
|
${tw`relative select-none w-12 leading-normal`};
|
||||||
|
|
||||||
& > input[type="checkbox"] {
|
& > input[type='checkbox'] {
|
||||||
${tw`hidden`};
|
${tw`hidden`};
|
||||||
|
|
||||||
&:checked + label {
|
&:checked + label {
|
||||||
|
@ -30,7 +30,7 @@ const ToggleContainer = styled.div`
|
||||||
right: calc(50% + 0.125rem);
|
right: calc(50% + 0.125rem);
|
||||||
//width: 1.25rem;
|
//width: 1.25rem;
|
||||||
//height: 1.25rem;
|
//height: 1.25rem;
|
||||||
content: "";
|
content: '';
|
||||||
transition: all 75ms ease-in;
|
transition: all 75ms ease-in;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,35 +52,28 @@ const Switch = ({ name, label, description, defaultChecked, readOnly, onChange,
|
||||||
return (
|
return (
|
||||||
<div css={tw`flex items-center`}>
|
<div css={tw`flex items-center`}>
|
||||||
<ToggleContainer css={tw`flex-none`}>
|
<ToggleContainer css={tw`flex-none`}>
|
||||||
{children
|
{children || (
|
||||||
|| <Input
|
<Input
|
||||||
id={uuid}
|
id={uuid}
|
||||||
name={name}
|
name={name}
|
||||||
type={'checkbox'}
|
type={'checkbox'}
|
||||||
onChange={e => onChange && onChange(e)}
|
onChange={(e) => onChange && onChange(e)}
|
||||||
defaultChecked={defaultChecked}
|
defaultChecked={defaultChecked}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<Label htmlFor={uuid}/>
|
<Label htmlFor={uuid} />
|
||||||
</ToggleContainer>
|
</ToggleContainer>
|
||||||
{(label || description) &&
|
{(label || description) && (
|
||||||
<div css={tw`ml-4 w-full`}>
|
<div css={tw`ml-4 w-full`}>
|
||||||
{label &&
|
{label && (
|
||||||
<Label
|
<Label css={[tw`cursor-pointer`, !!description && tw`mb-0`]} htmlFor={uuid}>
|
||||||
css={[ tw`cursor-pointer`, !!description && tw`mb-0` ]}
|
{label}
|
||||||
htmlFor={uuid}
|
</Label>
|
||||||
>
|
)}
|
||||||
{label}
|
{description && <p css={tw`text-neutral-400 text-sm mt-2`}>{description}</p>}
|
||||||
</Label>
|
</div>
|
||||||
}
|
)}
|
||||||
{description &&
|
|
||||||
<p css={tw`text-neutral-400 text-sm mt-2`}>
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,17 +14,16 @@ interface Props {
|
||||||
const TitledGreyBox = ({ icon, title, children, className }: Props) => (
|
const TitledGreyBox = ({ icon, title, children, className }: Props) => (
|
||||||
<div css={tw`rounded shadow-md bg-neutral-700`} className={className}>
|
<div css={tw`rounded shadow-md bg-neutral-700`} className={className}>
|
||||||
<div css={tw`bg-neutral-900 rounded-t p-3 border-b border-black`}>
|
<div css={tw`bg-neutral-900 rounded-t p-3 border-b border-black`}>
|
||||||
{typeof title === 'string' ?
|
{typeof title === 'string' ? (
|
||||||
<p css={tw`text-sm uppercase`}>
|
<p css={tw`text-sm uppercase`}>
|
||||||
{icon && <FontAwesomeIcon icon={icon} css={tw`mr-2 text-neutral-300`}/>}{title}
|
{icon && <FontAwesomeIcon icon={icon} css={tw`mr-2 text-neutral-300`} />}
|
||||||
|
{title}
|
||||||
</p>
|
</p>
|
||||||
:
|
) : (
|
||||||
title
|
title
|
||||||
}
|
)}
|
||||||
</div>
|
|
||||||
<div css={tw`p-3`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div css={tw`p-3`}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,12 @@ const formatProperties = (properties: Record<string, unknown>): Record<string, u
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
[key]: isCount || typeof value !== 'string'
|
[key]:
|
||||||
? (isObject(value) ? formatProperties(value) : value)
|
isCount || typeof value !== 'string'
|
||||||
: `<strong>${value}</strong>`,
|
? isObject(value)
|
||||||
|
? formatProperties(value)
|
||||||
|
: value
|
||||||
|
: `<strong>${value}</strong>`,
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
@ -58,16 +61,18 @@ export default ({ activity, children }: Props) => {
|
||||||
{activity.event}
|
{activity.event}
|
||||||
</Link>
|
</Link>
|
||||||
<div className={classNames(style.icons, 'group-hover:text-gray-300')}>
|
<div className={classNames(style.icons, 'group-hover:text-gray-300')}>
|
||||||
{activity.isApi &&
|
{activity.isApi && (
|
||||||
<Tooltip placement={'top'} content={'Performed using API Key'}>
|
<Tooltip placement={'top'} content={'Performed using API Key'}>
|
||||||
<span><TerminalIcon/></span>
|
<span>
|
||||||
|
<TerminalIcon />
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className={style.description}>
|
<p className={style.description}>
|
||||||
<Translate ns={'activity'} values={properties} i18nKey={activity.event.replace(':', '.')}/>
|
<Translate ns={'activity'} values={properties} i18nKey={activity.event.replace(':', '.')} />
|
||||||
</p>
|
</p>
|
||||||
<div className={'mt-1 flex items-center text-sm'}>
|
<div className={'mt-1 flex items-center text-sm'}>
|
||||||
<Link
|
<Link
|
||||||
|
@ -77,17 +82,12 @@ export default ({ activity, children }: Props) => {
|
||||||
{activity.ip}
|
{activity.ip}
|
||||||
</Link>
|
</Link>
|
||||||
<span className={'text-gray-400'}> | </span>
|
<span className={'text-gray-400'}> | </span>
|
||||||
<Tooltip
|
<Tooltip placement={'right'} content={format(activity.timestamp, 'MMM do, yyyy H:mm:ss')}>
|
||||||
placement={'right'}
|
<span>{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}</span>
|
||||||
content={format(activity.timestamp, 'MMM do, yyyy H:mm:ss')}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{formatDistanceToNowStrict(activity.timestamp, { addSuffix: true })}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{activity.hasAdditionalMetadata && <ActivityLogMetaButton meta={activity.properties}/>}
|
{activity.hasAdditionalMetadata && <ActivityLogMetaButton meta={activity.properties} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue