feat(AppActions): add drop down menu on open to give the user the option to choose which url to open to

This commit is contained in:
Nicolas Meienberger 2023-06-07 21:15:10 +02:00 committed by Nicolas Meienberger
parent 633baf24d0
commit 8a871a35f3
15 changed files with 712 additions and 56 deletions

View file

@ -30,7 +30,8 @@ test('user can install and uninstall app', async ({ page, context }) => {
await expect(page.getByText('Running')).toBeVisible({ timeout: 60000 });
await expect(page.getByText('App installed successfully')).toBeVisible();
const [newPage] = await Promise.all([context.waitForEvent('page'), await page.getByTestId('app-details').getByRole('button', { name: 'Open' }).click()]);
await page.getByTestId('app-details').getByRole('button', { name: 'Open' }).click();
const [newPage] = await Promise.all([context.waitForEvent('page'), await page.getByRole('menuitem', { name: 'localhost:8000' }).click()]);
await newPage.waitForLoadState();
await expect(newPage.getByText('Hello World')).toBeVisible();

View file

@ -34,6 +34,7 @@
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-select": "^1.2.1",
"@radix-ui/react-switch": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.3",
@ -87,8 +88,8 @@
"devDependencies": {
"@babel/core": "^7.21.8",
"@faker-js/faker": "^8.0.1",
"@testing-library/dom": "^9.3.0",
"@playwright/test": "^1.32.3",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",

492
pnpm-lock.yaml generated
View file

@ -16,6 +16,9 @@ dependencies:
'@radix-ui/react-dialog':
specifier: ^1.0.3
version: 1.0.3(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.5
version: 2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-select':
specifier: ^1.2.1
version: 1.2.1(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
@ -1062,6 +1065,10 @@ packages:
resolution: {integrity: sha512-LSqwPZkK3rYfD7GKoIeExXOyYx6Q1O4iqZWwIehDNuv3Dv425FIAE8PRwtAx1imEolFTHgBEcoFHm9MDnYgPCg==}
dev: false
/@floating-ui/core@1.2.6:
resolution: {integrity: sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==}
dev: false
/@floating-ui/dom@0.5.4:
resolution: {integrity: sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==}
dependencies:
@ -1074,6 +1081,12 @@ packages:
'@floating-ui/core': 1.2.1
dev: false
/@floating-ui/dom@1.2.9:
resolution: {integrity: sha512-sosQxsqgxMNkV3C+3UqTS6LxP7isRLwX8WMepp843Rb3/b0Wz8+MdUkxJksByip3C2WwLugLHN1b4ibn//zKwQ==}
dependencies:
'@floating-ui/core': 1.2.6
dev: false
/@floating-ui/react-dom@0.7.2(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==}
peerDependencies:
@ -1088,6 +1101,17 @@ packages:
- '@types/react'
dev: false
/@floating-ui/react-dom@2.0.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Ke0oU3SeuABC2C4OFu2mSAwHIP5WUiV98O9YWoHV4Q5aT6E9k06DV0Khi5uYspR8xmmBk08t8ZDcz3TR3ARkEg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/dom': 1.2.9
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@formatjs/ecma402-abstract@1.11.4:
resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==}
dependencies:
@ -1640,6 +1664,12 @@ packages:
'@babel/runtime': 7.20.13
dev: false
/@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies:
'@babel/runtime': 7.20.13
dev: false
/@radix-ui/react-arrow@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==}
peerDependencies:
@ -1652,6 +1682,27 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collection@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw==}
peerDependencies:
@ -1667,6 +1718,30 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-compose-refs@1.0.0(react@18.2.0):
resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==}
peerDependencies:
@ -1676,6 +1751,20 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@types/react': 18.2.7
react: 18.2.0
dev: false
/@radix-ui/react-context@1.0.0(react@18.2.0):
resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==}
peerDependencies:
@ -1685,6 +1774,20 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-context@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@types/react': 18.2.7
react: 18.2.0
dev: false
/@radix-ui/react-dialog@1.0.3(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-owNhq36kNPqC2/a+zJRioPg6HHnTn5B/sh/NjTY8r4W9g1L5VJlrzZIVcBr7R9Mg8iLjVmh6MGgMlfoVf/WO/A==}
peerDependencies:
@ -1721,6 +1824,20 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-direction@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@types/react': 18.2.7
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:
@ -1737,6 +1854,58 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-dropdown-menu@2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-menu': 2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
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:
@ -1746,6 +1915,20 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@types/react': 18.2.7
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:
@ -1760,6 +1943,29 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
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:
@ -1770,6 +1976,59 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-id@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
react: 18.2.0
dev: false
/@radix-ui/react-menu@2.0.5(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
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.2.7)(react@18.2.0)
dev: false
/@radix-ui/react-popper@1.1.1(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==}
peerDependencies:
@ -1793,6 +2052,36 @@ packages:
- '@types/react'
dev: false
/@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@floating-ui/react-dom': 2.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-use-rect': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/rect': 1.0.1
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(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:
@ -1805,6 +2094,27 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
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:
@ -1818,6 +2128,28 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-primitive@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==}
peerDependencies:
@ -1830,6 +2162,27 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-roving-focus@1.0.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ==}
peerDependencies:
@ -1850,6 +2203,35 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-id': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
'@types/react-dom': 18.2.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-select@1.2.1(@types/react@18.2.7)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GULRMITaOHNj79BZvQs3iZO0+f2IgI8g5HDhMi7Bnc13t7IlG86NFtOCfTLme4PNZdEtU+no+oGgcl6IFiphpQ==}
peerDependencies:
@ -1894,6 +2276,21 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-slot@1.0.2(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
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:
@ -1940,6 +2337,20 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@types/react': 18.2.7
react: 18.2.0
dev: false
/@radix-ui/react-use-controllable-state@1.0.0(react@18.2.0):
resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==}
peerDependencies:
@ -1950,6 +2361,21 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
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:
@ -1960,6 +2386,21 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
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:
@ -1969,6 +2410,20 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@types/react': 18.2.7
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:
@ -1988,6 +2443,21 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-rect@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/rect': 1.0.1
'@types/react': 18.2.7
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:
@ -1998,6 +2468,21 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-size@1.0.1(@types/react@18.2.7)(react@18.2.0):
resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.7)(react@18.2.0)
'@types/react': 18.2.7
react: 18.2.0
dev: false
/@radix-ui/react-visually-hidden@1.0.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg==}
peerDependencies:
@ -2016,6 +2501,12 @@ packages:
'@babel/runtime': 7.20.13
dev: false
/@radix-ui/rect@1.0.1:
resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==}
dependencies:
'@babel/runtime': 7.20.13
dev: false
/@redis/bloom@1.2.0(@redis/client@1.5.7):
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies:
@ -2579,7 +3070,6 @@ packages:
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
dependencies:
'@types/react': 18.2.7
dev: true
/@types/react-transition-group@4.4.5:
resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==}

