refactor: replace old modals with new dialog component

This commit is contained in:
Nicolas Meienberger 2023-04-07 13:07:53 +02:00
parent 67b9c43ae1
commit e9590f8806
19 changed files with 470 additions and 277 deletions

View file

@ -1,5 +1,5 @@
module.exports = {
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsdoc', 'import'],
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsdoc'],
extends: [
'plugin:@typescript-eslint/recommended',
'next/core-web-vitals',

View file

@ -31,6 +31,8 @@
"dependencies": {
"@hookform/resolvers": "^2.9.10",
"@prisma/client": "^4.11.0",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-switch": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.3",
"@runtipi/postgres-migrations": "^5.3.0",
"@tabler/core": "1.0.0-beta17",

236
pnpm-lock.yaml generated
View file

@ -7,6 +7,12 @@ dependencies:
'@prisma/client':
specifier: ^4.11.0
version: 4.11.0(prisma@4.11.0)
'@radix-ui/react-dialog':
specifier: ^1.0.3
version: 1.0.3(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-switch':
specifier: ^1.0.2
version: 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tabs':
specifier: ^1.0.3
version: 1.0.3(react-dom@18.2.0)(react@18.2.0)
@ -1558,6 +1564,33 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-dialog@1.0.3(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-owNhq36kNPqC2/a+zJRioPg6HHnTn5B/sh/NjTY8r4W9g1L5VJlrzZIVcBr7R9Mg8iLjVmh6MGgMlfoVf/WO/A==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.3(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.0(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-portal': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.1(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.0.28)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-direction@1.0.0(react@18.2.0):
resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==}
peerDependencies:
@ -1567,6 +1600,45 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-dismissable-layer@1.0.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-use-escape-keydown': 1.0.2(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-focus-guards@1.0.0(react@18.2.0):
resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
react: 18.2.0
dev: false
/@radix-ui/react-focus-scope@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-spwXlNTfeIprt+kaEWE/qYuYT3ZAqJiAGjN/JgdvgVDTu8yc+HuX+WOWXrKliKnLnwck0F6JDkqIERncnih+4A==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-id@1.0.0(react@18.2.0):
resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==}
peerDependencies:
@ -1577,6 +1649,18 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-portal@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==}
peerDependencies:
@ -1632,6 +1716,24 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-switch@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-BcG/LKehxt36NXG0wPnoCitIfSMtU9Xo7BmythYA1PAMLtsMvW7kALfBzmduQoHTWcKr0AVcFyh0gChBUp9TiQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.0(react@18.2.0)
'@radix-ui/react-use-size': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-tabs@1.0.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-4CkF/Rx1GcrusI/JZ1Rvyx4okGUs6wEenWA0RG/N+CwkRhTy7t54y7BLsWUXrAz/GRbBfHQg/Odfs/RoW0CiRA==}
peerDependencies:
@ -1670,6 +1772,16 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-escape-keydown@1.0.2(react@18.2.0):
resolution: {integrity: sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
/@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0):
resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==}
peerDependencies:
@ -1679,6 +1791,25 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-previous@1.0.0(react@18.2.0):
resolution: {integrity: sha512-RG2K8z/K7InnOKpq6YLDmT49HGjNmrK+fr82UCVKT2sW0GYfVnYp4wZWBooT/EYfQ5faA9uIjvsuMMhH61rheg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
react: 18.2.0
dev: false
/@radix-ui/react-use-size@1.0.0(react@18.2.0):
resolution: {integrity: sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
/@redis/bloom@1.2.0(@redis/client@1.5.6):
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies:
@ -2616,6 +2747,13 @@ packages:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
/aria-hidden@1.2.3:
resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==}
engines: {node: '>=10'}
dependencies:
tslib: 2.5.0
dev: false
/aria-query@5.1.3:
resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
dependencies:
@ -3370,6 +3508,10 @@ packages:
engines: {node: '>=8'}
dev: true
/detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dev: false
/diff-sequences@29.4.3:
resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -4401,6 +4543,11 @@ packages:
has: 1.0.3
has-symbols: 1.0.3
/get-nonce@1.0.1:
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
engines: {node: '>=6'}
dev: false
/get-package-type@0.1.0:
resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
engines: {node: '>=8.0.0'}
@ -4735,6 +4882,12 @@ packages:
side-channel: 1.0.4
dev: true
/invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
dependencies:
loose-envify: 1.4.0
dev: false
/ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@ -7059,6 +7212,41 @@ packages:
- supports-color
dev: false
/react-remove-scroll-bar@2.3.4(@types/react@18.0.28)(react@18.2.0):
resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.28
react: 18.2.0
react-style-singleton: 2.2.1(@types/react@18.0.28)(react@18.2.0)
tslib: 2.5.0
dev: false
/react-remove-scroll@2.5.5(@types/react@18.0.28)(react@18.2.0):
resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.28
react: 18.2.0
react-remove-scroll-bar: 2.3.4(@types/react@18.0.28)(react@18.2.0)
react-style-singleton: 2.2.1(@types/react@18.0.28)(react@18.2.0)
tslib: 2.5.0
use-callback-ref: 1.3.0(@types/react@18.0.28)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.0.28)(react@18.2.0)
dev: false
/react-select@5.7.0(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-lJGiMxCa3cqnUr2Jjtg9YHsaytiZqeNOKeibv6WF5zbK/fPegZ1hg3y/9P1RZVLhqBTs0PfqQLKuAACednYGhQ==}
peerDependencies:
@ -7088,6 +7276,23 @@ packages:
react: 18.2.0
dev: false
/react-style-singleton@2.2.1(@types/react@18.0.28)(react@18.2.0):
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.28
get-nonce: 1.0.1
invariant: 2.2.4
react: 18.2.0
tslib: 2.5.0
dev: false
/react-tooltip@4.5.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Zo+CSFUGXar1uV+bgXFFDe7VeS2iByeIp5rTgTcc2HqtuOS5D76QapejNNfx320MCY91TlhTQat36KGFTqgcvw==}
engines: {npm: '>=6.13'}
@ -8107,6 +8312,21 @@ packages:
requires-port: 1.0.0
dev: true
/use-callback-ref@1.3.0(@types/react@18.0.28)(react@18.2.0):
resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.28
react: 18.2.0
tslib: 2.5.0
dev: false
/use-isomorphic-layout-effect@1.1.2(@types/react@18.0.28)(react@18.2.0):
resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
peerDependencies:
@ -8120,6 +8340,22 @@ packages:
react: 18.2.0
dev: false
/use-sidecar@1.1.2(@types/react@18.0.28)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.28
detect-node-es: 1.1.0
react: 18.2.0
tslib: 2.5.0
dev: false
/use-sync-external-store@1.2.0(react@18.2.0):
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:

View file

@ -13,70 +13,91 @@ describe('Test: AppActions', () => {
exposable: [],
} as unknown as AppInfo;
it('should render the correct buttons when app status is stopped', () => {
// Arrange
it('should call the callbacks when buttons are clicked', () => {
// arrange
const onStart = jest.fn();
const onRemove = jest.fn();
// @ts-expect-error
const { getByText } = render(<AppActions status="stopped" info={app} onStart={onStart} onUninstall={onRemove} />);
render(<AppActions status="stopped" info={app} onStart={onStart} onUninstall={onRemove} />);
// Act
fireEvent.click(getByText('Start'));
fireEvent.click(getByText('Remove'));
// act
const startButton = screen.getByRole('button', { name: 'Start' });
fireEvent.click(startButton);
const removeButton = screen.getByText('Remove');
fireEvent.click(removeButton);
// Assert
expect(getByText('Start')).toBeInTheDocument();
expect(getByText('Remove')).toBeInTheDocument();
// assert
expect(onStart).toHaveBeenCalled();
expect(onRemove).toHaveBeenCalled();
});
it('should render the correct buttons when app status is running', () => {
// arrange
// @ts-expect-error
const { getByText } = render(<AppActions status="running" info={app} />);
expect(getByText('Stop')).toBeInTheDocument();
expect(getByText('Open')).toBeInTheDocument();
expect(getByText('Settings')).toBeInTheDocument();
render(<AppActions status="running" info={app} />);
// assert
expect(screen.getByRole('button', { name: 'Stop' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Settings' })).toBeInTheDocument();
});
it('should render the correct buttons when app status is starting', () => {
// arrange
// @ts-expect-error
render(<AppActions status="starting" info={app} />);
expect(screen.getByText('Cancel')).toBeInTheDocument();
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByTestId('action-button-loading')).toBeInTheDocument();
});
it('should render the correct buttons when app status is stopping', () => {
// arrange
// @ts-expect-error
render(<AppActions status="stopping" info={app} />);
expect(screen.getByText('Cancel')).toBeInTheDocument();
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByTestId('action-button-loading')).toBeInTheDocument();
});
it('should render the correct buttons when app status is removing', () => {
// arrange
// @ts-expect-error
render(<AppActions status="uninstalling" info={app} />);
expect(screen.getByText('Cancel')).toBeInTheDocument();
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByTestId('action-button-loading')).toBeInTheDocument();
});
it('should render the correct buttons when app status is installing', () => {
// arrange
// @ts-ignore
render(<AppActions status="installing" info={app} />);
expect(screen.getByText('Cancel')).toBeInTheDocument();
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByTestId('action-button-loading')).toBeInTheDocument();
});
it('should render the correct buttons when app status is updating', () => {
// arrange
// @ts-expect-error
render(<AppActions status="updating" info={app} />);
expect(screen.getByText('Cancel')).toBeInTheDocument();
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(screen.getByTestId('action-button-loading')).toBeInTheDocument();
});
it('should render the correct buttons when app status is missing', () => {
// arrange
// @ts-expect-error
render(<AppActions status="missing" info={app} />);
expect(screen.getByText('Install')).toBeInTheDocument();
// assert
expect(screen.getByRole('button', { name: 'Install' })).toBeInTheDocument();
});
});

View file

@ -32,8 +32,10 @@ interface BtnProps {
const ActionButton: React.FC<BtnProps> = (props) => {
const { IconComponent, onClick, title, loading, color, width = 140 } = props;
const testId = loading ? 'action-button-loading' : undefined;
return (
<Button loading={loading} data-testid={`action-button-${title?.toLowerCase()}`} onClick={onClick} width={width} className={clsx('me-2 px-4 mt-2', [`btn-${color}`])}>
<Button loading={loading} data-testid={testId} onClick={onClick} width={width} className={clsx('me-2 px-4 mt-2', [`btn-${color}`])}>
{title}
{IconComponent && <IconComponent className="ms-1" size={14} />}
</Button>

View file

@ -83,10 +83,8 @@ export const InstallForm: React.FC<IProps> = ({ formFields, onSubmit, initalValu
}
};
const name = initalValues ? 'update' : 'install';
return (
<form data-testid={`${name}-form`} className="flex flex-col" onSubmit={handleSubmit(validate)}>
<form className="flex flex-col" onSubmit={handleSubmit(validate)}>
{formFields.filter(typeFilter).map(renderField)}
{exposable && renderExposeForm()}
<Button loading={loading} type="submit" className="btn-success">

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
import { InstallForm } from '../InstallForm';
import { Modal, ModalBody, ModalHeader } from '../../../../components/ui/Modal';
import { AppInfo } from '../../../../core/types';
import { FormValues } from '../InstallForm/InstallForm';
@ -12,12 +12,14 @@ interface IProps {
}
export const InstallModal: React.FC<IProps> = ({ info, isOpen, onClose, onSubmit }) => (
<Modal onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Install {info.name}</h5>
</ModalHeader>
<ModalBody>
<InstallForm onSubmit={onSubmit} formFields={info.form_fields} exposable={info.exposable} />
</ModalBody>
</Modal>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<h5 className="modal-title">Install {info.name}</h5>
</DialogHeader>
<DialogDescription>
<InstallForm onSubmit={onSubmit} formFields={info.form_fields} exposable={info.exposable} />
</DialogDescription>
</DialogContent>
</Dialog>
);

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { Button } from '../../../components/ui/Button';
import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../../components/ui/Modal';
import { AppInfo } from '../../../core/types';
interface IProps {
@ -11,17 +11,19 @@ interface IProps {
}
export const StopModal: React.FC<IProps> = ({ info, isOpen, onClose, onConfirm }) => (
<Modal size="sm" onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Stop {info.name} ?</h5>
</ModalHeader>
<ModalBody>
<div className="text-muted">All data will be retained</div>
</ModalBody>
<ModalFooter>
<Button data-testid="modal-stop-button" onClick={onConfirm} className="btn-danger">
Stop
</Button>
</ModalFooter>
</Modal>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent size="sm">
<DialogHeader>
<h5 className="modal-title">Stop {info.name} ?</h5>
</DialogHeader>
<DialogDescription>
<div className="text-muted">All data will be retained</div>
</DialogDescription>
<DialogFooter>
<Button onClick={onConfirm} className="btn-danger">
Stop
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);

View file

@ -1,7 +1,7 @@
import { IconAlertTriangle } from '@tabler/icons-react';
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { Button } from '../../../components/ui/Button';
import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../../components/ui/Modal';
import { AppInfo } from '../../../core/types';
interface IProps {
@ -12,19 +12,21 @@ interface IProps {
}
export const UninstallModal: React.FC<IProps> = ({ info, isOpen, onClose, onConfirm }) => (
<Modal size="sm" type="danger" onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Uninstall {info.name} ?</h5>
</ModalHeader>
<ModalBody className="text-center py-4">
<IconAlertTriangle className="icon mb-2 text-danger icon-lg" />
<h3>Are you sure?</h3>
<div className="text-muted">All data for this app will be lost.</div>
</ModalBody>
<ModalFooter>
<Button onClick={onConfirm} className="btn-danger">
Uninstall
</Button>
</ModalFooter>
</Modal>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent type="danger" size="sm">
<DialogHeader>
<h5 className="modal-title">Uninstall {info.name} ?</h5>
</DialogHeader>
<DialogDescription className="text-center py-4">
<IconAlertTriangle className="icon mb-2 text-danger icon-lg" />
<h3>Are you sure?</h3>
<div className="text-muted">All data for this app will be lost.</div>
</DialogDescription>
<DialogFooter>
<Button onClick={onConfirm} className="btn-danger">
Uninstall
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);

View file

@ -7,40 +7,40 @@ describe('UpdateModal', () => {
const newVersion = '1.2.3';
it('renders with the correct title and version number', () => {
// Arrange
// arrange
render(<UpdateModal info={app} newVersion={newVersion} isOpen onClose={jest.fn()} onConfirm={jest.fn()} />);
// Assert
// assert
expect(screen.getByText(`Update ${app.name} ?`)).toBeInTheDocument();
expect(screen.getByText(`${newVersion}`)).toBeInTheDocument();
});
it('should not render when isOpen is false', () => {
// Arrange
// arrange
render(<UpdateModal info={app} newVersion={newVersion} isOpen={false} onClose={jest.fn()} onConfirm={jest.fn()} />);
const modal = screen.queryByTestId('modal');
// Assert (modal should have style display: none)
expect(modal).toHaveStyle('display: none');
// assert
expect(modal).not.toBeInTheDocument();
});
it('calls onClose when the close button is clicked', () => {
// Arrange
// arrange
const onClose = jest.fn();
render(<UpdateModal info={app} newVersion={newVersion} isOpen onClose={onClose} onConfirm={jest.fn()} />);
// Act
// act
const closeButton = screen.getByTestId('modal-close-button');
fireEvent.click(closeButton);
expect(onClose).toHaveBeenCalled();
});
it('calls onConfirm when the update button is clicked', () => {
// Arrange
// arrange
const onConfirm = jest.fn();
render(<UpdateModal info={app} newVersion={newVersion} isOpen onClose={jest.fn()} onConfirm={onConfirm} />);
// Act
// act
const updateButton = screen.getByText('Update');
fireEvent.click(updateButton);
expect(onConfirm).toHaveBeenCalled();

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { Button } from '../../../../components/ui/Button';
import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../../../components/ui/Modal';
import { AppInfo } from '../../../../core/types';
interface IProps {
@ -12,20 +12,22 @@ interface IProps {
}
export const UpdateModal: React.FC<IProps> = ({ info, newVersion, isOpen, onClose, onConfirm }) => (
<Modal size="sm" onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Update {info.name} ?</h5>
</ModalHeader>
<ModalBody>
<div className="text-muted">
Update app to latest verion : <b>{newVersion}</b> ?<br />
This will reset your custom configuration (e.g. changes in docker-compose.yml)
</div>
</ModalBody>
<ModalFooter>
<Button data-testid="modal-update-button" onClick={onConfirm} className="btn-success">
Update
</Button>
</ModalFooter>
</Modal>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent size="sm">
<DialogHeader>
<h5 className="modal-title">Update {info.name} ?</h5>
</DialogHeader>
<DialogDescription>
<div className="text-muted">
Update app to latest verion : <b>{newVersion}</b> ?<br />
This will reset your custom configuration (e.g. changes in docker-compose.yml)
</div>
</DialogDescription>
<DialogFooter>
<Button onClick={onConfirm} className="btn-success">
Update
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogHeader } from '@/components/ui/Dialog';
import { InstallForm } from './InstallForm';
import { Modal, ModalBody, ModalHeader } from '../../../components/ui/Modal';
import { AppInfo } from '../../../core/types';
import { FormValues } from './InstallForm/InstallForm';
@ -15,12 +15,14 @@ interface IProps {
}
export const UpdateSettingsModal: React.FC<IProps> = ({ info, config, isOpen, onClose, onSubmit, exposed, domain }) => (
<Modal onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Update {info.name} config</h5>
</ModalHeader>
<ModalBody>
<InstallForm onSubmit={onSubmit} formFields={info.form_fields} exposable={info.exposable} initalValues={{ ...config, exposed, domain }} />
</ModalBody>
</Modal>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<h5 className="modal-title">Update {info.name} config</h5>
</DialogHeader>
<DialogDescription>
<InstallForm onSubmit={onSubmit} formFields={info.form_fields} exposable={info.exposable} initalValues={{ ...config, exposed, domain }} />
</DialogDescription>
</DialogContent>
</Dialog>
);

View file

@ -23,7 +23,7 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Assert
expect(screen.getByTestId('action-button-update')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Update' })).toBeInTheDocument();
});
it('should display install button when app is not installed', async () => {
@ -33,7 +33,7 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Assert
expect(screen.getByTestId('action-button-install')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Install' })).toBeInTheDocument();
});
it('should display uninstall and start button when app is stopped', async () => {
@ -43,8 +43,8 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Assert
expect(screen.getByTestId('action-button-remove')).toBeInTheDocument();
expect(screen.getByTestId('action-button-start')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Remove' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Start' })).toBeInTheDocument();
});
it('should display stop, open and settings buttons when app is running', async () => {
@ -53,9 +53,9 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Assert
expect(screen.getByTestId('action-button-stop')).toBeInTheDocument();
expect(screen.getByTestId('action-button-open')).toBeInTheDocument();
expect(screen.getByTestId('action-button-settings')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Stop' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Settings' })).toBeInTheDocument();
});
it('should not display update button when update is not available', async () => {
@ -64,7 +64,7 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Assert
expect(screen.queryByTestId('action-button-update')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Update' })).not.toBeInTheDocument();
});
it('should not display open button when app has no_gui set to true', async () => {
@ -73,7 +73,7 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Assert
expect(screen.queryByTestId('action-button-open')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Open' })).not.toBeInTheDocument();
});
});
@ -85,7 +85,7 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Act
const openButton = screen.getByTestId('action-button-open');
const openButton = screen.getByRole('button', { name: 'Open' });
openButton.click();
// Assert
@ -99,7 +99,7 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Act
const openButton = screen.getByTestId('action-button-open');
const openButton = screen.getByRole('button', { name: 'Open' });
openButton.click();
// Assert
@ -114,10 +114,12 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMock({ path: ['app', 'installApp'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Install' });
fireEvent.click(openModalButton);
// Act
const installForm = screen.getByTestId('install-form');
fireEvent.submit(installForm);
const installButton = screen.getByRole('button', { name: 'Install' });
fireEvent.click(installButton);
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
@ -139,10 +141,12 @@ describe('Test: AppDetailsContainer', () => {
const app = createAppEntity({ overrides: { status: 'missing' } });
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Install' });
fireEvent.click(openModalButton);
// Act
const installForm = screen.getByTestId('install-form');
fireEvent.submit(installForm);
const installButton = screen.getByRole('button', { name: 'Install' });
fireEvent.click(installButton);
await waitFor(() => {
expect(result.current.toasts).toHaveLength(1);
@ -150,24 +154,6 @@ describe('Test: AppDetailsContainer', () => {
expect(result.current.toasts[0].status).toEqual('error');
});
});
// Skipping because trpc.useContext is not working in tests
it.skip('should put the app in installing state when install mutation is called', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
const app = createAppEntity({ overrides: { status: 'missing' } });
server.use(getTRPCMock({ path: ['app', 'installApp'], type: 'mutation', response: app, delay: 100 }));
render(<AppDetailsContainer app={app} />);
// Act
const installForm = screen.getByTestId('install-form');
fireEvent.submit(installForm);
await waitFor(() => {
expect(screen.getByText('installing')).toBeInTheDocument();
expect(result.current.toasts).toHaveLength(1);
});
});
});
describe('Test: Update app', () => {
@ -177,11 +163,11 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Update' });
fireEvent.click(openModalButton);
// Act
const updateButton = screen.getByTestId('action-button-update');
updateButton.click();
const modalUpdateButton = screen.getByTestId('modal-update-button');
const modalUpdateButton = screen.getByRole('button', { name: 'Update' });
modalUpdateButton.click();
await waitFor(() => {
@ -197,11 +183,11 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMockError({ path: ['app', 'updateApp'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 }, overridesInfo: { tipi_version: 3 } });
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Update' });
fireEvent.click(openModalButton);
// Act
const updateButton = screen.getByTestId('action-button-update');
updateButton.click();
const modalUpdateButton = screen.getByTestId('modal-update-button');
const modalUpdateButton = screen.getByRole('button', { name: 'Update' });
modalUpdateButton.click();
// Assert
@ -211,26 +197,6 @@ describe('Test: AppDetailsContainer', () => {
expect(result.current.toasts[0].status).toEqual('error');
});
});
// Skipping because trpc.useContext is not working in tests
it.skip('should put the app in updating state when update mutation is called', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
const app = createAppEntity({ overrides: { version: 2 }, overridesInfo: { tipi_version: 3 } });
server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app, delay: 100 }));
render(<AppDetailsContainer app={app} />);
// Act
const updateButton = screen.getByTestId('action-button-update');
updateButton.click();
const modalUpdateButton = screen.getByTestId('modal-update-button');
modalUpdateButton.click();
await waitFor(() => {
expect(screen.getByText('updating')).toBeInTheDocument();
expect(result.current.toasts).toHaveLength(1);
});
});
});
describe('Test: Uninstall app', () => {
@ -240,11 +206,11 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMock({ path: ['app', 'uninstallApp'], type: 'mutation', response: { id: app.id, config: {}, status: 'missing' } }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Remove' });
fireEvent.click(openModalButton);
// Act
const uninstallButton = screen.getByTestId('action-button-remove');
uninstallButton.click();
const modalUninstallButton = screen.getByText('Uninstall');
const modalUninstallButton = screen.getByRole('button', { name: 'Uninstall' });
modalUninstallButton.click();
// Assert
@ -261,11 +227,11 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMockError({ path: ['app', 'uninstallApp'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ status: 'stopped' });
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Remove' });
fireEvent.click(openModalButton);
// Act
const uninstallButton = screen.getByTestId('action-button-remove');
uninstallButton.click();
const modalUninstallButton = screen.getByText('Uninstall');
const modalUninstallButton = screen.getByRole('button', { name: 'Uninstall' });
modalUninstallButton.click();
// Assert
@ -275,27 +241,6 @@ describe('Test: AppDetailsContainer', () => {
expect(result.current.toasts[0].status).toEqual('error');
});
});
// Skipping because trpc.useContext is not working in tests
it.skip('should put the app in uninstalling state when uninstall mutation is called', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
const app = createAppEntity({ status: 'stopped' });
server.use(getTRPCMock({ path: ['app', 'uninstallApp'], type: 'mutation', response: { id: app.id, config: {}, status: 'missing' }, delay: 100 }));
render(<AppDetailsContainer app={app} />);
// Act
const uninstallButton = screen.getByTestId('action-button-remove');
uninstallButton.click();
const modalUninstallButton = screen.getByText('Uninstall');
modalUninstallButton.click();
await waitFor(() => {
expect(screen.getByText('uninstalling')).toBeInTheDocument();
expect(screen.queryByText('installing')).not.toBeInTheDocument();
expect(result.current.toasts).toHaveLength(1);
});
});
});
describe('Test: Start app', () => {
@ -307,7 +252,7 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Act
const startButton = screen.getByTestId('action-button-start');
const startButton = screen.getByRole('button', { name: 'Start' });
startButton.click();
// Assert
@ -326,7 +271,7 @@ describe('Test: AppDetailsContainer', () => {
render(<AppDetailsContainer app={app} />);
// Act
const startButton = screen.getByTestId('action-button-start');
const startButton = screen.getByRole('button', { name: 'Start' });
startButton.click();
// Assert
@ -336,24 +281,6 @@ describe('Test: AppDetailsContainer', () => {
expect(result.current.toasts[0].status).toEqual('error');
});
});
// Skipping because trpc.useContext is not working in tests
it.skip('should put the app in starting state when start mutation is called', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
const app = createAppEntity({ status: 'stopped' });
server.use(getTRPCMock({ path: ['app', 'startApp'], type: 'mutation', response: app, delay: 100 }));
render(<AppDetailsContainer app={app} />);
// Act
const startButton = screen.getByTestId('action-button-start');
startButton.click();
await waitFor(() => {
expect(screen.getByText('starting')).toBeInTheDocument();
expect(result.current.toasts).toHaveLength(1);
});
});
});
describe('Test: Stop app', () => {
@ -363,11 +290,11 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMock({ path: ['app', 'stopApp'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Stop' });
fireEvent.click(openModalButton);
// Act
const stopButton = screen.getByTestId('action-button-stop');
stopButton.click();
const modalStopButton = screen.getByTestId('modal-stop-button');
const modalStopButton = screen.getByRole('button', { name: 'Stop' });
modalStopButton.click();
// Assert
@ -384,11 +311,11 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMockError({ path: ['app', 'stopApp'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ status: 'running' });
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Stop' });
fireEvent.click(openModalButton);
// Act
const stopButton = screen.getByTestId('action-button-stop');
stopButton.click();
const modalStopButton = screen.getByTestId('modal-stop-button');
const modalStopButton = screen.getByRole('button', { name: 'Stop' });
modalStopButton.click();
// Assert
@ -398,26 +325,6 @@ describe('Test: AppDetailsContainer', () => {
expect(result.current.toasts[0].status).toEqual('error');
});
});
// Skipping because trpc.useContext is not working in tests
it.skip('should put the app in stopping state when stop mutation is called', async () => {
// Arrange
const { result } = renderHook(() => useToastStore());
const app = createAppEntity({ status: 'running' });
server.use(getTRPCMock({ path: ['app', 'stopApp'], type: 'mutation', response: app }));
render(<AppDetailsContainer app={app} />);
// Act
const stopButton = screen.getByTestId('action-button-stop');
stopButton.click();
const modalStopButton = screen.getByTestId('modal-stop-button');
modalStopButton.click();
await waitFor(() => {
expect(screen.getByText('stopping')).toBeInTheDocument();
expect(result.current.toasts).toHaveLength(1);
});
});
});
describe('Test: Update app config', () => {
@ -427,12 +334,12 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMock({ path: ['app', 'updateAppConfig'], type: 'mutation', response: app }));
const { result } = renderHook(() => useToastStore());
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Settings' });
fireEvent.click(openModalButton);
// Act
const configButton = screen.getByTestId('action-button-settings');
const configButton = screen.getByRole('button', { name: 'Update' });
configButton.click();
const modalConfigButton = screen.getAllByText('Update');
modalConfigButton[1]?.click();
// Assert
await waitFor(() => {
@ -448,12 +355,12 @@ describe('Test: AppDetailsContainer', () => {
server.use(getTRPCMockError({ path: ['app', 'updateAppConfig'], type: 'mutation', message: 'my big error' }));
const app = createAppEntity({ status: 'running', overridesInfo: { exposable: true } });
render(<AppDetailsContainer app={app} />);
const openModalButton = screen.getByRole('button', { name: 'Settings' });
fireEvent.click(openModalButton);
// Act
const configButton = screen.getByTestId('action-button-settings');
const configButton = screen.getByRole('button', { name: 'Update' });
configButton.click();
const modalConfigButton = screen.getAllByText('Update');
modalConfigButton[1]?.click();
// Assert
await waitFor(() => {

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/Dialog';
import { Button } from '../../../../components/ui/Button';
import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../../../components/ui/Modal';
interface IProps {
isOpen: boolean;
@ -10,17 +10,19 @@ interface IProps {
}
export const RestartModal: React.FC<IProps> = ({ isOpen, onClose, onConfirm, loading }) => (
<Modal size="sm" onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Restart Tipi</h5>
</ModalHeader>
<ModalBody>
<div className="text-muted">Would you like to restart your Tipi server?</div>
</ModalBody>
<ModalFooter>
<Button data-testid="settings-modal-restart-button" onClick={onConfirm} className="btn-danger" loading={loading}>
Restart
</Button>
</ModalFooter>
</Modal>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent type="danger" size="sm">
<DialogHeader>
<h5 className="modal-title">Restart Tipi</h5>
</DialogHeader>
<DialogDescription>
<div className="text-muted">Would you like to restart your Tipi server?</div>
</DialogDescription>
<DialogFooter>
<Button onClick={onConfirm} className="btn-danger" loading={loading}>
Restart
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);

View file

@ -83,7 +83,7 @@ export const SettingsForm = (props: IProps) => {
};
return (
<form data-testid="settings-form" className="flex flex-col" onSubmit={handleSubmit(validate)}>
<form className="flex flex-col" onSubmit={handleSubmit(validate)}>
<h2 className="text-2xl font-bold">General settings</h2>
<p className="mb-4">This will update your settings.json file. Make sure you know what you are doing before updating these values.</p>
<div className="mb-3">

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Button } from '../../../../components/ui/Button';
import { Modal, ModalBody, ModalFooter, ModalHeader } from '../../../../components/ui/Modal';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '../../../../components/ui/Dialog';
interface IProps {
isOpen: boolean;
@ -10,17 +10,19 @@ interface IProps {
}
export const UpdateModal: React.FC<IProps> = ({ isOpen, onClose, onConfirm, loading }) => (
<Modal size="sm" onClose={onClose} isOpen={isOpen}>
<ModalHeader>
<h5 className="modal-title">Update Tipi</h5>
</ModalHeader>
<ModalBody>
<div className="text-muted">Would you like to update Tipi to the latest version?</div>
</ModalBody>
<ModalFooter>
<Button onClick={onConfirm} className="btn-success" loading={loading}>
Update
</Button>
</ModalFooter>
</Modal>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent size="sm">
<DialogHeader>
<h5 className="modal-title">Update Tipi</h5>
</DialogHeader>
<DialogDescription>
<div className="text-muted">Would you like to update Tipi to the latest version?</div>
</DialogDescription>
<DialogFooter>
<Button onClick={onConfirm} className="btn-success" loading={loading}>
Update
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);

View file

@ -9,7 +9,7 @@ describe('Test: GeneralActions', () => {
it('should render without error', () => {
render(<GeneralActions />);
expect(screen.getByText('Update')).toBeInTheDocument();
expect(screen.getByText('Actions')).toBeInTheDocument();
});
it('should show toast if update mutation fails', async () => {
@ -21,10 +21,12 @@ describe('Test: GeneralActions', () => {
await waitFor(() => {
expect(screen.getByText('Update to 2.0.0')).toBeInTheDocument();
});
const updateButton = screen.getByText('Update');
const updateButton = screen.getByRole('button', { name: /Update/i });
fireEvent.click(updateButton);
// act
fireEvent.click(updateButton);
const updateButtonModal = screen.getByRole('button', { name: /Update/i });
fireEvent.click(updateButtonModal);
// assert
await waitFor(() => {
@ -42,12 +44,14 @@ describe('Test: GeneralActions', () => {
server.use(getTRPCMock({ path: ['system', 'update'], response: true }));
render(<GeneralActions />);
await waitFor(() => {
expect(screen.getByText('Update to 2.0.0')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Update to 2.0.0' })).toBeInTheDocument();
});
const updateButton = screen.getByText('Update');
const updateButton = screen.getByRole('button', { name: /Update to 2.0.0/i });
fireEvent.click(updateButton);
// act
fireEvent.click(updateButton);
const updateButtonModal = screen.getByRole('button', { name: /Update/i });
fireEvent.click(updateButtonModal);
// assert
await waitFor(() => {
@ -60,12 +64,12 @@ describe('Test: GeneralActions', () => {
const { result } = renderHook(() => useToastStore());
server.use(getTRPCMockError({ path: ['system', 'restart'], type: 'mutation', status: 500, message: 'Something went wrong' }));
render(<GeneralActions />);
// Find button near the top of the page
const restartButton = screen.getByTestId('settings-modal-restart-button');
const restartButton = screen.getByRole('button', { name: /Restart/i });
// act
fireEvent.click(restartButton);
const restartButtonModal = screen.getByRole('button', { name: /Restart/i });
fireEvent.click(restartButtonModal);
// assert
await waitFor(() => {
@ -83,10 +87,12 @@ describe('Test: GeneralActions', () => {
render(<GeneralActions />);
// Find button near the top of the page
const restartButton = screen.getByTestId('settings-modal-restart-button');
const restartButton = screen.getByRole('button', { name: /Restart/i });
// act
fireEvent.click(restartButton);
const restartButtonModal = screen.getByRole('button', { name: /Restart/i });
fireEvent.click(restartButtonModal);
// assert
await waitFor(() => {

View file

@ -68,7 +68,7 @@ export const GeneralActions = () => {
);
};
return (
<div className="col d-flex flex-column">
<>
<div className="card-body">
<h2 className="mb-4">Actions</h2>
<h3 className="card-title mt-4">Version {versionQuery.data?.current}</h3>
@ -82,6 +82,6 @@ export const GeneralActions = () => {
</div>
<RestartModal isOpen={restartDisclosure.isOpen} onClose={restartDisclosure.close} onConfirm={() => restart.mutate()} loading={loading} />
<UpdateModal isOpen={updateDisclosure.isOpen} onClose={updateDisclosure.close} onConfirm={() => update.mutate()} loading={loading} />
</div>
</>
);
};

View file

@ -15,6 +15,12 @@ jest.mock('remark-gfm', () => () => ({}));
console.error = jest.fn();
class ResizeObserver {
observe() {}
unobserve() {}
}
// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {};
@ -35,6 +41,7 @@ const localStorageMock = (() => {
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
Object.defineProperty(window, 'ResizeObserver', { value: ResizeObserver });
beforeAll(() => {
// Enable the mocking in tests.