From 8900944da4f10b12e03798935e57059df5df7b8f Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Sat, 13 Jan 2024 11:52:16 +0100 Subject: [PATCH 1/5] chore: fixed issues with members view and email invite --- frontend/package.json | 2 +- frontend/public/messages/en.json | 12 +- frontend/src/api/project-users-api.spec.ts | 4 +- frontend/src/api/project-users-api.ts | 2 +- .../config-create-wizard/page.spec.tsx | 28 +- .../[locale]/projects/[projectId]/page.tsx | 4 +- frontend/src/components/modal.tsx | 28 +- ... project-members-invitation-form.spec.tsx} | 22 +- ...sx => project-members-invitation-form.tsx} | 64 ++-- .../[projectId]/project-members.spec.tsx | 22 +- .../projects/[projectId]/project-members.tsx | 351 ++++++++++-------- .../resource-deletion-banner.spec.tsx | 7 +- .../components/resource-deletion-banner.tsx | 40 +- frontend/src/styles/globals.scss | 6 +- go.mod | 4 +- go.sum | 4 + go.work.sum | 7 +- monorepo.iml | 2 + package.json | 6 +- pnpm-lock.yaml | 284 +++++++------- .../internal/api/projectusers.go | 13 +- services/openai-connector/package.json | 2 +- 22 files changed, 498 insertions(+), 416 deletions(-) rename frontend/src/components/projects/[projectId]/{invite-member.spec.tsx => project-members-invitation-form.spec.tsx} (91%) rename frontend/src/components/projects/[projectId]/{invite-project-members.tsx => project-members-invitation-form.tsx} (78%) diff --git a/frontend/package.json b/frontend/package.json index e7dd465c..9d751587 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "react-string-replace": "^1.1.1", "react-syntax-highlighter": "^15.5.0", "react-tailwindcss-datepicker": "^1.6.6", - "sharp": "^0.33.1", + "sharp": "^0.33.2", "swr": "^2.2.4", "validator": "^13.11.0", "zustand": "^4.4.7" diff --git a/frontend/public/messages/en.json b/frontend/public/messages/en.json index 98ea9848..89cbb965 100644 --- a/frontend/public/messages/en.json +++ b/frontend/public/messages/en.json @@ -225,6 +225,9 @@ }, "members": { "admin": "Admin", + "cancel": "Cancel", + "continue": "Continue", + "edit": "Edit", "emailAddress": "Email Address", "emailAddresses": "Email address(es)", "emailPlaceholder": "moishe.zuchmir@example.com", @@ -233,16 +236,13 @@ "member": "Member", "members": "Members", "name": "Name", - "ok": "Ok", - "removeMember": "Remove Member", + "remove": "Remove", + "removeUserWarningMessage": "Are you sure you want to remove {username} from the {projectName} project?", "role": "Role", "roleUpdated": "Role Updated", - "roles": "Roles", "sendInvite": "Send Invite", "userRemoved": "User Removed", - "usersInvited": "User(s) Invited", - "warning": "Warning", - "warningMessage": "Your about to remove a user access from this project" + "usersInvited": "User(s) Invited" }, "navbar": { "createNewProject": "Create New Project", diff --git a/frontend/src/api/project-users-api.spec.ts b/frontend/src/api/project-users-api.spec.ts index 2323545b..f507645a 100644 --- a/frontend/src/api/project-users-api.spec.ts +++ b/frontend/src/api/project-users-api.spec.ts @@ -5,7 +5,7 @@ import { handleAddUsersToProject, handleRemoveUserFromProject, handleRetrieveProjectUsers, - handleUpdateUserToPermission, + handleUpdateUserPermission, } from '@/api/index'; import { HttpMethod } from '@/constants'; import { AddUserToProjectBody } from '@/types'; @@ -99,7 +99,7 @@ describe('project users API tests', () => { userId: userAccount.id, }; - const data = await handleUpdateUserToPermission({ + const data = await handleUpdateUserPermission({ data: body, projectId: project.id, }); diff --git a/frontend/src/api/project-users-api.ts b/frontend/src/api/project-users-api.ts index cf866c7b..94be963f 100644 --- a/frontend/src/api/project-users-api.ts +++ b/frontend/src/api/project-users-api.ts @@ -31,7 +31,7 @@ export async function handleAddUsersToProject({ }); } -export async function handleUpdateUserToPermission({ +export async function handleUpdateUserPermission({ projectId, data, }: { diff --git a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.spec.tsx b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.spec.tsx index ed680246..de912d55 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.spec.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/applications/[applicationId]/config-create-wizard/page.spec.tsx @@ -179,29 +179,38 @@ describe('PromptConfigCreateWizard Page tests', () => { }); it('does not allow the user to save the config if messages are empty', async () => { + const promptConfig = OpenAIPromptConfigFactory.buildSync(); + mockFetch.mockResolvedValue({ + json: () => Promise.resolve(promptConfig), + ok: true, + }); + const store = getStore(); act(() => { store.setConfigName(faker.lorem.word()); + store.setMessages([]); + store.setParameters(promptConfig.modelParameters); + store.setModelType(promptConfig.modelType); + store.setModelVendor(promptConfig.modelVendor); + store.setNextWizardStage(); + store.setNextWizardStage(); }); render( , ); - const continueButton = screen.getByTestId( - 'config-create-wizard-continue-button', - ); - expect(continueButton).toBeInTheDocument(); - fireEvent.click(continueButton); - await waitFor(() => { expect( - screen.getByTestId('parameters-and-prompt-form-container'), + screen.getByTestId('prompt-config-testing-form'), ).toBeInTheDocument(); }); - expect(continueButton).toBeInTheDocument(); - expect(continueButton).toBeDisabled(); + const saveButton = screen.getByTestId( + 'config-create-wizard-save-button', + ); + expect(saveButton).toBeInTheDocument(); + expect(saveButton).toBeDisabled(); }); it('allows the user to continue to the third stage if messages are not empty', async () => { @@ -260,6 +269,7 @@ describe('PromptConfigCreateWizard Page tests', () => { store.setNextWizardStage(); store.setNextWizardStage(); }); + render( , ); diff --git a/frontend/src/app/[locale]/projects/[projectId]/page.tsx b/frontend/src/app/[locale]/projects/[projectId]/page.tsx index b4a60abe..180b2c86 100644 --- a/frontend/src/app/[locale]/projects/[projectId]/page.tsx +++ b/frontend/src/app/[locale]/projects/[projectId]/page.tsx @@ -7,12 +7,12 @@ import useSWR from 'swr'; import { handleRetrieveApplications } from '@/api'; import { Navbar } from '@/components/navbar'; -import { InviteProjectMembers } from '@/components/projects/[projectId]/invite-project-members'; import { ProjectAnalytics } from '@/components/projects/[projectId]/project-analytics'; import { ProjectApplicationsList } from '@/components/projects/[projectId]/project-applications-list'; import { ProjectDeletion } from '@/components/projects/[projectId]/project-deletion'; import { ProjectGeneralSettings } from '@/components/projects/[projectId]/project-general-settings'; import { ProjectMembers } from '@/components/projects/[projectId]/project-members'; +import { ProjectMembersInvitationForm } from '@/components/projects/[projectId]/project-members-invitation-form'; import { ProjectProviderKeys } from '@/components/projects/[projectId]/project-provider-keys'; import { TabData, TabNavigation } from '@/components/tab-navigation'; import { ProjectPageTabNames } from '@/constants'; @@ -39,7 +39,7 @@ const TabComponent = memo( ) : selectedTab === ProjectPageTabNames.MEMBERS ? (
- +
diff --git a/frontend/src/components/modal.tsx b/frontend/src/components/modal.tsx index 920deb90..8c2a0906 100644 --- a/frontend/src/components/modal.tsx +++ b/frontend/src/components/modal.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef } from 'react'; +import { useClickAway } from '@uidotdev/usehooks'; +import { LegacyRef, useEffect, useRef } from 'react'; export function Modal({ children, @@ -14,6 +15,10 @@ export function Modal({ onOpen?: () => void; }) { const dialogRef = useRef(null); + const clickAwayRef = useClickAway(() => { + dialogRef.current?.close(); + onClose?.(); + }); useEffect(() => { if (modalOpen) { @@ -26,13 +31,18 @@ export function Modal({ }, [modalOpen, dialogRef.current]); return ( - -
- {children} -
-
-
+
}> + +
+ {children} +
+
+
+
); } diff --git a/frontend/src/components/projects/[projectId]/invite-member.spec.tsx b/frontend/src/components/projects/[projectId]/project-members-invitation-form.spec.tsx similarity index 91% rename from frontend/src/components/projects/[projectId]/invite-member.spec.tsx rename to frontend/src/components/projects/[projectId]/project-members-invitation-form.spec.tsx index ee35c679..c1c1686d 100644 --- a/frontend/src/components/projects/[projectId]/invite-member.spec.tsx +++ b/frontend/src/components/projects/[projectId]/project-members-invitation-form.spec.tsx @@ -5,7 +5,7 @@ import { fireEvent, render, screen, waitFor } from 'tests/test-utils'; import { beforeEach, expect } from 'vitest'; import * as ProjectUsersAPI from '@/api/project-users-api'; -import { InviteProjectMembers } from '@/components/projects/[projectId]/invite-project-members'; +import { ProjectMembersInvitationForm } from '@/components/projects/[projectId]/project-members-invitation-form'; import { ApiError } from '@/errors'; import { ToastType } from '@/stores/toast-store'; import { AccessPermission } from '@/types'; @@ -26,7 +26,7 @@ describe('InviteMember', () => { }); it('renders invite member', async () => { - render(); + render(); const emailInput = screen.getByTestId('invite-email-input'); expect(emailInput).toBeInTheDocument(); @@ -51,7 +51,7 @@ describe('InviteMember', () => { ])( 'enables send invite only on valid email %p valid: %p', async (emails, valid) => { - render(); + render(); const sendInviteButton = screen.getByTestId('send-invite-btn'); @@ -73,7 +73,7 @@ describe('InviteMember', () => { ); it('changes role when a different role is chosen from dropdown', async () => { - render(); + render(); const permissionSelect = screen.getByTestId('permission-select'); @@ -89,7 +89,7 @@ describe('InviteMember', () => { }); it('sends invite to email and promptly clears the email input', async () => { - render(); + render(); const validEmail = faker.internet.email(); const emailInput = @@ -124,7 +124,7 @@ describe('InviteMember', () => { it('debounce invite button when loading', async () => { vi.resetAllMocks(); - render(); + render(); const validEmail = faker.internet.email(); const emailInput = @@ -148,7 +148,7 @@ describe('InviteMember', () => { }); it('throws error when user does not exist', async () => { - render(); + render(); const validEmail = faker.internet.email(); const emailInput = @@ -173,7 +173,7 @@ describe('InviteMember', () => { }); it('removes duplicate emails on user input', async () => { - render(); + render(); const validEmail = faker.internet.email(); const emailInput = @@ -214,7 +214,7 @@ describe('InviteMember', () => { }); it('disables submit when blank email is entered', async () => { - render(); + render(); const emailInput = screen.getByTestId('invite-email-input'); @@ -230,7 +230,7 @@ describe('InviteMember', () => { }); it('removes an email when clicked on cross button', async () => { - render(); + render(); const emailInput = screen.getByTestId('invite-email-input'); @@ -251,7 +251,7 @@ describe('InviteMember', () => { }); it('sendEmailInvites calls track with invite_user and email, permission and project', async () => { - render(); + render(); const validEmail = faker.internet.email(); const emailInput = screen.getByTestId('invite-email-input'); diff --git a/frontend/src/components/projects/[projectId]/invite-project-members.tsx b/frontend/src/components/projects/[projectId]/project-members-invitation-form.tsx similarity index 78% rename from frontend/src/components/projects/[projectId]/invite-project-members.tsx rename to frontend/src/components/projects/[projectId]/project-members-invitation-form.tsx index 664e60c1..705d9b50 100644 --- a/frontend/src/components/projects/[projectId]/invite-project-members.tsx +++ b/frontend/src/components/projects/[projectId]/project-members-invitation-form.tsx @@ -1,5 +1,6 @@ +import { useClickAway } from '@uidotdev/usehooks'; import { useTranslations } from 'next-intl'; -import { KeyboardEvent, useState } from 'react'; +import { KeyboardEvent, LegacyRef, useState } from 'react'; import { XCircleFill } from 'react-bootstrap-icons'; import isEmail from 'validator/es/lib/isEmail'; @@ -18,27 +19,26 @@ function EmailChip({ email: string; onRemove: () => void; }) { - const emailValid = isEmail(email); - return ( -
- {email} +
+ {email}
); } -export function InviteProjectMembers({ project }: { project: Project }) { +export function ProjectMembersInvitationForm({ + project, +}: { + project: Project; +}) { const t = useTranslations('members'); const setProjectUsers = useSetProjectUsers(); const handleError = useHandleError(); const showInfo = useShowInfo(); + const { initialized, track } = useAnalytics(); const [emails, setEmails] = useState([]); @@ -46,6 +46,10 @@ export function InviteProjectMembers({ project }: { project: Project }) { const [permission, setPermission] = useState(AccessPermission.MEMBER); const [isLoading, setIsLoading] = useState(false); + const ref = useClickAway(() => { + handleDeselect(); + }); + async function sendEmailInvites() { setIsLoading(true); try { @@ -79,20 +83,15 @@ export function InviteProjectMembers({ project }: { project: Project }) { } } - function addEmailToList() { - if (currentEmail && isEmail(currentEmail)) { - if (!emails.includes(currentEmail)) { - setEmails([...emails, currentEmail]); - } - + const handleDeselect = () => { + if ( + currentEmail && + isEmail(currentEmail) && + !emails.includes(currentEmail) + ) { + setEmails([...emails, currentEmail]); setCurrentEmail(''); } - } - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Enter') { - addEmailToList(); - } }; const removeEmail = (emailToRemove: string) => { @@ -113,9 +112,9 @@ export function InviteProjectMembers({ project }: { project: Project }) { {t('emailAddresses')} -
+
{emails.length !== 0 && ( -
+
{emails.map((email) => ( )} } type="email" id="email-address-input" data-testid="invite-email-input" - className="flex flex-1 input w-full bg-neutral text-neutral-content min-w-[9rem]" - placeholder={t('emailPlaceholder')} + className="input p-0 w-full bg-neutral text-neutral-content min-w-[9rem] border-none outline-none active:border-none" + placeholder={ + emails.length ? '' : t('emailPlaceholder') + } value={currentEmail} - onKeyDown={handleKeyDown} - onBlur={addEmailToList} + onKeyDown={(event: KeyboardEvent) => { + if (event.key === 'Enter') { + handleDeselect(); + } + }} + onBlur={handleDeselect} onChange={handleChange(setCurrentEmail)} />
diff --git a/frontend/src/components/projects/[projectId]/project-members.spec.tsx b/frontend/src/components/projects/[projectId]/project-members.spec.tsx index c0373cd4..851871d1 100644 --- a/frontend/src/components/projects/[projectId]/project-members.spec.tsx +++ b/frontend/src/components/projects/[projectId]/project-members.spec.tsx @@ -18,7 +18,7 @@ describe('ProjectMembers', () => { const handleUpdateUserToPermissionSpy = vi.spyOn( ProjectUsersAPI, - 'handleUpdateUserToPermission', + 'handleUpdateUserPermission', ); const handleRetrieveProjectUsersSpy = vi.spyOn( ProjectUsersAPI, @@ -54,7 +54,7 @@ describe('ProjectMembers', () => { const email = screen.getByText(projectUser.email); expect(email).toBeInTheDocument(); } - const editButtons = screen.getAllByTestId('remove-member-btn'); + const editButtons = screen.getAllByTestId('remove-project-user-button'); expect(editButtons.length).not.toBe(0); }); @@ -67,8 +67,9 @@ describe('ProjectMembers', () => { render(); - const removeMemberButtons = - screen.queryAllByTestId('remove-member-btn'); + const removeMemberButtons = screen.queryAllByTestId( + 'remove-project-user-button', + ); expect(removeMemberButtons.length).toBe(0); const permissionSelects = screen.queryAllByTestId('permission-select'); @@ -85,8 +86,9 @@ describe('ProjectMembers', () => { render(); await waitFor(() => { - const removeMemberButtons = - screen.queryAllByTestId('remove-member-btn'); + const removeMemberButtons = screen.queryAllByTestId( + 'remove-project-user-button', + ); expect(removeMemberButtons.length).toBe(0); }); @@ -154,7 +156,9 @@ describe('ProjectMembers', () => { let removeMemberButton: HTMLButtonElement; await waitFor(() => { - const [button] = screen.getAllByTestId('remove-member-btn'); + const [button] = screen.getAllByTestId( + 'remove-project-user-button', + ); expect(button).toBeInTheDocument(); removeMemberButton = button as HTMLButtonElement; @@ -185,7 +189,9 @@ describe('ProjectMembers', () => { let removeMemberButton: HTMLButtonElement; await waitFor(() => { - const [button] = screen.getAllByTestId('remove-member-btn'); + const [button] = screen.getAllByTestId( + 'remove-project-user-button', + ); expect(button).toBeInTheDocument(); removeMemberButton = button as HTMLButtonElement; }); diff --git a/frontend/src/components/projects/[projectId]/project-members.tsx b/frontend/src/components/projects/[projectId]/project-members.tsx index 4c7e1bac..dee67777 100644 --- a/frontend/src/components/projects/[projectId]/project-members.tsx +++ b/frontend/src/components/projects/[projectId]/project-members.tsx @@ -1,13 +1,13 @@ import Image from 'next/image'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; -import { Eraser } from 'react-bootstrap-icons'; +import { PencilFill, XCircle } from 'react-bootstrap-icons'; import useSWR from 'swr'; import { handleRemoveUserFromProject, handleRetrieveProjectUsers, - handleUpdateUserToPermission, + handleUpdateUserPermission, } from '@/api'; import { Modal } from '@/components/modal'; import { ResourceDeletionBanner } from '@/components/resource-deletion-banner'; @@ -21,66 +21,11 @@ import { useUser, } from '@/stores/api-store'; import { useShowInfo } from '@/stores/toast-store'; -import { AccessPermission, Project } from '@/types'; +import { AccessPermission, Project, ProjectUserAccount } from '@/types'; import { handleChange } from '@/utils/events'; const DEFAULT_AVATAR = '/images/avatar.svg'; -function UserCard({ - photoUrl, - displayName, - email, -}: { - displayName: string; - email: string; - photoUrl: string; -}) { - return ( -
- {displayName} -
-

{displayName}

-

{email}

-
-
- ); -} - -function PermissionSelect({ - permission, - onChange, -}: { - onChange: (value: AccessPermission) => void; - permission: AccessPermission; -}) { - const t = useTranslations('members'); - - return ( - - ); -} - export function ProjectMembers({ project }: { project: Project }) { const t = useTranslations('members'); @@ -93,7 +38,7 @@ export function ProjectMembers({ project }: { project: Project }) { const handleError = useHandleError(); const showInfo = useShowInfo(); - const { isLoading } = useSWR( + const { isLoading: isSwrLoading } = useSWR( { projectId: project.id, }, @@ -106,154 +51,242 @@ export function ProjectMembers({ project }: { project: Project }) { }, ); - const [isRemoveMemberModalOpen, setIsRemoveMemberModalOpen] = - useState(false); - const [removalUserId, setRemovalUserId] = useState(null); - const [removeUserLoading, setRemoveUserLoading] = useState(false); + const [subjectUser, setSubjectUser] = useState( + null, + ); + const [isRemoveModalOpen, setIsRemoveModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [newMemberPermission, setNewMemberPermission] = + useState(null); const currentUser = projectUsers?.find( (projectUser) => projectUser.email === user?.email, ); const isAdmin = currentUser?.permission === AccessPermission.ADMIN; - const showRemoveMemberColumn = + const showActionColumns = isAdmin && projectUsers && projectUsers.length > 1; - const canChangePermission = (memberId: string) => - isAdmin && currentUser.id !== memberId; - const canRemoveMember = (memberId: string) => - isAdmin && currentUser.id !== memberId; - async function updatePermission( - userId: string, - permission: AccessPermission, - ) { - const updatedProjectUser = await handleUpdateUserToPermission({ - data: { - permission, - userId, - }, - projectId: project.id, - }); - updateProjectUser(project.id, updatedProjectUser); - showInfo(t('roleUpdated')); - } + async function handleUpdatePermission() { + setIsLoading(true); - function markUserForRemoval(userId: string) { - setRemovalUserId(userId); - setIsRemoveMemberModalOpen(true); + try { + const updatedProjectUser = await handleUpdateUserPermission({ + data: { + permission: subjectUser!.permission, + userId: subjectUser!.id, + }, + projectId: project.id, + }); + updateProjectUser(project.id, updatedProjectUser); + showInfo(t('roleUpdated')); + } catch (e) { + handleError(e); + } finally { + setSubjectUser(null); + setNewMemberPermission(null); + setIsEditModalOpen(false); + setIsLoading(false); + } } - async function removeUser() { - if (!removalUserId || removeUserLoading) { - return; - } + const handleRemoveUser = async () => { + setIsLoading(true); try { - setRemoveUserLoading(true); await handleRemoveUserFromProject({ projectId: project.id, - userId: removalUserId, + userId: subjectUser!.id, }); - removeProjectUser(project.id, removalUserId); + removeProjectUser(project.id, subjectUser!.id); showInfo(t('userRemoved')); } catch (e) { handleError(e); } finally { - setRemovalUserId(null); - setIsRemoveMemberModalOpen(false); - setRemoveUserLoading(false); + setSubjectUser(null); + setIsRemoveModalOpen(false); + setIsLoading(false); } - } - - function renderProjectUsers() { - return projectUsers?.map( - ({ displayName, id: memberId, permission, photoUrl, email }) => { - return ( - - - - - - {canChangePermission(memberId) && ( - - void updatePermission( - memberId, - value as AccessPermission, - ) - } - /> - )} - {!canChangePermission(memberId) && ( -

- {t(permission.toLowerCase())} -

- )} - - {canRemoveMember(memberId) && ( - - - - )} - - ); - }, - ); - } + }; return (

{t('members')}

-
- {isLoading && ( +
+ {isSwrLoading ? (
- )} - {!isLoading && ( + ) : ( + - - {showRemoveMemberColumn && ( - + + + {showActionColumns && ( + <> + + + )} - {renderProjectUsers()} + + {projectUsers?.map((projectUser) => ( + + + + + + {showActionColumns && ( + <> + + + + )} + + ))} +
{t('name')}{t('roles')}{t('removeMember')}{t('emailAddress')}{t('role')}{t('edit')}{t('remove')}
+ {projectUser.displayName} + + {projectUser.displayName} + + {projectUser.email} + + {t( + projectUser.permission.toLowerCase(), + )} + + {projectUser.id !== + currentUser.id && ( + + )} + + {projectUser.id !== + currentUser.id && ( + + )} +
)}
- + { - setIsRemoveMemberModalOpen(false); + setIsRemoveModalOpen(false); + setSubjectUser(null); }} - onConfirm={() => void removeUser()} + onConfirm={() => void handleRemoveUser()} confirmCTA={ - removeUserLoading ? ( + isLoading ? ( ) : ( - t('ok') + t('continue') ) } /> + { + setIsEditModalOpen(false); + setSubjectUser(null); + }} + > +
+
+ +
+
+ + +
+
+
); } diff --git a/frontend/src/components/resource-deletion-banner.spec.tsx b/frontend/src/components/resource-deletion-banner.spec.tsx index a3a7de6e..b7c86c7f 100644 --- a/frontend/src/components/resource-deletion-banner.spec.tsx +++ b/frontend/src/components/resource-deletion-banner.spec.tsx @@ -1,12 +1,9 @@ import { fireEvent, render, screen } from 'tests/test-utils'; -import { - ResourceDeletionBanner, - ResourceDeletionBannerProps, -} from '@/components/resource-deletion-banner'; +import { ResourceDeletionBanner } from '@/components/resource-deletion-banner'; describe('ResourceDeletionBanner tests', () => { - const props: ResourceDeletionBannerProps = { + const props = { description: 'description', onCancel: vi.fn(), onConfirm: vi.fn(), diff --git a/frontend/src/components/resource-deletion-banner.tsx b/frontend/src/components/resource-deletion-banner.tsx index 6c484f4b..d9006b61 100644 --- a/frontend/src/components/resource-deletion-banner.tsx +++ b/frontend/src/components/resource-deletion-banner.tsx @@ -3,18 +3,6 @@ import { useState } from 'react'; import { handleChange } from '@/utils/events'; -export interface ResourceDeletionBannerProps { - confirmCTA?: string | React.ReactElement; - description: string; - errorMessage?: string; - isDisabled?: boolean; - onCancel: () => void; - onConfirm: () => void; - placeholder?: string; - resourceName?: string; - title: string; -} - export function ResourceDeletionBanner({ title, description, @@ -25,7 +13,17 @@ export function ResourceDeletionBanner({ confirmCTA, isDisabled, errorMessage, -}: ResourceDeletionBannerProps) { +}: { + confirmCTA?: string | React.ReactElement; + description: string; + errorMessage?: string; + isDisabled?: boolean; + onCancel: () => void; + onConfirm: () => void; + placeholder?: string; + resourceName?: string; + title?: string; +}) { const t = useTranslations('deletionBanner'); const [confirmText, setConfirmText] = useState(''); @@ -39,12 +37,14 @@ export function ResourceDeletionBanner({ resourceName && 'border-b border-neutral' }`} > -

- {title} -

+ {title && ( +

+ {title} +

+ )}

{confirmCTA ?? t('delete')} diff --git a/frontend/src/styles/globals.scss b/frontend/src/styles/globals.scss index bd79ae8c..1ea86aa1 100644 --- a/frontend/src/styles/globals.scss +++ b/frontend/src/styles/globals.scss @@ -48,11 +48,11 @@ div#__next > div { @layer components { .card-divider { - @apply mt-8; + @apply md:mt-8 sm:mt-4; } .card-section-divider { - @apply mt-4; + @apply md:mt-4 sm:mt-2; } .card-header { @@ -80,7 +80,7 @@ div#__next > div { } .rounded-data-card { - @apply mt-3.5 rounded-4xl w-full bg-base-200 px-16 pt-5 pb-5; + @apply rounded-4xl w-full bg-base-200 md:px-16 md:pt-5 md:pb-5 md:mt-3.5 sm:px-8 sm:pt-3 sm:pb-3 sm:mt-2; } .rounded-dark-card { @apply mt-3.5 rounded-4xl w-full bg-base-300 px-16 shadow-xl; diff --git a/go.mod b/go.mod index f531c947..dfc5e03f 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudevents/sdk-go/v2 v2.14.0 // indirect - github.com/containerd/containerd v1.7.11 // indirect + github.com/containerd/containerd v1.7.12 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect @@ -129,7 +129,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.18.0 // indirect - golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect diff --git a/go.sum b/go.sum index 89d54c10..0a119333 100644 --- a/go.sum +++ b/go.sum @@ -687,6 +687,8 @@ github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+g github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw= github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE= +github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= +github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -1218,6 +1220,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/go.work.sum b/go.work.sum index 48be9639..cbf54666 100644 --- a/go.work.sum +++ b/go.work.sum @@ -561,6 +561,8 @@ github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJ github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/container-orchestrated-devices/container-device-interface v0.5.4 h1:PqQGqJqQttMP5oJ/qNGEg8JttlHqGY3xDbbcKb5T9E8= github.com/container-orchestrated-devices/container-device-interface v0.5.4/go.mod h1:DjE95rfPiiSmG7uVXtg0z6MnPm/Lx4wxKCIts0ZE0vg= +github.com/container-orchestrated-devices/container-device-interface v0.6.1 h1:mz77uJoP8im/4Zins+mPqt677ZMaflhoGaYrRAl5jvA= +github.com/container-orchestrated-devices/container-device-interface v0.6.1/go.mod h1:40T6oW59rFrL/ksiSs7q45GzjGlbvxnA4xaK6cyq+kA= github.com/containerd/aufs v1.0.0 h1:2oeJiwX5HstO7shSrPZjrohJZLzK36wvpdmzDRkL/LY= github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= github.com/containerd/btrfs/v2 v2.0.0 h1:FN4wsx7KQrYoLXN7uLP0vBV4oVWHOIKDRQ1G2Z0oL5M= @@ -747,6 +749,8 @@ github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc= github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/mrunalp/fileutils v0.5.1 h1:F+S7ZlNKnrwHfSwdlgNSkKo67ReVf8o9fel6C3dkm/Q= github.com/mrunalp/fileutils v0.5.1/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -755,6 +759,8 @@ github.com/open-policy-agent/opa v0.42.2 h1:qocVAKyjrqMjCqsU02S/gHyLr4AQQ9xMtuV1 github.com/open-policy-agent/opa v0.42.2/go.mod h1:MrmoTi/BsKWT58kXlVayBb+rYVeaMwuBm3nYAN3923s= github.com/opencontainers/runtime-spec v1.1.0-rc.1 h1:wHa9jroFfKGQqFHj0I1fMRKLl0pfj+ynAqBxo3v6u9w= github.com/opencontainers/runtime-spec v1.1.0-rc.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0= github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= @@ -849,7 +855,6 @@ go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPi go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= diff --git a/monorepo.iml b/monorepo.iml index 1d7c23e5..c1d87d34 100644 --- a/monorepo.iml +++ b/monorepo.iml @@ -9,6 +9,8 @@ + + diff --git a/package.json b/package.json index 2ab0024d..bb36e979 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@types/webpack-node-externals": "^3.0.4", "@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/parser": "^6.18.1", - "@vitest/coverage-v8": "^1.1.3", + "@vitest/coverage-v8": "^1.2.0", "eslint": "^8.56.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-vitest": "^0.3.20", @@ -33,14 +33,14 @@ "pino": "^8.17.2", "pino-pretty": "^10.3.1", "pino-webpack-plugin": "^2.0.0", - "prettier": "^3.1.1", + "prettier": "^3.2.1", "prettier-plugin-sort-json": "^3.1.0", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", "tsconfig-paths-webpack-plugin": "^4.1.0", "typescript": "^5.3.3", "vite-tsconfig-paths": "^4.2.3", - "vitest": "^1.1.3", + "vitest": "^1.2.0", "vitest-mock-extended": "^1.3.1", "webpack": "^5.89.0", "webpack-cli": "^5.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d76e3bd9..f3c12807 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,7 +18,7 @@ importers: version: 1.9.13 '@prettier/plugin-xml': specifier: ^3.2.2 - version: 3.2.2(prettier@3.1.1) + version: 3.2.2(prettier@3.2.1) '@protobuf-ts/plugin': specifier: ^2.9.3 version: 2.9.3 @@ -30,7 +30,7 @@ importers: version: 2.9.3 '@tool-belt/eslint-config': specifier: ^5.0.4 - version: 5.0.4(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)(prettier@3.1.1)(typescript@5.3.3) + version: 5.0.4(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)(prettier@3.2.1)(typescript@5.3.3) '@types/node': specifier: 20.11.0 version: 20.11.0 @@ -47,8 +47,8 @@ importers: specifier: ^6.18.1 version: 6.18.1(eslint@8.56.0)(typescript@5.3.3) '@vitest/coverage-v8': - specifier: ^1.1.3 - version: 1.1.3(vitest@1.1.3) + specifier: ^1.2.0 + version: 1.2.0(vitest@1.2.0) eslint: specifier: ^8.56.0 version: 8.56.0 @@ -57,7 +57,7 @@ importers: version: 3.6.1(@typescript-eslint/parser@6.18.1)(eslint-plugin-import@2.29.1)(eslint@8.56.0) eslint-plugin-vitest: specifier: ^0.3.20 - version: 0.3.20(@typescript-eslint/eslint-plugin@6.18.1)(eslint@8.56.0)(typescript@5.3.3)(vitest@1.1.3) + version: 0.3.20(@typescript-eslint/eslint-plugin@6.18.1)(eslint@8.56.0)(typescript@5.3.3)(vitest@1.2.0) fork-ts-checker-webpack-plugin: specifier: ^9.0.2 version: 9.0.2(typescript@5.3.3)(webpack@5.89.0) @@ -80,11 +80,11 @@ importers: specifier: ^2.0.0 version: 2.0.0(webpack@5.89.0) prettier: - specifier: ^3.1.1 - version: 3.1.1 + specifier: ^3.2.1 + version: 3.2.1 prettier-plugin-sort-json: specifier: ^3.1.0 - version: 3.1.0(prettier@3.1.1) + version: 3.1.0(prettier@3.2.1) ts-loader: specifier: ^9.5.1 version: 9.5.1(typescript@5.3.3)(webpack@5.89.0) @@ -101,11 +101,11 @@ importers: specifier: ^4.2.3 version: 4.2.3(typescript@5.3.3) vitest: - specifier: ^1.1.3 - version: 1.1.3(@types/node@20.11.0) + specifier: ^1.2.0 + version: 1.2.0(@types/node@20.11.0) vitest-mock-extended: specifier: ^1.3.1 - version: 1.3.1(typescript@5.3.3)(vitest@1.1.3) + version: 1.3.1(typescript@5.3.3)(vitest@1.2.0) webpack: specifier: ^5.89.0 version: 5.89.0(webpack-cli@5.1.4) @@ -176,8 +176,8 @@ importers: specifier: ^1.6.6 version: 1.6.6(dayjs@1.11.10)(react@18.2.0) sharp: - specifier: ^0.33.1 - version: 0.33.1 + specifier: ^0.33.2 + version: 0.33.2 swr: specifier: ^2.2.4 version: 2.2.4(react@18.2.0) @@ -205,7 +205,7 @@ importers: version: 9.3.4 '@testing-library/jest-dom': specifier: ^6.2.0 - version: 6.2.0(vitest@1.1.3) + version: 6.2.0(vitest@1.2.0) '@testing-library/react': specifier: ^14.1.2 version: 14.1.2(react-dom@18.2.0)(react@18.2.0) @@ -285,8 +285,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 openai: - specifier: ^4.24.4 - version: 4.24.4 + specifier: ^4.24.7 + version: 4.24.7 pino: specifier: ^8.17.2 version: 8.17.2 @@ -328,10 +328,10 @@ packages: '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.21 - /@asamuzakjp/dom-selector@2.0.1: + /@asamuzakjp/dom-selector@2.0.2: resolution: { - integrity: sha512-QJAJffmCiymkv6YyQ7voyQb5caCth6jzZsQncYCpHXrJ7RqdYG5y43+is8mnFcYubdOkr7cn1+na9BdFMxqw7w==, + integrity: sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==, } dependencies: bidi-js: 1.0.3 @@ -2090,10 +2090,10 @@ packages: engines: { node: '>=10.0.0' } dev: true - /@emnapi/runtime@0.44.0: + /@emnapi/runtime@0.45.0: resolution: { - integrity: sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==, + integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==, } requiresBuild: true dependencies: @@ -3140,10 +3140,10 @@ packages: } dev: true - /@img/sharp-darwin-arm64@0.33.1: + /@img/sharp-darwin-arm64@0.33.2: resolution: { - integrity: sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw==, + integrity: sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==, } engines: { @@ -3157,14 +3157,14 @@ packages: os: [darwin] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.0 + '@img/sharp-libvips-darwin-arm64': 1.0.1 dev: false optional: true - /@img/sharp-darwin-x64@0.33.1: + /@img/sharp-darwin-x64@0.33.2: resolution: { - integrity: sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw==, + integrity: sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==, } engines: { @@ -3178,14 +3178,14 @@ packages: os: [darwin] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.0 + '@img/sharp-libvips-darwin-x64': 1.0.1 dev: false optional: true - /@img/sharp-libvips-darwin-arm64@1.0.0: + /@img/sharp-libvips-darwin-arm64@1.0.1: resolution: { - integrity: sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==, + integrity: sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==, } engines: { macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0' } @@ -3195,10 +3195,10 @@ packages: dev: false optional: true - /@img/sharp-libvips-darwin-x64@1.0.0: + /@img/sharp-libvips-darwin-x64@1.0.1: resolution: { - integrity: sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==, + integrity: sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==, } engines: { @@ -3213,10 +3213,10 @@ packages: dev: false optional: true - /@img/sharp-libvips-linux-arm64@1.0.0: + /@img/sharp-libvips-linux-arm64@1.0.1: resolution: { - integrity: sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==, + integrity: sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==, } engines: { @@ -3231,10 +3231,10 @@ packages: dev: false optional: true - /@img/sharp-libvips-linux-arm@1.0.0: + /@img/sharp-libvips-linux-arm@1.0.1: resolution: { - integrity: sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==, + integrity: sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==, } engines: { @@ -3249,10 +3249,10 @@ packages: dev: false optional: true - /@img/sharp-libvips-linux-s390x@1.0.0: + /@img/sharp-libvips-linux-s390x@1.0.1: resolution: { - integrity: sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==, + integrity: sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==, } engines: { @@ -3267,10 +3267,10 @@ packages: dev: false optional: true - /@img/sharp-libvips-linux-x64@1.0.0: + /@img/sharp-libvips-linux-x64@1.0.1: resolution: { - integrity: sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==, + integrity: sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==, } engines: { @@ -3285,10 +3285,10 @@ packages: dev: false optional: true - /@img/sharp-libvips-linuxmusl-arm64@1.0.0: + /@img/sharp-libvips-linuxmusl-arm64@1.0.1: resolution: { - integrity: sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==, + integrity: sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==, } engines: { @@ -3303,10 +3303,10 @@ packages: dev: false optional: true - /@img/sharp-libvips-linuxmusl-x64@1.0.0: + /@img/sharp-libvips-linuxmusl-x64@1.0.1: resolution: { - integrity: sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==, + integrity: sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==, } engines: { @@ -3321,10 +3321,10 @@ packages: dev: false optional: true - /@img/sharp-linux-arm64@0.33.1: + /@img/sharp-linux-arm64@0.33.2: resolution: { - integrity: sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw==, + integrity: sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==, } engines: { @@ -3338,14 +3338,14 @@ packages: os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.0 + '@img/sharp-libvips-linux-arm64': 1.0.1 dev: false optional: true - /@img/sharp-linux-arm@0.33.1: + /@img/sharp-linux-arm@0.33.2: resolution: { - integrity: sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA==, + integrity: sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==, } engines: { @@ -3359,14 +3359,14 @@ packages: os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.0 + '@img/sharp-libvips-linux-arm': 1.0.1 dev: false optional: true - /@img/sharp-linux-s390x@0.33.1: + /@img/sharp-linux-s390x@0.33.2: resolution: { - integrity: sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A==, + integrity: sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==, } engines: { @@ -3380,14 +3380,14 @@ packages: os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.0 + '@img/sharp-libvips-linux-s390x': 1.0.1 dev: false optional: true - /@img/sharp-linux-x64@0.33.1: + /@img/sharp-linux-x64@0.33.2: resolution: { - integrity: sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg==, + integrity: sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==, } engines: { @@ -3401,14 +3401,14 @@ packages: os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.0 + '@img/sharp-libvips-linux-x64': 1.0.1 dev: false optional: true - /@img/sharp-linuxmusl-arm64@0.33.1: + /@img/sharp-linuxmusl-arm64@0.33.2: resolution: { - integrity: sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA==, + integrity: sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==, } engines: { @@ -3422,14 +3422,14 @@ packages: os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.1 dev: false optional: true - /@img/sharp-linuxmusl-x64@0.33.1: + /@img/sharp-linuxmusl-x64@0.33.2: resolution: { - integrity: sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ==, + integrity: sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==, } engines: { @@ -3443,14 +3443,14 @@ packages: os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.0 + '@img/sharp-libvips-linuxmusl-x64': 1.0.1 dev: false optional: true - /@img/sharp-wasm32@0.33.1: + /@img/sharp-wasm32@0.33.2: resolution: { - integrity: sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA==, + integrity: sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==, } engines: { @@ -3462,14 +3462,14 @@ packages: cpu: [wasm32] requiresBuild: true dependencies: - '@emnapi/runtime': 0.44.0 + '@emnapi/runtime': 0.45.0 dev: false optional: true - /@img/sharp-win32-ia32@0.33.1: + /@img/sharp-win32-ia32@0.33.2: resolution: { - integrity: sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ==, + integrity: sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==, } engines: { @@ -3484,10 +3484,10 @@ packages: dev: false optional: true - /@img/sharp-win32-x64@0.33.1: + /@img/sharp-win32-x64@0.33.2: resolution: { - integrity: sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg==, + integrity: sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==, } engines: { @@ -3785,7 +3785,7 @@ packages: engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } dev: true - /@prettier/plugin-xml@3.2.2(prettier@3.1.1): + /@prettier/plugin-xml@3.2.2(prettier@3.2.1): resolution: { integrity: sha512-SoE70SQF1AKIvK7LVK80JcdAe6wrDcbodFFjcoqb1FkOqV0G0oSlgAFDwoRXPqkUE5p/YF2nGsnUbnfm6471sw==, @@ -3794,7 +3794,7 @@ packages: prettier: ^3.0.0 dependencies: '@xml-tools/parser': 1.0.11 - prettier: 3.1.1 + prettier: 3.2.1 dev: true /@protobuf-ts/plugin-framework@2.9.3: @@ -5729,7 +5729,7 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.2.0(vitest@1.1.3): + /@testing-library/jest-dom@6.2.0(vitest@1.2.0): resolution: { integrity: sha512-+BVQlJ9cmEn5RDMUS8c2+TU6giLvzaHZ8sU/x0Jj7fk+6/46wPdwlgOPcpxS17CjcanBi/3VmGMqVr2rmbUmNw==, @@ -5758,7 +5758,7 @@ packages: dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 - vitest: 1.1.3(@types/node@20.11.0) + vitest: 1.2.0(@types/node@20.11.0) dev: true /@testing-library/react@14.1.2(react-dom@18.2.0)(react@18.2.0): @@ -5778,7 +5778,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@tool-belt/eslint-config@5.0.4(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)(prettier@3.1.1)(typescript@5.3.3): + /@tool-belt/eslint-config@5.0.4(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0)(prettier@3.2.1)(typescript@5.3.3): resolution: { integrity: sha512-+RB0mObIdmL7xSVRE4qLs0oMMEHUsOwy20SuQ06gvFqOVo2cWENgSHOXx97dgaIn7/pEZ4EyQiqaer81SKAmtw==, @@ -5802,7 +5802,7 @@ packages: eslint-plugin-markdown: 3.0.1(eslint@8.56.0) eslint-plugin-n: 16.5.0(eslint@8.56.0) eslint-plugin-optimize-regex: 1.2.1 - eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.1.1) + eslint-plugin-prettier: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.2.1) eslint-plugin-promise: 6.1.1(eslint@8.56.0) eslint-plugin-react: 7.33.2(eslint@8.56.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.56.0) @@ -5813,7 +5813,7 @@ packages: eslint-plugin-typescript-sort-keys: 3.1.0(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3) eslint-plugin-unicorn: 50.0.1(eslint@8.56.0) eslint-plugin-unused-imports: 3.0.0(@typescript-eslint/eslint-plugin@6.18.1)(eslint@8.56.0) - prettier: 3.1.1 + prettier: 3.2.1 typescript: 5.3.3 transitivePeerDependencies: - '@types/eslint' @@ -6401,10 +6401,10 @@ packages: - supports-color dev: true - /@vitest/coverage-v8@1.1.3(vitest@1.1.3): + /@vitest/coverage-v8@1.2.0(vitest@1.2.0): resolution: { - integrity: sha512-Uput7t3eIcbSTOTQBzGtS+0kah96bX+szW9qQrLeGe3UmgL2Akn8POnyC2lH7XsnREZOds9aCUTxgXf+4HX5RA==, + integrity: sha512-YvX8ULTUm1+zkvkl14IqXYGxE1h13OXKPoDsxazARKlp4YLrP28hHEBdplaU7ZTN/Yn6zy6Z3JadWNRJwcmyrQ==, } peerDependencies: vitest: ^1.0.0 @@ -6422,37 +6422,37 @@ packages: std-env: 3.7.0 test-exclude: 6.0.0 v8-to-istanbul: 9.2.0 - vitest: 1.1.3(@types/node@20.11.0) + vitest: 1.2.0(@types/node@20.11.0) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@1.1.3: + /@vitest/expect@1.2.0: resolution: { - integrity: sha512-MnJqsKc1Ko04lksF9XoRJza0bGGwTtqfbyrsYv5on4rcEkdo+QgUdITenBQBUltKzdxW7K3rWh+nXRULwsdaVg==, + integrity: sha512-H+2bHzhyvgp32o7Pgj2h9RTHN0pgYaoi26Oo3mE+dCi1PAqV31kIIVfTbqMO3Bvshd5mIrJLc73EwSRrbol9Lw==, } dependencies: - '@vitest/spy': 1.1.3 - '@vitest/utils': 1.1.3 - chai: 4.4.0 + '@vitest/spy': 1.2.0 + '@vitest/utils': 1.2.0 + chai: 4.4.1 dev: true - /@vitest/runner@1.1.3: + /@vitest/runner@1.2.0: resolution: { - integrity: sha512-Va2XbWMnhSdDEh/OFxyUltgQuuDRxnarK1hW5QNN4URpQrqq6jtt8cfww/pQQ4i0LjoYxh/3bYWvDFlR9tU73g==, + integrity: sha512-vaJkDoQaNUTroT70OhM0NPznP7H3WyRwt4LvGwCVYs/llLaqhoSLnlIhUClZpbF5RgAee29KRcNz0FEhYcgxqA==, } dependencies: - '@vitest/utils': 1.1.3 + '@vitest/utils': 1.2.0 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.1.3: + /@vitest/snapshot@1.2.0: resolution: { - integrity: sha512-U0r8pRXsLAdxSVAyGNcqOU2H3Z4Y2dAAGGelL50O0QRMdi1WWeYHdrH/QWpN1e8juWfVKsb8B+pyJwTC+4Gy9w==, + integrity: sha512-P33EE7TrVgB3HDLllrjK/GG6WSnmUtWohbwcQqmm7TAk9AVHpdgf7M3F3qRHKm6vhr7x3eGIln7VH052Smo6Kw==, } dependencies: magic-string: 0.30.5 @@ -6460,19 +6460,19 @@ packages: pretty-format: 29.7.0 dev: true - /@vitest/spy@1.1.3: + /@vitest/spy@1.2.0: resolution: { - integrity: sha512-Ec0qWyGS5LhATFQtldvChPTAHv08yHIOZfiNcjwRQbFPHpkih0md9KAbs7TfeIfL7OFKoe7B/6ukBTqByubXkQ==, + integrity: sha512-MNxSAfxUaCeowqyyGwC293yZgk7cECZU9wGb8N1pYQ0yOn/SIr8t0l9XnGRdQZvNV/ZHBYu6GO/W3tj5K3VN1Q==, } dependencies: tinyspy: 2.2.0 dev: true - /@vitest/utils@1.1.3: + /@vitest/utils@1.2.0: resolution: { - integrity: sha512-Dyt3UMcdElTll2H75vhxfpZu03uFpXRCHxWnzcrFjZxT1kTbq8ALUYIeBgGolo1gldVdI0YSlQRacsqxTwNqwg==, + integrity: sha512-FyD5bpugsXlwVpTcGLDf3wSPYy8g541fQt14qtzo8mJ4LdEpDKZ9mQy2+qdJm2TZRpjY5JLXihXCgIxiRJgi5g==, } dependencies: diff-sequences: 29.6.3 @@ -7232,7 +7232,7 @@ packages: hasBin: true dependencies: caniuse-lite: 1.0.30001576 - electron-to-chromium: 1.4.629 + electron-to-chromium: 1.4.630 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.22.2) @@ -7327,10 +7327,10 @@ packages: integrity: sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==, } - /chai@4.4.0: + /chai@4.4.1: resolution: { - integrity: sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==, + integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==, } engines: { node: '>=4' } dependencies: @@ -8133,10 +8133,10 @@ packages: } dev: true - /electron-to-chromium@1.4.629: + /electron-to-chromium@1.4.630: resolution: { - integrity: sha512-5UUkr3k3CZ/k+9Sw7vaaIMyOzMC0XbPyprKI3n0tbKDqkzTDOjK4izm7DxlkueRMim6ZZQ1ja9F7hoFVplHihA==, + integrity: sha512-osHqhtjojpCsACVnuD11xO5g9xaCyw7Qqn/C2KParkMv42i8jrJJgx3g7mkHfpxwhy9MnOJr8+pKOdZ7qzgizg==, } /emoji-regex@8.0.0: @@ -8619,7 +8619,7 @@ packages: regexp-tree: 0.1.27 dev: true - /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.1.1): + /eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.2.1): resolution: { integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==, @@ -8638,7 +8638,7 @@ packages: dependencies: eslint: 8.56.0 eslint-config-prettier: 9.1.0(eslint@8.56.0) - prettier: 3.1.1 + prettier: 3.2.1 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 dev: true @@ -8816,7 +8816,7 @@ packages: eslint-rule-composer: 0.3.0 dev: true - /eslint-plugin-vitest@0.3.20(@typescript-eslint/eslint-plugin@6.18.1)(eslint@8.56.0)(typescript@5.3.3)(vitest@1.1.3): + /eslint-plugin-vitest@0.3.20(@typescript-eslint/eslint-plugin@6.18.1)(eslint@8.56.0)(typescript@5.3.3)(vitest@1.2.0): resolution: { integrity: sha512-O05k4j9TGMOkkghj9dRgpeLDyOSiVIxQWgNDPfhYPm5ioJsehcYV/zkRLekQs+c8+RBCVXucSED3fYOyy2EoWA==, @@ -8835,7 +8835,7 @@ packages: '@typescript-eslint/eslint-plugin': 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3) eslint: 8.56.0 - vitest: 1.1.3(@types/node@20.11.0) + vitest: 1.2.0(@types/node@20.11.0) transitivePeerDependencies: - supports-color - typescript @@ -10431,7 +10431,7 @@ packages: canvas: optional: true dependencies: - '@asamuzakjp/dom-selector': 2.0.1 + '@asamuzakjp/dom-selector': 2.0.2 cssstyle: 4.0.1 data-urls: 5.0.0 decimal.js: 10.4.3 @@ -11433,10 +11433,10 @@ packages: mimic-fn: 4.0.0 dev: true - /openai@4.24.4: + /openai@4.24.7: resolution: { - integrity: sha512-y58aEgq4HFCkfwiVCAvdxTkjGqHUWYFx4vJP6FWmH5udJn5SooZNXn+0N6uAe0ShmUT6846aZy3boJ0iN/KWvw==, + integrity: sha512-JUesECWPtsDHO0TlZGb6q73hnAmXUdzj9NrwgZeL4lqlRt/kR1sWrXoy8LocxN/6uOtitywvcJqe0O1PLkG45g==, } hasBin: true dependencies: @@ -11914,7 +11914,7 @@ packages: fast-diff: 1.3.0 dev: true - /prettier-plugin-sort-json@3.1.0(prettier@3.1.1): + /prettier-plugin-sort-json@3.1.0(prettier@3.2.1): resolution: { integrity: sha512-eIDEUjwzekiVd+oKrpd0aoACBTp5zOW71wDTNy+qQ5C9Q8oqt9n9wCm4F+SeRZbXfgblh/WYIguJynImlBXrvQ==, @@ -11923,13 +11923,13 @@ packages: peerDependencies: prettier: ^3.0.0 dependencies: - prettier: 3.1.1 + prettier: 3.2.1 dev: true - /prettier@3.1.1: + /prettier@3.2.1: resolution: { - integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==, + integrity: sha512-qSUWshj1IobVbKc226Gw2pync27t0Kf0EdufZa9j7uBSJay1CC+B3K5lAAZoqgX3ASiKuWsk6OmzKRetXNObWg==, } engines: { node: '>=14' } hasBin: true @@ -12724,37 +12724,37 @@ packages: kind-of: 6.0.3 dev: true - /sharp@0.33.1: + /sharp@0.33.2: resolution: { - integrity: sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==, + integrity: sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==, } - engines: { libvips: '>=8.15.0', node: ^18.17.0 || ^20.3.0 || >=21.0.0 } + engines: { libvips: '>=8.15.1', node: ^18.17.0 || ^20.3.0 || >=21.0.0 } requiresBuild: true dependencies: color: 4.2.3 detect-libc: 2.0.2 semver: 7.5.4 optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.1 - '@img/sharp-darwin-x64': 0.33.1 - '@img/sharp-libvips-darwin-arm64': 1.0.0 - '@img/sharp-libvips-darwin-x64': 1.0.0 - '@img/sharp-libvips-linux-arm': 1.0.0 - '@img/sharp-libvips-linux-arm64': 1.0.0 - '@img/sharp-libvips-linux-s390x': 1.0.0 - '@img/sharp-libvips-linux-x64': 1.0.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.0 - '@img/sharp-libvips-linuxmusl-x64': 1.0.0 - '@img/sharp-linux-arm': 0.33.1 - '@img/sharp-linux-arm64': 0.33.1 - '@img/sharp-linux-s390x': 0.33.1 - '@img/sharp-linux-x64': 0.33.1 - '@img/sharp-linuxmusl-arm64': 0.33.1 - '@img/sharp-linuxmusl-x64': 0.33.1 - '@img/sharp-wasm32': 0.33.1 - '@img/sharp-win32-ia32': 0.33.1 - '@img/sharp-win32-x64': 0.33.1 + '@img/sharp-darwin-arm64': 0.33.2 + '@img/sharp-darwin-x64': 0.33.2 + '@img/sharp-libvips-darwin-arm64': 1.0.1 + '@img/sharp-libvips-darwin-x64': 1.0.1 + '@img/sharp-libvips-linux-arm': 1.0.1 + '@img/sharp-libvips-linux-arm64': 1.0.1 + '@img/sharp-libvips-linux-s390x': 1.0.1 + '@img/sharp-libvips-linux-x64': 1.0.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.1 + '@img/sharp-libvips-linuxmusl-x64': 1.0.1 + '@img/sharp-linux-arm': 0.33.2 + '@img/sharp-linux-arm64': 0.33.2 + '@img/sharp-linux-s390x': 0.33.2 + '@img/sharp-linux-x64': 0.33.2 + '@img/sharp-linuxmusl-arm64': 0.33.2 + '@img/sharp-linuxmusl-x64': 0.33.2 + '@img/sharp-wasm32': 0.33.2 + '@img/sharp-win32-ia32': 0.33.2 + '@img/sharp-win32-x64': 0.33.2 dev: false /shebang-command@2.0.0: @@ -13956,10 +13956,10 @@ packages: engines: { node: '>= 0.10' } dev: false - /vite-node@1.1.3(@types/node@20.11.0): + /vite-node@1.2.0(@types/node@20.11.0): resolution: { - integrity: sha512-BLSO72YAkIUuNrOx+8uznYICJfTEbvBAmWClY3hpath5+h1mbPS5OMn42lrTxXuyCazVyZoDkSRnju78GiVCqA==, + integrity: sha512-ETnQTHeAbbOxl7/pyBck9oAPZZZo+kYnFt1uQDD+hPReOc+wCjXw4r4jHriBRuVDB5isHmPXxrfc1yJnfBERqg==, } engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true @@ -14053,7 +14053,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest-mock-extended@1.3.1(typescript@5.3.3)(vitest@1.1.3): + /vitest-mock-extended@1.3.1(typescript@5.3.3)(vitest@1.2.0): resolution: { integrity: sha512-OpghYjh4BDuQ/Mzs3lFMQ1QRk9D8/2O9T47MLUA5eLn7K4RWIy+MfIivYOWEyxjTENjsBnzgMihDjyNalN/K0Q==, @@ -14064,13 +14064,13 @@ packages: dependencies: ts-essentials: 9.4.1(typescript@5.3.3) typescript: 5.3.3 - vitest: 1.1.3(@types/node@20.11.0) + vitest: 1.2.0(@types/node@20.11.0) dev: true - /vitest@1.1.3(@types/node@20.11.0): + /vitest@1.2.0(@types/node@20.11.0): resolution: { - integrity: sha512-2l8om1NOkiA90/Y207PsEvJLYygddsOyr81wLQ20Ra8IlLKbyQncWsGZjnbkyG2KwwuTXLQjEPOJuxGMG8qJBQ==, + integrity: sha512-Ixs5m7BjqvLHXcibkzKRQUvD/XLw0E3rvqaCMlrm/0LMsA0309ZqYvTlPzkhh81VlEyVZXFlwWnkhb6/UMtcaQ==, } engines: { node: ^18.0.0 || >=20.0.0 } hasBin: true @@ -14096,14 +14096,14 @@ packages: optional: true dependencies: '@types/node': 20.11.0 - '@vitest/expect': 1.1.3 - '@vitest/runner': 1.1.3 - '@vitest/snapshot': 1.1.3 - '@vitest/spy': 1.1.3 - '@vitest/utils': 1.1.3 + '@vitest/expect': 1.2.0 + '@vitest/runner': 1.2.0 + '@vitest/snapshot': 1.2.0 + '@vitest/spy': 1.2.0 + '@vitest/utils': 1.2.0 acorn-walk: 8.3.2 cac: 6.7.14 - chai: 4.4.0 + chai: 4.4.1 debug: 4.3.4 execa: 8.0.1 local-pkg: 0.5.0 @@ -14115,7 +14115,7 @@ packages: tinybench: 2.5.1 tinypool: 0.8.1 vite: 5.0.11(@types/node@20.11.0)(sass@1.69.7) - vite-node: 1.1.3(@types/node@20.11.0) + vite-node: 1.2.0(@types/node@20.11.0) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/services/dashboard-backend/internal/api/projectusers.go b/services/dashboard-backend/internal/api/projectusers.go index 50233b71..b8d63e8f 100644 --- a/services/dashboard-backend/internal/api/projectusers.go +++ b/services/dashboard-backend/internal/api/projectusers.go @@ -19,6 +19,7 @@ import ( "github.com/jackc/pgx/v5/pgtype" "github.com/rs/zerolog/log" "net/http" + "strings" "sync" "time" ) @@ -66,13 +67,21 @@ func handleInviteUsersToProject(w http.ResponseWriter, r *http.Request) { topic := pubsubutils.GetTopic(r.Context(), pubsubutils.EmailSenderPubSubTopicID) baseURL := fmt.Sprintf( - "https://%s:%d/v1%s?projectId=%s", + "https://%s/v1%s?projectId=%s", cfg.ServerHost, - cfg.ServerPort, InviteUserWebhookEndpoint, db.UUIDToString(&projectID), ) + if cfg.Environment == "development" && cfg.ServerHost == "localhost" { + baseURL = strings.Replace( + "https://localhost", + fmt.Sprintf("http://%s:%d", cfg.ServerHost, cfg.ServerPort), + baseURL, + 1, + ) + } + var wg sync.WaitGroup publishContext, cancel := context.WithTimeout( diff --git a/services/openai-connector/package.json b/services/openai-connector/package.json index 362e1620..b24ad4fd 100644 --- a/services/openai-connector/package.json +++ b/services/openai-connector/package.json @@ -14,7 +14,7 @@ "@protobuf-ts/runtime": "^2.9.3", "@protobuf-ts/runtime-rpc": "^2.9.3", "grpc-health-check": "^2.0.0", - "openai": "^4.24.4", + "openai": "^4.24.7", "pino": "^8.17.2", "tiktoken": "^1.0.11" } From a60a9bed3b8368aa694d0149988b2ac20eb8a42b Mon Sep 17 00:00:00 2001 From: Na'aman Hirschfeld Date: Sat, 13 Jan 2024 14:38:46 +0100 Subject: [PATCH 2/5] chore: update tests --- .../[projectId]/project-members.spec.tsx | 268 +++++++++++++----- .../projects/[projectId]/project-members.tsx | 16 +- 2 files changed, 205 insertions(+), 79 deletions(-) diff --git a/frontend/src/components/projects/[projectId]/project-members.spec.tsx b/frontend/src/components/projects/[projectId]/project-members.spec.tsx index 851871d1..2bd2b2b5 100644 --- a/frontend/src/components/projects/[projectId]/project-members.spec.tsx +++ b/frontend/src/components/projects/[projectId]/project-members.spec.tsx @@ -1,3 +1,4 @@ +import { act } from 'react-dom/test-utils'; import { ProjectFactory, ProjectUserAccountFactory } from 'tests/factories'; import { fireEvent, @@ -16,18 +17,11 @@ import { AccessPermission } from '@/types'; describe('ProjectMembers', () => { const project = ProjectFactory.buildSync(); - const handleUpdateUserToPermissionSpy = vi.spyOn( - ProjectUsersAPI, - 'handleUpdateUserPermission', - ); const handleRetrieveProjectUsersSpy = vi.spyOn( ProjectUsersAPI, 'handleRetrieveProjectUsers', ); - const handleRemoveUserFromProjectSpy = vi.spyOn( - ProjectUsersAPI, - 'handleRemoveUserFromProject', - ); + const projectUsers = ProjectUserAccountFactory.batchSync(2); projectUsers[0].permission = AccessPermission.ADMIN; @@ -40,7 +34,11 @@ describe('ProjectMembers', () => { const { result: { current: setUser }, } = renderHook(useSetUser); - setUser(adminUser); + + act(() => { + setUser(adminUser); + }); + handleRetrieveProjectUsersSpy.mockResolvedValueOnce(projectUsers); render(); @@ -58,158 +56,282 @@ describe('ProjectMembers', () => { expect(editButtons.length).not.toBe(0); }); - it('does not render edit button or permission select for non admins', async () => { + it('removes a user from project', async () => { + const handleRemoveUserFromProjectSpy = vi.spyOn( + ProjectUsersAPI, + 'handleRemoveUserFromProject', + ); + const { result: { current: setUser }, } = renderHook(useSetUser); - setUser(memberUser); + + act(() => { + setUser(adminUser); + }); + handleRetrieveProjectUsersSpy.mockResolvedValueOnce(projectUsers); render(); - const removeMemberButtons = screen.queryAllByTestId( - 'remove-project-user-button', + let removeMemberButton: HTMLButtonElement; + await waitFor(() => { + const [button] = screen.getAllByTestId( + 'remove-project-user-button', + ); + + expect(button).toBeInTheDocument(); + removeMemberButton = button as HTMLButtonElement; + }); + + fireEvent.click(removeMemberButton!); + + const confirmButton = screen.getByTestId( + 'resource-deletion-delete-btn', ); - expect(removeMemberButtons.length).toBe(0); + fireEvent.click(confirmButton); - const permissionSelects = screen.queryAllByTestId('permission-select'); - expect(permissionSelects.length).toBe(0); + expect(handleRemoveUserFromProjectSpy).toHaveBeenCalledWith({ + projectId: project.id, + userId: memberUser.id, + }); }); - it('does not render edit button or permission select for current admin user', async () => { + it('shows error when unable to remove user', async () => { + const handleRemoveUserFromProjectSpy = vi.spyOn( + ProjectUsersAPI, + 'handleRemoveUserFromProject', + ); const { result: { current: setUser }, } = renderHook(useSetUser); - setUser(adminUser); - handleRetrieveProjectUsersSpy.mockResolvedValueOnce([projectUsers[0]]); + + act(() => { + setUser(adminUser); + }); + + handleRetrieveProjectUsersSpy.mockResolvedValueOnce(projectUsers); render(); + let removeMemberButton: HTMLButtonElement; + await waitFor(() => { - const removeMemberButtons = screen.queryAllByTestId( + const [button] = screen.getAllByTestId( 'remove-project-user-button', ); - expect(removeMemberButtons.length).toBe(0); + expect(button).toBeInTheDocument(); + removeMemberButton = button as HTMLButtonElement; }); - const permissionSelects = screen.queryAllByTestId('permission-select'); - expect(permissionSelects.length).toBe(0); + fireEvent.click(removeMemberButton!); + + handleRemoveUserFromProjectSpy.mockImplementationOnce(() => { + throw new ApiError('unable to remove user', { + statusCode: 401, + statusText: 'Bad Request', + }); + }); + const confirmButton = screen.getByTestId( + 'resource-deletion-delete-btn', + ); + fireEvent.click(confirmButton); + + const errorToast = screen.getByText('unable to remove user'); + expect(errorToast).toBeInTheDocument(); }); - it('changes permission of a user by selecting one from dropdown', async () => { + it('closes the remove user modal when pressing cancel', async () => { + const handleRemoveUserFromProjectSpy = vi.spyOn( + ProjectUsersAPI, + 'handleRemoveUserFromProject', + ); const { result: { current: setUser }, } = renderHook(useSetUser); - setUser(adminUser); + + act(() => { + setUser(adminUser); + }); + handleRetrieveProjectUsersSpy.mockResolvedValueOnce(projectUsers); render(); - let permissionSelect: HTMLSelectElement; - + let removeMemberButton: HTMLButtonElement; await waitFor(() => { - const [select] = screen.getAllByTestId('permission-select'); - expect(select).toBeInTheDocument(); - permissionSelect = select as HTMLSelectElement; + const [button] = screen.getAllByTestId( + 'remove-project-user-button', + ); + + expect(button).toBeInTheDocument(); + removeMemberButton = button as HTMLButtonElement; }); - fireEvent.change(permissionSelect!, { - target: { value: AccessPermission.ADMIN }, + fireEvent.click(removeMemberButton!); + + const cancelButton = screen.getByTestId('resource-deletion-cancel-btn'); + fireEvent.click(cancelButton); + + expect(handleRemoveUserFromProjectSpy).not.toHaveBeenCalled(); + }); + + it('opens the edit user permission modal when pressing edit', async () => { + const { + result: { current: setUser }, + } = renderHook(useSetUser); + + act(() => { + setUser(adminUser); }); - handleUpdateUserToPermissionSpy.mockResolvedValueOnce(memberUser); + handleRetrieveProjectUsersSpy.mockResolvedValueOnce(projectUsers); - expect(handleUpdateUserToPermissionSpy).toHaveBeenCalledWith({ - data: { - permission: AccessPermission.ADMIN, - userId: memberUser.id, - }, - projectId: project.id, + render(); + + let editMemberButton: HTMLButtonElement; + await waitFor(() => { + const [button] = screen.getAllByTestId('edit-project-user-button'); + + expect(button).toBeInTheDocument(); + editMemberButton = button as HTMLButtonElement; }); + + fireEvent.click(editMemberButton!); + + const editModal = screen.getByTestId('edit-project-user-modal'); + expect(editModal).toBeInTheDocument(); }); - it('does not remove a user if removalUserId is null', async () => { + it('closes the edit user permission modal when pressing cancel', async () => { + const handleUpdateUserToPermissionSpy = vi.spyOn( + ProjectUsersAPI, + 'handleUpdateUserPermission', + ); const { result: { current: setUser }, } = renderHook(useSetUser); - setUser(adminUser); + + act(() => { + setUser(adminUser); + }); handleRetrieveProjectUsersSpy.mockResolvedValueOnce(projectUsers); render(); - const confirmButton = screen.getByTestId( - 'resource-deletion-delete-btn', + let editMemberButton: HTMLButtonElement; + await waitFor(() => { + const [button] = screen.getAllByTestId('edit-project-user-button'); + + expect(button).toBeInTheDocument(); + editMemberButton = button as HTMLButtonElement; + }); + + fireEvent.click(editMemberButton!); + + const cancelButton = screen.getByTestId( + 'edit-project-user-modal-cancel-button', ); - fireEvent.click(confirmButton); + fireEvent.click(cancelButton); - expect(handleRemoveUserFromProjectSpy).not.toHaveBeenCalled(); + expect(handleUpdateUserToPermissionSpy).not.toHaveBeenCalled(); }); - it('removes a user from project', async () => { + it('updates user permission when pressing continue', async () => { + const handleUpdateUserToPermissionSpy = vi.spyOn( + ProjectUsersAPI, + 'handleUpdateUserPermission', + ); const { result: { current: setUser }, } = renderHook(useSetUser); - setUser(adminUser); + + act(() => { + setUser(adminUser); + }); + handleRetrieveProjectUsersSpy.mockResolvedValueOnce(projectUsers); render(); - let removeMemberButton: HTMLButtonElement; + let editMemberButton: HTMLButtonElement; await waitFor(() => { - const [button] = screen.getAllByTestId( - 'remove-project-user-button', - ); + const [button] = screen.getAllByTestId('edit-project-user-button'); expect(button).toBeInTheDocument(); - removeMemberButton = button as HTMLButtonElement; + editMemberButton = button as HTMLButtonElement; }); - fireEvent.click(removeMemberButton!); + fireEvent.click(editMemberButton!); - const confirmButton = screen.getByTestId( - 'resource-deletion-delete-btn', + const permissionSelect = screen.getByTestId( + 'edit-project-user-modal-permission-select', ); - fireEvent.click(confirmButton); + fireEvent.change(permissionSelect, { + target: { value: AccessPermission.MEMBER }, + }); - expect(handleRemoveUserFromProjectSpy).toHaveBeenCalledWith({ + const continueButton = screen.getByTestId( + 'edit-project-user-modal-continue-button', + ); + fireEvent.click(continueButton); + + expect(handleUpdateUserToPermissionSpy).toHaveBeenCalledWith({ + data: { + permission: AccessPermission.MEMBER, + userId: memberUser.id, + }, projectId: project.id, - userId: memberUser.id, }); }); - it('shows error when unable to remove user', async () => { + it('shows error when unable to update user permission', async () => { + const handleUpdateUserToPermissionSpy = vi.spyOn( + ProjectUsersAPI, + 'handleUpdateUserPermission', + ); const { result: { current: setUser }, } = renderHook(useSetUser); - setUser(adminUser); + + act(() => { + setUser(adminUser); + }); + handleRetrieveProjectUsersSpy.mockResolvedValueOnce(projectUsers); render(); - let removeMemberButton: HTMLButtonElement; - + let editMemberButton: HTMLButtonElement; await waitFor(() => { - const [button] = screen.getAllByTestId( - 'remove-project-user-button', - ); + const [button] = screen.getAllByTestId('edit-project-user-button'); + expect(button).toBeInTheDocument(); - removeMemberButton = button as HTMLButtonElement; + editMemberButton = button as HTMLButtonElement; }); - fireEvent.click(removeMemberButton!); + fireEvent.click(editMemberButton!); - handleRemoveUserFromProjectSpy.mockImplementationOnce(() => { - throw new ApiError('unable to remove user', { + const permissionSelect = screen.getByTestId( + 'edit-project-user-modal-permission-select', + ); + fireEvent.change(permissionSelect, { + target: { value: AccessPermission.MEMBER }, + }); + + handleUpdateUserToPermissionSpy.mockImplementationOnce(() => { + throw new ApiError('unable to update user permission', { statusCode: 401, statusText: 'Bad Request', }); }); - const confirmButton = screen.getByTestId( - 'resource-deletion-delete-btn', + + const continueButton = screen.getByTestId( + 'edit-project-user-modal-continue-button', ); - fireEvent.click(confirmButton); + fireEvent.click(continueButton); - const errorToast = screen.getByText('unable to remove user'); + const errorToast = screen.getByText('unable to update user permission'); expect(errorToast).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/projects/[projectId]/project-members.tsx b/frontend/src/components/projects/[projectId]/project-members.tsx index dee67777..8522422e 100644 --- a/frontend/src/components/projects/[projectId]/project-members.tsx +++ b/frontend/src/components/projects/[projectId]/project-members.tsx @@ -63,10 +63,11 @@ export function ProjectMembers({ project }: { project: Project }) { const currentUser = projectUsers?.find( (projectUser) => projectUser.email === user?.email, ); - const isAdmin = currentUser?.permission === AccessPermission.ADMIN; const showActionColumns = - isAdmin && projectUsers && projectUsers.length > 1; + currentUser?.permission === AccessPermission.ADMIN && + projectUsers && + projectUsers.length > 1; async function handleUpdatePermission() { setIsLoading(true); @@ -241,10 +242,13 @@ export function ProjectMembers({ project }: { project: Project }) { setSubjectUser(null); }} > -

+