View file

@ -11,7 +11,9 @@ type SessionContent = {
};
declare module 'express-session' {
export type SessionData = SessionContent;
interface SessionData extends SessionContent {
userId?: number;
}
}
interface ExtendedGetServerSidePropsContext<Params, Preview> extends GetServerSidePropsContext<Params, Preview> {

View file

@ -144,6 +144,7 @@
"link": "Link",
"website": "Website",
"supported-arch": "Supported architectures",
"choose-open-method": "Choose open method",
"categories": {
"data": "Data",
"network": "Network",

View file

@ -1,24 +1,28 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React from 'react';
import { AppActions } from './AppActions';
import { cleanup, fireEvent, render, screen } from '../../../../../../tests/test-utils';
import { cleanup, fireEvent, render, screen, waitFor, userEvent } from '../../../../../../tests/test-utils';
import { AppInfo } from '../../../../core/types';
afterEach(cleanup);
describe('Test: AppActions', () => {
const app = {
name: 'My App',
form_fields: [],
exposable: [],
id: 'test',
info: {
port: 3000,
id: 'test',
name: 'My App',
form_fields: [],
exposable: [],
},
} as unknown as AppInfo;
it('should call the callbacks when buttons are clicked', () => {
// arrange
const onStart = jest.fn();
const onRemove = jest.fn();
// @ts-expect-error
render(<AppActions status="stopped" info={app} onStart={onStart} onUninstall={onRemove} />);
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="stopped" app={app} onStart={onStart} onUninstall={onRemove} />);
// act
const startButton = screen.getByRole('button', { name: 'Start' });
@ -33,8 +37,8 @@ describe('Test: AppActions', () => {
it('should render the correct buttons when app status is running', () => {
// arrange
// @ts-expect-error
render(<AppActions status="running" info={app} />);
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="running" app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Stop' })).toBeInTheDocument();
@ -44,8 +48,8 @@ describe('Test: AppActions', () => {
it('should render the correct buttons when app status is starting', () => {
// arrange
// @ts-expect-error
render(<AppActions status="starting" info={app} />);
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="starting" app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
@ -54,8 +58,8 @@ describe('Test: AppActions', () => {
it('should render the correct buttons when app status is stopping', () => {
// arrange
// @ts-expect-error
render(<AppActions status="stopping" info={app} />);
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="stopping" app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
@ -64,8 +68,8 @@ describe('Test: AppActions', () => {
it('should render the correct buttons when app status is removing', () => {
// arrange
// @ts-expect-error
render(<AppActions status="uninstalling" info={app} />);
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="uninstalling" app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
@ -74,8 +78,8 @@ describe('Test: AppActions', () => {
it('should render the correct buttons when app status is installing', () => {
// arrange
// @ts-ignore
render(<AppActions status="installing" info={app} />);
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="installing" app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
@ -84,8 +88,8 @@ describe('Test: AppActions', () => {
it('should render the correct buttons when app status is updating', () => {
// arrange
// @ts-expect-error
render(<AppActions status="updating" info={app} />);
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="updating" app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
@ -94,10 +98,96 @@ describe('Test: AppActions', () => {
it('should render the correct buttons when app status is missing', () => {
// arrange
// @ts-expect-error
render(<AppActions status="missing" info={app} />);
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="missing" app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Install' })).toBeInTheDocument();
});
it('should render update button if app is running and has an update available', () => {
// arrange
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="running" updateAvailable app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Update' })).toBeInTheDocument();
});
it('should render update button if app is stopped and has an update available', () => {
// arrange
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions status="stopped" updateAvailable app={app} />);
// assert
expect(screen.getByRole('button', { name: 'Update' })).toBeInTheDocument();
});
it('should render domain button if app is running and has a domain', async () => {
// arrange
const appWithDomain = {
...app,
exposed: true,
domain: 'myapp.example.com',
};
const openFn = jest.fn();
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions onOpen={openFn} status="running" app={appWithDomain} />);
// act
const openButton = screen.getByRole('button', { name: 'Open' });
userEvent.type(openButton, '{arrowdown}');
await waitFor(() => {
expect(screen.getByText(/myapp.example.com/)).toBeInTheDocument();
});
const domainButton = screen.getByText(/myapp.example.com/);
// assert
userEvent.click(domainButton);
await waitFor(() => {
expect(openFn).toHaveBeenCalledWith('domain');
});
});
it('should render local_domain open button', async () => {
// arrange
const openFn = jest.fn();
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions localDomain="tipi.lan" onOpen={openFn} status="running" app={app} />);
// act
const openButton = screen.getByRole('button', { name: 'Open' });
userEvent.type(openButton, '{arrowdown}');
await waitFor(() => {
expect(screen.getByText(/test.tipi.lan/)).toBeInTheDocument();
});
const localButton = screen.getByText(/test.tipi.lan/);
// assert
userEvent.click(localButton);
await waitFor(() => {
expect(openFn).toHaveBeenCalledWith('local_domain');
});
});
it('should render local open button', async () => {
// arrange
const openFn = jest.fn();
// @ts-expect-error - we don't need to pass all props for this test
render(<AppActions localUrl="http://localhost:3000" onOpen={openFn} status="running" app={app} />);
// act
const openButton = screen.getByRole('button', { name: 'Open' });
userEvent.type(openButton, '{arrowdown}');
await waitFor(() => {
expect(screen.getByText(/localhost:3000/)).toBeInTheDocument();
});
const localButton = screen.getByText(/localhost:3000/);
// assert
userEvent.click(localButton);
await waitFor(() => {
expect(openFn).toHaveBeenCalledWith('local');
});
});
});

View file

@ -1,21 +1,23 @@
import { Icon, IconDownload, IconExternalLink, IconPlayerPause, IconPlayerPlay, IconSettings, IconTrash, IconX } from '@tabler/icons-react';
import { Icon, IconDownload, IconExternalLink, IconLock, IconLockOff, IconPlayerPause, IconPlayerPlay, IconSettings, IconTrash, IconX } from '@tabler/icons-react';
import clsx from 'clsx';
import React from 'react';
import type { AppStatus } from '@/server/db/schema';
import { useTranslations } from 'next-intl';
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from '@/components/ui/DropdownMenu';
import { Button } from '../../../../components/ui/Button';
import { AppInfo } from '../../../../core/types';
import { AppWithInfo } from '../../../../core/types';
interface IProps {
info: AppInfo;
app: AppWithInfo;
status?: AppStatus;
updateAvailable: boolean;
localDomain?: string;
onInstall: () => void;
onUninstall: () => void;
onStart: () => void;
onStop: () => void;
onOpen: () => void;
onOpen: (url: OpenType) => void;
onUpdate: () => void;
onUpdateSettings: () => void;
onCancel: () => void;
@ -23,7 +25,7 @@ interface IProps {
interface BtnProps {
IconComponent?: Icon;
onClick: () => void;
onClick?: () => void;
width?: number | null;
title?: string;
color?: string;
@ -43,7 +45,10 @@ const ActionButton: React.FC<BtnProps> = (props) => {
);
};
export const AppActions: React.FC<IProps> = ({ info, status, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate, onCancel, updateAvailable, onUpdateSettings }) => {
type OpenType = 'local' | 'domain' | 'local_domain';
export const AppActions: React.FC<IProps> = ({ app, status, localDomain, onInstall, onUninstall, onStart, onStop, onOpen, onUpdate, onCancel, updateAvailable, onUpdateSettings }) => {
const { info } = app;
const t = useTranslations('apps.app-details');
const hasSettings = Object.keys(info.form_fields).length > 0 || info.exposable;
@ -53,12 +58,41 @@ export const AppActions: React.FC<IProps> = ({ info, status, onInstall, onUninst
const RemoveButton = <ActionButton key="remove" IconComponent={IconTrash} onClick={onUninstall} title={t('actions.remove')} color="danger" />;
const SettingsButton = <ActionButton key="settings" IconComponent={IconSettings} onClick={onUpdateSettings} title={t('actions.settings')} />;
const StopButton = <ActionButton key="stop" IconComponent={IconPlayerPause} onClick={onStop} title={t('actions.stop')} color="danger" />;
const OpenButton = <ActionButton key="open" IconComponent={IconExternalLink} onClick={onOpen} title={t('actions.open')} />;
const LoadingButtion = <ActionButton key="loading" loading onClick={() => null} color="success" title={t('actions.loading')} />;
const LoadingButtion = <ActionButton key="loading" loading color="success" title={t('actions.loading')} />;
const CancelButton = <ActionButton key="cancel" IconComponent={IconX} onClick={onCancel} title={t('actions.cancel')} />;
const InstallButton = <ActionButton key="install" onClick={onInstall} title={t('actions.install')} color="success" />;
const UpdateButton = <ActionButton key="update" IconComponent={IconDownload} onClick={onUpdate} width={null} title={t('actions.update')} color="success" />;
const OpenButton = (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button width={140} className={clsx('me-2 px-4 mt-2')}>
{t('actions.open')}
<IconExternalLink className="ms-1" size={14} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>{t('choose-open-method')}</DropdownMenuLabel>
<DropdownMenuGroup>
{app.exposed && app.domain && (
<DropdownMenuItem onClick={() => onOpen('domain')}>
<IconLock className="text-green me-2" size={16} />
{app.domain}
</DropdownMenuItem>
)}
<DropdownMenuItem onClick={() => onOpen('local_domain')}>
<IconLock className="text-muted me-2" size={16} />
{app.id}.{localDomain}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onOpen('local')}>
<IconLockOff className="text-muted me-2" size={16} />
{window.location.hostname}:{app.info.port}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
switch (status) {
case 'stopped':
buttons.push(StartButton, RemoveButton);

View file

@ -1,6 +1,6 @@
import React from 'react';
import { faker } from '@faker-js/faker';
import { fireEvent, render, screen, waitFor } from '../../../../../../tests/test-utils';
import { fireEvent, render, screen, userEvent, waitFor } from '../../../../../../tests/test-utils';
import { createAppEntity } from '../../../../mocks/fixtures/app.fixtures';
import { getTRPCMock, getTRPCMockError } from '../../../../mocks/getTrpcMock';
import { server } from '../../../../mocks/server';
@ -86,10 +86,19 @@ describe('Test: AppDetailsContainer', () => {
// Act
const openButton = screen.getByRole('button', { name: 'Open' });
openButton.click();
userEvent.type(openButton, '{arrowdown}');
await waitFor(() => {
expect(screen.getByText(/localhost:/)).toBeInTheDocument();
});
const openButtonItem = screen.getByText(/localhost:/);
userEvent.click(openButtonItem);
// Assert
expect(spy).toHaveBeenCalledWith(`http://localhost:${app.info.port}`, '_blank', 'noreferrer');
await waitFor(() => {
expect(spy).toHaveBeenCalledWith(`http://localhost:${app.info.port}`, '_blank', 'noreferrer');
});
spy.mockRestore();
});
it('should open with https when app info has https set to true', async () => {
@ -100,10 +109,20 @@ describe('Test: AppDetailsContainer', () => {
// Act
const openButton = screen.getByRole('button', { name: 'Open' });
openButton.click();
userEvent.type(openButton, '{arrowdown}');
await waitFor(() => {
expect(screen.getByText(/localhost:/)).toBeInTheDocument();
});
const openButtonItem = screen.getByText(/localhost:/);
userEvent.click(openButtonItem);
// Assert
expect(spy).toHaveBeenCalledWith(`https://localhost:${app.info.port}`, '_blank', 'noreferrer');
await waitFor(() => {
expect(spy).toHaveBeenCalledWith(`https://localhost:${app.info.port}`, '_blank', 'noreferrer');
});
spy.mockRestore();
});
});

View file

@ -20,6 +20,7 @@ import { castAppConfig } from '../../helpers/castAppConfig';
interface IProps {
app: AppRouterOutput['getApp'];
}
type OpenType = 'local' | 'domain' | 'local_domain';
export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
const t = useTranslations();
@ -29,6 +30,8 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
const updateDisclosure = useDisclosure();
const updateSettingsDisclosure = useDisclosure();
const getSettings = trpc.system.getSettings.useQuery();
const utils = trpc.useContext();
const invalidate = () => {
@ -135,15 +138,26 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
update.mutate({ id: app.id });
};
const handleOpen = () => {
const handleOpen = (type: OpenType) => {
let url = '';
const { https } = app.info;
const protocol = https ? 'https' : 'http';
if (typeof window !== 'undefined') {
// Current domain
const domain = window.location.hostname;
window.open(`${protocol}://${domain}:${app.info.port}${app.info.url_suffix || ''}`, '_blank', 'noreferrer');
url = `${protocol}://${domain}:${app.info.port}${app.info.url_suffix || ''}`;
}
if (type === 'domain' && app.domain) {
url = `https://${app.domain}${app.info.url_suffix || ''}`;
}
if (type === 'local_domain') {
url = `https://${app.id}.${getSettings.data?.localDomain}`;
}
window.open(url, '_blank', 'noreferrer');
};
const newVersion = [app?.latestDockerVersion ? `${app?.latestDockerVersion}` : '', `(${String(app?.latestVersion)})`].join(' ');
@ -170,14 +184,10 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
<span className="mt-1 me-1">{t('apps.app-details.version')}: </span>
<span className="badge bg-gray mt-2">{app.info.version}</span>
</div>
{app.domain && (
<a target="_blank" rel="noreferrer" className="mt-1" href={`https://${app.domain}`}>
https://{app.domain}
</a>
)}
<span className="mt-1 text-muted text-center mb-2">{app.info.short_desc}</span>
<div className="mb-1">{app.status !== 'missing' && <AppStatus status={app.status} />}</div>
<AppActions
localDomain={getSettings.data?.localDomain}
updateAvailable={updateAvailable}
onUpdate={updateDisclosure.open}
onUpdateSettings={updateSettingsDisclosure.open}
@ -187,7 +197,7 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
onInstall={installDisclosure.open}
onOpen={handleOpen}
onStart={handleStartSubmit}
info={app.info}
app={app}
status={app.status}
/>
</div>

View file

@ -56,10 +56,10 @@ describe('Test: SettingsForm', () => {
// arrange
render(<SettingsForm onSubmit={jest.fn()} />);
const submitButton = screen.getByRole('button', { name: 'Save' });
const dnsIpInput = screen.getByLabelText('DNS IP');
const domainInput = screen.getByLabelText('Domain name');
const internalIpInput = screen.getByLabelText('Internal IP');
const appsRepoUrlInput = screen.getByLabelText('Apps repo URL');
const dnsIpInput = screen.getByRole('textbox', { name: 'dnsIp' });
const domainInput = screen.getByRole('textbox', { name: 'domain' });
const internalIpInput = screen.getByRole('textbox', { name: 'internalIp' });
const appsRepoUrlInput = screen.getByRole('textbox', { name: 'appsRepoUrl' });
// act
fireEvent.change(dnsIpInput, { target: { value: 'invalid ip' } });

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable global-require */
import express, { Request } from 'express';
import express from 'express';
import { parse } from 'url';
import type { NextServer } from 'next/dist/server/next';
@ -44,7 +44,7 @@ nextApp.prepare().then(async () => {
app.use('/certificate', async (req, res) => {
const userId = req.session?.userId;
const user = await authService.getUserById(userId);
const user = await authService.getUserById(userId as number);
if (user?.operator) {
res.setHeader('Content-Dispositon', 'attachment; filename=cert.pem');

View file

@ -40,7 +40,7 @@ describe('Install app', () => {
const envFile = fs.readFileSync(`/app/storage/app-data/${appConfig.id}/app.env`).toString();
// assert
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${appConfig.port}`);
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nAPP_ID=${appConfig.id}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${appConfig.port}`);
});
it('Should add app in database', async () => {
@ -336,7 +336,7 @@ describe('Start app', () => {
const envFile = fs.readFileSync(`/app/storage/app-data/${appConfig.id}/app.env`).toString();
// assert
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${appConfig.port}`);
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nAPP_ID=${appConfig.id}\nTEST_FIELD=test\nAPP_DOMAIN=localhost:${appConfig.port}`);
});
it('Should throw if start script fails', async () => {
@ -395,7 +395,7 @@ describe('Update app config', () => {
const envFile = fs.readFileSync(`/app/storage/app-data/${appConfig.id}/app.env`).toString();
// assert
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nTEST_FIELD=${word}\nAPP_DOMAIN=localhost:${appConfig.port}`);
expect(envFile.trim()).toBe(`TEST=test\nAPP_PORT=${appConfig.port}\nAPP_ID=${appConfig.id}\nTEST_FIELD=${word}\nAPP_DOMAIN=localhost:${appConfig.port}`);
});
it('Should throw if required field is missing', async () => {

View file

@ -185,7 +185,7 @@ export class AppServiceClass {
*
* @param {string} id - The ID of the app to update.
* @param {object} form - The new configuration of the app.
* @param {boolean} [exposed=false] - If the app should be exposed or not.
* @param {boolean} [exposed] - If the app should be exposed or not.
* @param {string} [domain] - The domain for the app if exposed is true.
*/
public updateAppConfig = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string) => {

View file

@ -18,6 +18,8 @@ class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
// Mock localStorage
@ -41,6 +43,8 @@ const localStorageMock = (() => {
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
Object.defineProperty(window, 'ResizeObserver', { value: ResizeObserver });
Object.defineProperty(window, 'MutationObserver', { value: ResizeObserver });
Object.defineProperty(window, 'matchMedia', {
value: () => {
return {

View file

@ -2,9 +2,12 @@ import React, { FC, ReactElement } from 'react';
import { render, RenderOptions, renderHook } from '@testing-library/react';
import { Toaster } from 'react-hot-toast';
import { NextIntlProvider } from 'next-intl';
import ue from '@testing-library/user-event';
import { TRPCTestClientProvider } from './TRPCTestClientProvider';
import messages from '../src/client/messages/en.json';
const userEvent = ue.setup();
const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => (
<NextIntlProvider locale="en" messages={messages}>
<TRPCTestClientProvider>
@ -20,3 +23,4 @@ const customRenderHook = (callback: () => any, options?: Omit<RenderOptions, 'wr
export * from '@testing-library/react';
export { customRender as render };
export { customRenderHook as renderHook };
export { userEvent };