fix: Manage Cookie Modal [INS-5007] (#8385)

* refactor: remove CookieList component

* refactor: remove unused defaultPerPage export from Invite modal

* refactor: remove unused styles for CookieList component

* refactor: clean up unused code in the cookie modal

* refactor: update CookiesModal props for better state management

* refactor: update CookiesModal props for improved state handling

* refactor: streamline state management in CookiesModal component

* refactor: update cookie editor test selectors for improved stability

* refactor: fix cookie editor test selector for accurate interaction

* refactor: update cookie editor test to use getByText for improved selector accuracy

* refactor: enhance cookie editor test to assert visibility of cookie entries

* refactor: update cookie editor test to assert visibility of specific cookie entries

* refactor: remove isOpen prop from CookiesModal for simplified usage
This commit is contained in:
Pavlos Koutoglou
2025-02-21 12:58:02 +02:00
committed by GitHub
parent 5203ce5bbc
commit 314dbb5db4
10 changed files with 533 additions and 517 deletions

View File

@@ -1,3 +1,5 @@
import { expect } from '@playwright/test';
import { loadFixture } from '../../playwright/paths';
import { test } from '../../playwright/test';
@@ -23,18 +25,18 @@ test.describe('Cookie editor', async () => {
await page.click('pre[role="presentation"]:has-text("bar")');
await page.locator('[data-testid="CookieValue"] >> textarea').nth(1).fill('123');
await page.locator('text=Done').nth(1).click();
await page.getByRole('cell', { name: 'foo=b123ar; Expires=' }).click();
await page.getByTestId('cookie-test-iteration-0').click();
// Create a new cookie
await page.locator('.cookie-list').getByRole('button', { name: 'Actions' }).click();
await page.getByRole('menuitem', { name: 'Add Cookie' }).click();
await page.getByRole('button', { name: 'Add Cookie' }).click();
await page.getByRole('button', { name: 'Edit' }).first().click();
// Try to replace text in Raw view
await page.getByRole('tab', { name: 'Raw' }).click();
await page.locator('text=Raw Cookie String >> input[type="text"]').fill('foo2=bar2; Expires=Tue, 19 Jan 2038 03:14:07 GMT; Domain=localhost; Path=/');
await page.locator('text=Done').nth(1).click();
await page.getByRole('cell', { name: 'foo2=bar2; Expires=' }).click();
await page.getByTestId('cookie-test-iteration-0').click();
await page.click('text=Done');
@@ -44,7 +46,7 @@ test.describe('Cookie editor', async () => {
// Check in the timeline that the cookie was sent
await page.getByRole('tab', { name: 'Console' }).click();
await page.click('text=foo2=bar2; foo=b123ar');
await expect(page.getByText('foo2=bar2')).toBeVisible();
// Send ws request
await page.getByLabel('Request Collection').getByTestId('example websocket').press('Enter');
@@ -53,7 +55,6 @@ test.describe('Cookie editor', async () => {
// Check in the timeline that the cookie was sent
await page.getByRole('tab', { name: 'Console' }).click();
await page.click('text=foo2=bar2; foo=b123ar;');
await expect(page.getByText('foo2=bar2')).toBeVisible();
});
});

View File

@@ -1,162 +0,0 @@
import { isValid } from 'date-fns';
import React, { type FC, useCallback, useState } from 'react';
import { Button } from 'react-aria-components';
import { Cookie as ToughCookie } from 'tough-cookie';
import { v4 as uuidv4 } from 'uuid';
import { cookieToString } from '../../common/cookies';
import type { Cookie } from '../../models/cookie-jar';
import { Dropdown, DropdownItem, ItemContent } from './base/dropdown';
import { PromptButton } from './base/prompt-button';
import { Icon } from './icon';
import { CookieModifyModal } from './modals/cookie-modify-modal';
import { RenderedText } from './rendered-text';
export interface CookieListProps {
handleCookieAdd: (cookie: Cookie) => void;
handleCookieDelete: (cookie: Cookie) => void;
handleDeleteAll: () => void;
cookies: Cookie[];
newCookieDomainName: string;
}
// Use tough-cookie MAX_DATE value
// https://github.com/salesforce/tough-cookie/blob/5ae97c6a28122f3fb309adcd8428274d9b2bd795/lib/cookie.js#L77
const MAX_TIME = 2147483647000;
const CookieRow: FC<{
cookie: Cookie;
deleteCookie: (cookie: Cookie) => void;
}> = ({ cookie, deleteCookie }) => {
const [isCookieModalOpen, setIsCookieModalOpen] = useState(false);
if (cookie.expires && !isValid(new Date(cookie.expires))) {
cookie.expires = null;
}
const c = ToughCookie.fromJSON(cookie);
const cookieString = c ? cookieToString(c) : '';
return <tr className="selectable" key={cookie.id}>
<td>
<RenderedText>{cookie.domain || ''}</RenderedText>
</td>
<td className="force-wrap wide">
<RenderedText>{cookieString || ''}</RenderedText>
</td>
<td onClick={() => { }} className="text-right no-wrap">
<button
className="btn btn--super-compact btn--outlined"
onClick={() => setIsCookieModalOpen(true)}
title="Edit cookie properties"
>
Edit
</button>{' '}
<PromptButton
className="btn btn--super-compact btn--outlined"
confirmMessage=""
onClick={() => deleteCookie(cookie)}
title="Delete cookie"
>
<i className="fa fa-trash-o" />
</PromptButton>
{isCookieModalOpen && (
<CookieModifyModal
cookie={cookie}
onHide={() => setIsCookieModalOpen(false)}
/>
)}
</td>
</tr>;
};
export const CookieList: FC<CookieListProps> = ({
cookies,
handleDeleteAll,
handleCookieAdd,
newCookieDomainName,
handleCookieDelete,
}) => {
const addCookie = useCallback(() => handleCookieAdd({
id: uuidv4(),
key: 'foo',
value: 'bar',
domain: newCookieDomainName,
expires: MAX_TIME as unknown as Date,
path: '/',
secure: false,
httpOnly: false,
}), [newCookieDomainName, handleCookieAdd]);
return <div>
<table className="table--fancy cookie-table table--striped">
<thead>
<tr>
<th
style={{
minWidth: '10rem',
}}
>
Domain
</th>
<th
style={{
width: '90%',
}}
>
Cookie
</th>
<th
style={{
width: '2rem',
}}
className="text-right"
>
<Dropdown
aria-label='Cookie Actions Dropdown'
triggerButton={
<Button
className="btn btn--super-super-compact btn--outlined txt-md"
>
Actions <i className="fa fa-caret-down" />
</Button>
}
>
<DropdownItem aria-label='Add Cookie'>
<ItemContent
icon="plus-circle"
label="Add Cookie"
onClick={addCookie}
/>
</DropdownItem>
<DropdownItem aria-label='Delete All'>
<ItemContent
icon="trash-o"
label="Delete All"
withPrompt
onClick={handleDeleteAll}
/>
</DropdownItem>
</Dropdown>
</th>
</tr>
</thead>
<tbody key={cookies.length}>
{cookies.map(cookie => (
<CookieRow
cookie={cookie}
key={cookie.id}
deleteCookie={handleCookieDelete}
/>
))}
</tbody>
</table>
{cookies.length === 0 && <div className="pad faint italic text-center">
<p>I couldn't find any cookies for you.</p>
<p className='pt-4'>
<Button className="border border-solid border-[--hl-lg] px-[--padding-md] h-[--line-height-xs] rounded-[--radius-md] hover:bg-[--hl-xs]" onPress={addCookie}>
<Icon icon="plus" /> Add Cookie
</Button>
</p>
</div>}
</div>;
};

View File

@@ -1,210 +0,0 @@
import clone from 'clone';
import { isValid } from 'date-fns';
import React, { useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components';
import { useRouteLoaderData } from 'react-router-dom';
import { useFetcher, useParams } from 'react-router-dom';
import { Cookie as ToughCookie } from 'tough-cookie';
import { cookieToString } from '../../../common/cookies';
import type { Cookie, CookieJar } from '../../../models/cookie-jar';
import type { WorkspaceLoaderData } from '../../routes/workspace';
import { Modal, type ModalHandle, type ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
import { OneLineEditor } from '../codemirror/one-line-editor';
export interface CookieModifyModalOptions {
cookie: Cookie;
}
export const CookieModifyModal = ((props: ModalProps & CookieModifyModalOptions) => {
const modalRef = useRef<ModalHandle>(null);
const [cookie, setCookie] = useState<Cookie | null>(props.cookie);
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const updateCookieJarFetcher = useFetcher<CookieJar>();
useEffect(() => {
modalRef.current?.show();
}, []);
const updateCookieJar = async (cookieJarId: string, patch: CookieJar) => {
updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), {
encType: 'application/json',
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/cookieJar/update`,
});
};
const handleCookieUpdate = async (nextCookie: any) => {
if (!cookie) {
return;
}
const newcookie = clone(nextCookie);
// transform to Date object or fallback to null
let dateFormat = null;
if (newcookie.expires && isValid(new Date(newcookie.expires))) {
dateFormat = new Date(newcookie.expires);
}
newcookie.expires = dateFormat;
setCookie(newcookie);
// Clone so we don't modify the original
const cookieJar = clone(activeCookieJar);
const index = activeCookieJar.cookies.findIndex(c => c.id === cookie.id);
if (index < 0) {
console.warn(`Could not find cookie with id=${cookie.id} to edit`);
return;
}
cookieJar.cookies = [...cookieJar.cookies.slice(0, index), newcookie, ...cookieJar.cookies.slice(index + 1)];
updateCookieJar(cookieJar._id, cookieJar);
};
let localDateTime;
if (cookie && cookie.expires && isValid(new Date(cookie.expires))) {
localDateTime = new Date(cookie.expires).toISOString().slice(0, 16);
}
let rawDefaultValue;
if (!cookie) {
rawDefaultValue = '';
} else {
try {
const c = ToughCookie.fromJSON(JSON.stringify(cookie));
rawDefaultValue = c ? cookieToString(c) : '';
} catch (err) {
console.warn('Failed to parse cookie string', err);
rawDefaultValue = '';
}
}
return (
<OverlayContainer>
<Modal ref={modalRef} onHide={props.onHide}>
<ModalHeader>Edit Cookie</ModalHeader>
<ModalBody className="cookie-modify">
{activeCookieJar && cookie && (
<Tabs aria-label='Cookie modify tabs' className="flex-1 w-full h-full flex flex-col">
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='friendly'
>
Friendly
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='raw'
>
Raw
</Tab>
</TabList>
<TabPanel className='w-full flex-1 flex flex-col overflow-y-auto' id='friendly'>
<div className="form-row">
<div className="form-control form-control--outlined">
<label data-testid="CookieKey">
Key
<OneLineEditor
id="cookie-key"
defaultValue={(cookie && cookie.key || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { key: value.trim() }))}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookieValue">
Value
<OneLineEditor
id="cookie-value"
defaultValue={(cookie && cookie.value || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { value: value.trim() }))}
/>
</label>
</div>
</div>
<div className="form-row">
<div className="form-control form-control--outlined">
<label data-testid="CookieDomain">
Domain
<OneLineEditor
id="cookie-domain"
defaultValue={(cookie && cookie.domain || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { domain: value.trim() }))}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookiePath">
Path
<OneLineEditor
id="cookie-path"
defaultValue={(cookie && cookie.path || '').toString()}
onChange={value => handleCookieUpdate(Object.assign({}, cookie, { path: value.trim() }))}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookieExpires">
Expires
<input type="datetime-local" defaultValue={localDateTime} onChange={event => handleCookieUpdate(Object.assign({}, cookie, { expires: event.target.value }))} />
</label>
</div>
<div className="grid grid-cols-2 gap-2 w-full">
<label>
Secure
<input
className="space-left"
type="checkbox"
name="secure"
defaultChecked={cookie.secure || false}
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { secure: event.target.checked }))}
/>
</label>
<label>
httpOnly
<input
className="space-left"
type="checkbox"
name="httpOnly"
defaultChecked={cookie.httpOnly || false}
onChange={event => handleCookieUpdate(Object.assign({}, cookie, { httpOnly: event.target.checked }))}
/>
</label>
</div>
</TabPanel>
<TabPanel className='w-full flex-1 flex flex-col overflow-y-auto' id='raw'>
<div className="form-control form-control--outlined">
<label>
Raw Cookie String
<input
type="text"
onChange={event => {
try {
// NOTE: Perform toJSON so we have a plain JS object instead of Cookie instance
const parsed = ToughCookie.parse(event.target.value, { loose: true })?.toJSON();
if (parsed) {
// Make sure cookie has an id
parsed.id = cookie.id;
handleCookieUpdate(parsed);
}
} catch (err) {
console.warn(`Failed to parse cookie string "${event.target.value}"`, err);
return;
}
}}
defaultValue={rawDefaultValue}
/>
</label>
</div>
</TabPanel>
</Tabs>
)}
</ModalBody>
<ModalFooter>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Modal>
</OverlayContainer>
);
});

View File

@@ -1,111 +1,541 @@
import React, { useEffect, useRef, useState } from 'react';
import { OverlayContainer } from 'react-aria';
import clone from 'clone';
import { isValid } from 'date-fns';
import React, { useState } from 'react';
import { Button, Dialog, Group, Heading, Input, ListBox, ListBoxItem, Modal, ModalOverlay, Tab, TabList, TabPanel, Tabs, TextField } from 'react-aria-components';
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
import { Cookie as ToughCookie } from 'tough-cookie';
import { v4 as uuidv4 } from 'uuid';
import { cookieToString } from '../../../common/cookies';
import { fuzzyMatch } from '../../../common/misc';
import type { Cookie, CookieJar } from '../../../models/cookie-jar';
import { useNunjucks } from '../../context/nunjucks/use-nunjucks';
import type { WorkspaceLoaderData } from '../../routes/workspace';
import { Modal, type ModalHandle, type ModalProps } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
import { CookieList } from '../cookie-list';
import { PromptButton } from '../base/prompt-button';
import { OneLineEditor } from '../codemirror/one-line-editor';
import { Icon } from '../icon';
import { RenderedText } from '../rendered-text';
export const CookiesModal = ({ onHide }: ModalProps) => {
const modalRef = useRef<ModalHandle>(null);
// Use tough-cookie MAX_DATE value
// https://github.com/salesforce/tough-cookie/blob/5ae97c6a28122f3fb309adcd8428274d9b2bd795/lib/cookie.js#L77
const MAX_TIME = 2147483647000;
const ItemsPerPage = 5;
const DefaultCookie: Cookie = {
id: uuidv4(),
key: 'foo',
value: 'bar',
domain: 'domain.com',
expires: MAX_TIME as unknown as Date,
path: '/',
secure: false,
httpOnly: false,
};
export function chunkArray<T>(array: T[], chunkSize: number = ItemsPerPage): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += chunkSize) {
chunks.push(array.slice(i, i + chunkSize));
}
return chunks;
}
interface Props {
setIsOpen: (isOpen: boolean) => void;
}
export const CookiesModal = ({ setIsOpen }: Props) => {
const { handleRender } = useNunjucks();
const [filter, setFilter] = useState<string>('');
const [visibleCookieIndexes, setVisibleCookieIndexes] = useState<number[] | null>(null);
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const updateCookieJarFetcher = useFetcher<CookieJar>();
useEffect(() => {
modalRef.current?.show();
}, []);
const updateCookieJar = async (cookieJarId: string, patch: CookieJar) => {
const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>();
const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
const updateCookieJarFetcher = useFetcher<CookieJar>();
const [page, setPage] = useState(0);
const [filter, setFilter] = useState<string>('');
const [filteredCookies, setFilteredCookies] = useState<Cookie[][]>(chunkArray(activeCookieJar?.cookies || []));
const updateCookieJar = (cookieJarId: string, patch: CookieJar) => {
updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), {
encType: 'application/json',
method: 'post',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/cookieJar/update`,
});
setFilteredCookies(chunkArray(patch.cookies));
};
const filteredCookies = visibleCookieIndexes ? (activeCookieJar?.cookies || []).filter((_, i) => visibleCookieIndexes.includes(i)) : (activeCookieJar?.cookies || []);
const handleFilterChange = async (value: string) => {
setFilter(value);
const renderedCookies: Cookie[] = [];
for (const cookie of (activeCookieJar?.cookies || [])) {
try {
renderedCookies.push(await handleRender(cookie));
} catch (err) {
renderedCookies.push(cookie);
}
}
if (!value) {
setFilteredCookies(chunkArray(renderedCookies));
return;
}
const filteredCookies: Cookie[] = [];
renderedCookies.forEach(cookie => {
if (fuzzyMatch(value, JSON.stringify(cookie), { splitSpace: true })) {
filteredCookies.push(cookie);
}
});
setFilteredCookies(chunkArray(filteredCookies));
};
const handleCookieDelete = (cookieId: string) => {
const updatedActiveCookieJar = activeCookieJar;
updatedActiveCookieJar.cookies = activeCookieJar.cookies.filter(c => c.id !== cookieId);
updateCookieJar(activeCookieJar._id, updatedActiveCookieJar);
};
const handleDeleteAll = () => {
const updatedActiveCookieJar = activeCookieJar;
updatedActiveCookieJar.cookies = [];
updateCookieJar(activeCookieJar._id, updatedActiveCookieJar);
};
const handleAddCookie = () => {
const updatedActiveCookieJar = activeCookieJar;
updatedActiveCookieJar.cookies = [DefaultCookie, ...activeCookieJar.cookies];
updateCookieJar(activeCookieJar._id, updatedActiveCookieJar);
};
const handleCookieUpdate = (cookie: Cookie) => {
const newCookie = clone(cookie);
// transform to Date object or fallback to null
let dateFormat = null;
if (newCookie.expires && isValid(new Date(newCookie.expires))) {
dateFormat = new Date(newCookie.expires);
}
newCookie.expires = dateFormat;
// Clone so we don't modify the original
const cookieJar = clone(activeCookieJar);
const index = activeCookieJar.cookies.findIndex(c => c.id === cookie.id);
if (index < 0) {
console.warn(`Could not find cookie with id=${cookie.id} to edit`);
return;
}
cookieJar.cookies = [...cookieJar.cookies.slice(0, index), newCookie, ...cookieJar.cookies.slice(index + 1)];
updateCookieJar(cookieJar._id, cookieJar);
};
return (
<OverlayContainer>
<Modal ref={modalRef} wide tall onHide={onHide}>
<ModalHeader>Manage Cookies</ModalHeader>
<ModalBody noScroll>
{activeCookieJar && (
<div className="cookie-list">
<div className="pad">
<div className="form-control form-control--outlined">
<label>
Filter Cookies
<input
onChange={async event => {
setFilter(event.target.value);
const renderedCookies: Cookie[] = [];
for (const cookie of (activeCookieJar?.cookies || [])) {
try {
renderedCookies.push(await handleRender(cookie));
} catch (err) {
renderedCookies.push(cookie);
}
}
if (!filter) {
setVisibleCookieIndexes(null);
}
const visibleCookieIndexes: number[] = [];
renderedCookies.forEach((cookie, i) => {
if (fuzzyMatch(filter, JSON.stringify(cookie), { splitSpace: true })) {
visibleCookieIndexes.push(i);
}
});
setVisibleCookieIndexes(visibleCookieIndexes);
}}
type="text"
placeholder="insomnia.rest"
defaultValue=""
/>
</label>
<ModalOverlay
isDismissable={true}
isOpen={true}
onOpenChange={setIsOpen}
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-center justify-center bg-[--color-bg] theme--transparent-overlay"
>
<Modal className="fixed top-[100px] w-full max-w-[900px] rounded-md border border-solid border-[--hl-sm] p-[32px] h-fit bg-[--color-bg] text-[--color-font] theme--dialog">
<Dialog className="outline-none relative">
{({ close }) => (
<>
{activeCookieJar && (
<div className="flex flex-col gap-4">
<Heading slot="title" className="text-[22px] leading-[34px] mb-[14px]">
Manage Cookies
</Heading>
<Button onPress={close} className="fa fa-times absolute top-0 right-0 text-xl" />
<div className='flex gap-4 justify-between'>
<Group
className="w-[50%] bg-[--hl-xs] py-[4px] px-[8px] rounded flex items-center gap-2"
>
<i className="fa fa-search" />
<TextField
value={filter}
onChange={handleFilterChange}
aria-label="Cookie search query"
className="flex-1"
>
<Input
className="w-full"
placeholder="Search cookies"
/>
</TextField>
{filter && (
<Button onPress={() => handleFilterChange('')}>
<Icon icon="circle-xmark" className='h-4 w-4' />
</Button>
)}
</Group>
<div className="flex gap-4 items-end">
<Button
className="flex items-center gap-2 min-w-[75px] py-1 px-2 font-semibold aria-pressed:bg-[--hl-sm] text-[--color-font] transition-all text-sm"
onPress={handleAddCookie}
>
<Icon icon="plus" /> Add Cookie
</Button>
<PromptButton
className="flex items-center gap-2 min-w-[85px] py-1 px-2 font-semibold aria-pressed:bg-[--hl-sm] text-[--color-font] transition-all text-sm"
confirmMessage='Confirm'
onClick={handleDeleteAll}
>
<Icon icon="trash" /> Delete All
</PromptButton>
</div>
</div>
<hr className="border my-[14px]" />
{filteredCookies.length === 0 ?
(<div className='flex items-center justify-center h-[200px]'>
<p className="text-[12px] text-[--color-font]">{filter ? `No cookies match your search: "${filter}"` : 'No cookies found.'}</p>
</div>)
: (
<>
<CookieList
cookies={filteredCookies[page] || []}
onCookieDelete={handleCookieDelete}
onUpdateCookie={handleCookieUpdate}
/>
<PaginationBar
isPrevDisabled={page === 0}
isNextDisabled={filteredCookies.length === 1 || page === filteredCookies.length - 1}
isHidden={filteredCookies.length === 1}
page={page + 1}
totalPages={filteredCookies.length}
onPrevPress={() => {
setPage(page - 1);
}}
onNextPress={() => {
setPage(page + 1);
}}
/>
</>
)
}
</div>
)}
<div className='flex items-center justify-between gap-3 mt-[2rem]'>
<div className="italic text-[12px]">
* cookies are automatically sent with relevant requests
</div>
<Button
className="text-[--color-font-surprise] font-semibold border border-solid border-[--hl-md] bg-opacity-100 bg-[rgba(var(--color-surprise-rgb),var(--tw-bg-opacity))] px-4 py-2 h-full flex items-center justify-center gap-2 aria-pressed:opacity-80 rounded-md hover:bg-opacity-80 focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
onPress={close}
>
Done
</Button>
</div>
<div className="cookie-list__list border-tops pad">
<CookieList
cookies={filteredCookies}
handleDeleteAll={() => {
const updated = activeCookieJar;
updated.cookies = [];
updateCookieJar(activeCookieJar._id, updated);
}}
handleCookieAdd={cookie => {
const updated = activeCookieJar;
updated.cookies = [cookie, ...activeCookieJar.cookies];
updateCookieJar(activeCookieJar._id, updated);
}}
handleCookieDelete={cookie => {
const updated = activeCookieJar;
updated.cookies = activeCookieJar.cookies.filter(c => c.id !== cookie.id);
updateCookieJar(activeCookieJar._id, updated);
}}
// Set the domain to the filter so that it shows up if we're filtering
newCookieDomainName={filter || 'domain.com'}
/>
</div>
</div>
</>
)}
</ModalBody>
<ModalFooter>
<div className="margin-left faint italic txt-sm">
* cookies are automatically sent with relevant requests
</div>
<button className="btn" onClick={() => modalRef.current?.hide()}>
Done
</button>
</ModalFooter>
</Dialog>
</Modal>
</OverlayContainer>
</ModalOverlay>
);
};
export interface CookieListProps {
cookies: Cookie[];
onCookieDelete: (cookieId: string) => void;
onUpdateCookie: (cookie: Cookie) => void;
}
const CookieList = ({ cookies, onCookieDelete, onUpdateCookie }: CookieListProps) => {
const [cookieToEdit, setCookieToEdit] = useState<Cookie | null>(null);
return (
<>
<ListBox
aria-label="Cookies list"
className="flex flex-col w-full min-h-[200px]"
>
{cookies.map((cookie, index) => {
const cookieJSON = ToughCookie.fromJSON(cookie);
const cookieString = cookieJSON ? cookieToString(cookieJSON) : '';
if (cookie.expires && !isValid(new Date(cookie.expires))) {
cookie.expires = null;
}
return (
<ListBoxItem
key={cookie.id}
id={cookie.id}
data-testid={`cookie-test-iteration-${index}`}
textValue={cookie.domain}
className='flex justify-between outline-none gap-2 leading-[36px] odd:bg-[--hl-xs] px-2 py-1 rounded-sm min-h-[40px]'
>
<span className='flex items-center leading-relaxed min-w-[20%]'><RenderedText>{cookie.domain || ''}</RenderedText></span>
<span className='flex items-center leading-relaxed w-[70%]'><RenderedText>{cookieString || ''}</RenderedText></span>
<div className='flex gap-1 min-w-[10%] items-center justify-end'>
<Button
className="flex items-center gap-2 min-w-[35px] py-1 px-2 justify-center font-semibold aria-pressed:bg-[--hl-sm] text-[--color-font] transition-all text-sm"
onPress={() => setCookieToEdit(cookie)}
>
Edit
</Button>
<PromptButton
className="flex items-center gap-2 min-w-[15px] py-1 px-2 font-semibold aria-pressed:bg-[--hl-sm] text-[--color-font] transition-all text-sm"
confirmMessage=""
doneMessage=''
onClick={() => onCookieDelete(cookie.id)}
title="Delete cookie"
>
<i className="fa fa-trash-o" />
</PromptButton>
</div>
</ListBoxItem>
);
})}
</ListBox>
{cookieToEdit && <CookieModifyModal
isOpen={cookieToEdit !== null}
cookie={cookieToEdit as Cookie}
setIsOpen={() => setCookieToEdit(null)}
onUpdateCookie={onUpdateCookie}
/>}
</>
);
};
interface PaginationBarProps {
isPrevDisabled?: boolean;
isNextDisabled?: boolean;
isHidden?: boolean;
page: number;
totalPages: number;
onPrevPress?: () => void;
onNextPress?: () => void;
};
const PaginationBar = ({ isNextDisabled, isPrevDisabled, isHidden, page, totalPages, onPrevPress, onNextPress }: PaginationBarProps) => {
if (isHidden) {
return null;
}
return (
<div className="flex flex-col items-end">
<div className="flex h-[50px] w-full flex-shrink-0 items-center justify-between">
<Button
isDisabled={isPrevDisabled}
aria-label="previous page"
className="flex h-[25px] items-center justify-center gap-[5px] p-1"
onPress={onPrevPress}
>
<Icon icon="arrow-left" className="h-[12px] w-[12px] text text-[--color-font] disabled:text-[#00000080]" />
<p className="m-0 text-[12px] font-normal capitalize leading-[15px] text-[--color-font] disabled:text-[#00000080]">Previous</p>
</Button>
<div className="flex gap-2 items-center">
<p className="m-0 text-[10px] font-normal leading-[15px] text-[--color-font] disabled:text-[#00000080]">{page}</p>
<p className="m-0 text-[10px] font-normal leading-[15px] text-[--color-font] disabled:text-[#00000080]">of</p>
<p className="m-0 text-[10px] font-normal leading-[15px] text-[--color-font] disabled:text-[#00000080]">{totalPages}</p>
</div>
<Button
isDisabled={isNextDisabled}
aria-label="next page"
className="flex h-[25px] items-center justify-center gap-[5px] p-1"
onPress={onNextPress}
>
<p className="m-0 text-[12px] font-normal capitalize leading-[15px] text-[--color-font] disabled:text-[#00000080]">Next</p>
<Icon icon="arrow-right" className="h-[12px] w-[12px] text-[--color-font] disabled:text-[#00000080]" />
</Button>
</div>
</div>
);
};
interface CookieModifyModalProps {
cookie: Cookie;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
onUpdateCookie: (cookie: Cookie) => void;
}
const CookieModifyModal = (({ cookie, isOpen, setIsOpen, onUpdateCookie }: CookieModifyModalProps) => {
const [editCookie, setEditCookie] = useState<Cookie>(cookie);
let localDateTime: string;
if (editCookie && editCookie.expires && isValid(new Date(editCookie.expires))) {
localDateTime = new Date(editCookie.expires).toISOString().slice(0, 16);
}
let rawDefaultValue;
if (!editCookie) {
rawDefaultValue = '';
} else {
try {
const c = ToughCookie.fromJSON(JSON.stringify(editCookie));
rawDefaultValue = c ? cookieToString(c) : '';
} catch (err) {
console.warn('Failed to parse cookie string', err);
rawDefaultValue = '';
}
}
return (
<ModalOverlay
isDismissable={true}
isOpen={isOpen}
onOpenChange={setIsOpen}
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-center justify-center bg-[--color-bg] theme--transparent-overlay"
>
<Modal className="fixed top-[100px] w-full max-w-[900px] rounded-md border border-solid border-[--hl-sm] p-[32px] h-fit bg-[--color-bg] text-[--color-font] theme--dialog">
<Dialog className="outline-none relative">
{({ close }) => (
<>
{editCookie && (
<>
<div className="flex flex-col gap-4">
<Heading slot="title" className="text-[22px] leading-[34px] mb-[14px]">
Manage Cookies
</Heading>
<Button onPress={close} className="fa fa-times absolute top-0 right-0 text-xl" />
<Tabs aria-label='Cookie modify tabs' className="flex-1 w-full h-full flex flex-col">
<TabList className='w-full flex-shrink-0 overflow-x-auto border-solid scro border-b border-b-[--hl-md] bg-[--color-bg] flex items-center h-[--line-height-sm]' aria-label='Request pane tabs'>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='friendly'
>
Friendly
</Tab>
<Tab
className='flex-shrink-0 h-full flex items-center justify-between cursor-pointer gap-2 outline-none select-none px-3 py-1 text-[--hl] aria-selected:text-[--color-font] hover:bg-[--hl-sm] hover:text-[--color-font] aria-selected:bg-[--hl-xs] aria-selected:focus:bg-[--hl-sm] aria-selected:hover:bg-[--hl-sm] focus:bg-[--hl-sm] transition-colors duration-300'
id='raw'
>
Raw
</Tab>
</TabList>
<TabPanel className='w-full flex-1 flex flex-col overflow-y-auto pt-3' id='friendly'>
<div className="form-row">
<div className="form-control form-control--outlined">
<label data-testid="CookieKey">
Key
<OneLineEditor
id="cookie-key"
defaultValue={(editCookie && editCookie.key || '').toString()}
onChange={value => setEditCookie({ ...editCookie, key: value.trim() })}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookieValue">
Value
<OneLineEditor
id="cookie-value"
defaultValue={(editCookie && editCookie.value || '').toString()}
onChange={value => setEditCookie({ ...editCookie, value: value.trim() })}
/>
</label>
</div>
</div>
<div className="form-row">
<div className="form-control form-control--outlined">
<label data-testid="CookieDomain">
Domain
<OneLineEditor
id="cookie-domain"
defaultValue={(editCookie && editCookie.domain || '').toString()}
onChange={value => setEditCookie({ ...editCookie, domain: value.trim() })}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookiePath">
Path
<OneLineEditor
id="cookie-path"
defaultValue={(editCookie && editCookie.path || '').toString()}
onChange={value => setEditCookie({ ...editCookie, path: value.trim() })}
/>
</label>
</div>
</div>
<div className="form-control form-control--outlined">
<label data-testid="CookieExpires">
Expires
<input
type="datetime-local"
defaultValue={localDateTime}
className='calendar-invert'
onChange={event => setEditCookie({ ...editCookie, expires: event.target.value })}
/>
</label>
</div>
<div className="grid grid-cols-2 gap-2 w-full">
<label className="flex items-center gap-1">
<input
className="space-left"
type="checkbox"
name="secure"
defaultChecked={editCookie.secure || false}
onChange={event => setEditCookie({ ...editCookie, secure: event.target.checked })}
/>
Secure
</label>
<label className="flex items-center gap-1">
<input
className="space-left"
type="checkbox"
name="httpOnly"
defaultChecked={editCookie.httpOnly || false}
onChange={event => setEditCookie({ ...editCookie, httpOnly: event.target.checked })}
/>
httpOnly
</label>
</div>
</TabPanel>
<TabPanel className='w-full flex-1 flex flex-col overflow-y-auto pt-3' id='raw'>
<div className="form-control form-control--outlined">
<label>
Raw Cookie String
<input
type="text"
onChange={event => {
try {
// NOTE: Perform toJSON so we have a plain JS object instead of Cookie instance
const parsed = ToughCookie.parse(event.target.value, { loose: true })?.toJSON();
if (parsed) {
// Make sure cookie has an id
parsed.id = editCookie.id;
setEditCookie(parsed as Cookie);
}
} catch (err) {
console.warn(`Failed to parse cookie string "${event.target.value}"`, err);
return;
}
}}
defaultValue={rawDefaultValue}
/>
</label>
</div>
</TabPanel>
</Tabs>
</div>
</>
)}
<div className='flex items-center justify-end mt-[2rem]'>
<Button
className="text-[--color-font-surprise] font-semibold border border-solid border-[--hl-md] bg-opacity-100 bg-[rgba(var(--color-surprise-rgb),var(--tw-bg-opacity))] px-4 py-2 h-full flex items-center justify-center gap-2 aria-pressed:opacity-80 rounded-md hover:bg-opacity-80 focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
onPress={() => {
onUpdateCookie(editCookie as Cookie);
setIsOpen(false);
}}
>
Done
</Button>
</div>
</>
)}
</Dialog>
</Modal>
</ModalOverlay>
);
});

View File

@@ -433,8 +433,6 @@ const MemberListItem: FC<{
);
};
export const defaultPerPage = 10;
interface PaginationBarProps {
isPrevDisabled?: boolean;
isNextDisabled?: boolean;

View File

@@ -65,9 +65,7 @@ export const ResponseCookiesViewer: FC<Props> = props => {
</button>
</p>
{isCookieModalOpen && (
<CookiesModal
onHide={() => setIsCookieModalOpen(false)}
/>
<CookiesModal setIsOpen={setIsCookieModalOpen} />
)}
</div>;
};

View File

@@ -1894,47 +1894,8 @@ html {
.changelog hr {
margin: var(--padding-lg) 0 !important;
}
.cookie-list {
height: 100%;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.cookie-list table input:not([type='checkbox']) {
padding: var(--padding-xs) var(--padding-xxs);
width: 100%;
background: none;
}
.cookie-list table .btn {
cursor: pointer;
margin: 0;
padding: 0 var(--padding-sm);
height: var(--line-height-xs);
}
.cookie-list .cookie-list__list {
height: 100%;
padding: 0 var(--padding-md) var(--padding-md) var(--padding-md);
position: relative;
overflow-y: auto;
}
.cookie-modify.modal__body {
overflow: visible;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.cookie-modify.modal__body table input:not([type='checkbox']) {
padding: var(--padding-xs) var(--padding-xxs);
width: 100%;
background: none;
}
.cookie-modify.modal__body table td {
cursor: pointer;
vertical-align: middle;
}
.cookie-modify.modal__body table .btn {
cursor: pointer;
margin: 0;
padding: 0 var(--padding-sm);
height: var(--line-height-xs);
.calendar-invert::-webkit-calendar-picker-indicator {
filter: invert(1);
}
.graphql-editor {
position: relative;

View File

@@ -1129,7 +1129,7 @@ export const Debug: FC = () => {
/>
)}
{isCookieModalOpen && (
<CookiesModal onHide={() => setIsCookieModalOpen(false)} />
<CookiesModal setIsOpen={setIsCookieModalOpen} />
)}
{isCertificatesModalOpen && (
<CertificatesModal onClose={() => setCertificatesModalOpen(false)} />

View File

@@ -991,7 +991,7 @@ const Design: FC = () => {
/>
)}
{isCookieModalOpen && (
<CookiesModal onHide={() => setIsCookieModalOpen(false)} />
<CookiesModal setIsOpen={setIsCookieModalOpen} />
)}
{isCertificatesModalOpen && (
<CertificatesModal onClose={() => setCertificatesModalOpen(false)} />

View File

@@ -464,7 +464,7 @@ const TestRoute: FC = () => {
/>
)}
{isCookieModalOpen && (
<CookiesModal onHide={() => setIsCookieModalOpen(false)} />
<CookiesModal setIsOpen={setIsCookieModalOpen} />
)}
{isCertificatesModalOpen && (
<CertificatesModal onClose={() => setCertificatesModalOpen(false)} />