Skip to content
6 changes: 6 additions & 0 deletions packages/insomnia-smoke-test/server/insomnia-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,4 +635,10 @@ export default (app: Application) => {
app.delete('/v1/desktop/organizations/:organizationId/collaborators/:collaboratorId/unlink', (_req, res) => {
res.json(null);
});

app.post('/v1/organizations/:organizationId/check-seats', (_req, res) => {
res.json({
isAllowed: true,
});
});
};
2 changes: 2 additions & 0 deletions packages/insomnia/src/basic-components/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from '../ui/components/icon';
// https://fontawesome.com/search?q=user&ic=free&o=r
69 changes: 69 additions & 0 deletions packages/insomnia/src/basic-components/modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import classnames from 'classnames';
import type React from 'react';
import { Button, Dialog, Heading, Modal as RAModal, ModalOverlay } from 'react-aria-components';

import { Icon } from '~/basic-components/icon';

interface Props {
isOpen: boolean;
onClose?: () => void;
title?: React.ReactNode;
closable?: boolean;
className?: string;
}

export const Modal: React.FC<React.PropsWithChildren<Props>> = ({
isOpen,
onClose,
className,
title,
closable,
children,
}) => {
return (
<ModalOverlay
isOpen={isOpen}
onOpenChange={isOpen => {
!isOpen && onClose?.();
}}
isDismissable
className="fixed left-0 top-0 z-10 flex h-[--visual-viewport-height] w-full items-center justify-center bg-black/30"
>
<RAModal
onOpenChange={isOpen => {
!isOpen && onClose?.();
}}
className={classnames(
'flex flex-col rounded-md border border-solid border-[--hl-sm] bg-[--color-bg] p-[--padding-lg] text-[--color-font]',
className,
)}
>
<Dialog className="flex h-full flex-1 flex-col overflow-hidden outline-none">
{({ close }) => (
<>
<div className="flex flex-1 flex-col gap-4 overflow-hidden">
{' '}
<div className="flex flex-shrink-0 items-center justify-between gap-2">
{title && (
<Heading slot="title" className="text-3xl">
{title}
</Heading>
)}
{closable && (
<Button
className="flex aspect-square h-6 flex-shrink-0 items-center justify-center rounded-sm text-sm text-[--color-font] ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md] aria-pressed:bg-[--hl-sm]"
onPress={() => close()}
>
<Icon icon="x" />
</Button>
)}
</div>
</div>
{children}
</>
)}
</Dialog>
</RAModal>
</ModalOverlay>
);
};
32 changes: 32 additions & 0 deletions packages/insomnia/src/basic-components/progress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import classNames from 'classnames';

interface Props {
percent: number;
className?: string;
status?: 'success' | 'error' | 'normal';
}

export const Progress = ({ className, percent, status }: Props) => {
return (
<div
// FIXME: use css variables for colors
className={classNames(
'h-[10px] flex-grow overflow-hidden rounded-full bg-[#f1e6ff]',
status === 'error' && 'bg-[#db110040]',
status === 'success' && 'bg-[#00bf7340]',
className,
)}
>
<div
className={classNames(
'transition-width h-full rounded-full bg-[--color-surprise] duration-1000 ease-in-out',
status === 'error' && 'bg-[--color-danger]',
status === 'success' && 'bg-[--color-success]',
)}
style={{
width: `${percent}%`,
}}
/>
</div>
);
};
3 changes: 3 additions & 0 deletions packages/insomnia/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,3 +558,6 @@ export const REALTIME_EVENTS_CHANNELS = {
READY_STATE: 'readyState',
NEW_EVENT: 'newEventReceived',
};

export const pricingLearnMoreLink =
'https://developer.konghq.com/insomnia/storage/#what-are-the-user-and-git-sync-limits-for-the-essentials-plan';
1 change: 1 addition & 0 deletions packages/insomnia/src/common/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ interface ResourceCacheType {

let resourceCacheList: ResourceCacheType[] = [];

// All models that can be exported should be listed here
export const MODELS_BY_EXPORT_TYPE: Record<AllExportTypes, AllTypes> = {
request: 'Request',
websocket_payload: 'WebSocketPayload',
Expand Down
4 changes: 3 additions & 1 deletion packages/insomnia/src/models/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export type PersonalPlanType = 'free' | 'individual' | 'team' | 'enterprise' | '
export const formatCurrentPlanType = (type: PersonalPlanType) => {
switch (type) {
case 'free': {
return 'Hobby';
return 'Essentials';
}
case 'individual': {
return 'Individual';
Expand Down Expand Up @@ -94,4 +94,6 @@ export interface CurrentPlan {
quantity: number;
type: PersonalPlanType;
planName: string;
status: 'trialing' | 'active';
trialingEnd: string;
}
5 changes: 5 additions & 0 deletions packages/insomnia/src/models/request-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { isRequest, type Request } from './request';
import { isSocketIORequest, type SocketIORequest } from './socket-io-request';
import { isWebSocketRequest, type WebSocketRequest } from './websocket-request';

/* When viewing a specific request, the user can click the Send button to test-send it.
Each time the user sends the request, the parameters may differ—they might edit the body, headers, and so on—and Insomnia records every sent request as history.
When the user browses the send history for a request and selects one of the entries, the current request is restored to the exact state it had when that request was sent, including the body, headers, and other settings.
A Request Version is essentially a snapshot of the request at the moment it was test-sent. */

export const name = 'Request Version';

export const type = 'RequestVersion';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { href } from 'react-router';
import { v4 as uuidv4 } from 'uuid';

import { userSession } from '~/models';
import { insomniaFetch } from '~/ui/insomniaFetch';
import { createFetcherLoadHook } from '~/utils/router';

import type { Route } from './+types/organization.$organizationId.collaborators-check-seats';

export const needsToUpgrade = 'NEEDS_TO_UPGRADE';
export const needsToIncreaseSeats = 'NEEDS_TO_INCREASE_SEATS';

export interface CheckSeatsResponse {
isAllowed: boolean;
code?: typeof needsToUpgrade | typeof needsToIncreaseSeats;
}

export async function clientLoader({ params }: Route.ClientLoaderArgs) {
const { id: sessionId } = await userSession.get();

const { organizationId } = params;

try {
const checkResponseData = await insomniaFetch<CheckSeatsResponse>({
method: 'POST',
path: `/v1/organizations/${organizationId}/check-seats`,
data: { emails: [`insomnia-mock-check-seats-${uuidv4()}@konghq.com`] },
sessionId,
onlyResolveOnSuccess: true,
});
return checkResponseData;
} catch {
return { isAllowed: true };
}
}

export const useCollaboratorsCheckSeatsLoaderFetcher = createFetcherLoadHook(
load =>
({ organizationId, query }: { organizationId: string; query?: string }) => {
return load(
`${href(`/organization/:organizationId/collaborators-check-seats`, { organizationId })}?${encodeURIComponent(query || '')}`,
);
},
clientLoader,
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { href, redirect } from 'react-router';
import { database } from '~/common/database';
import { projectLock } from '~/common/project';
import * as models from '~/models';
import { reportGitProjectCount } from '~/routes/organization.$organizationId.project.new';
import { insomniaFetch } from '~/ui/insomniaFetch';
import { invariant } from '~/utils/invariant';
import { createFetcherSubmitHook, getInitialRouteForOrganization } from '~/utils/router';
Expand Down Expand Up @@ -53,6 +54,8 @@ export async function clientAction({ params }: Route.ClientActionArgs) {

await database.flushChanges(bufferId);

project.gitRepositoryId && reportGitProjectCount(organizationId, sessionId);

// When redirect to `/organizations/:organizationId`, it sometimes doesn't reload the index loader, so manually redirect to the initial route for the organization
const initialOrganizationRoute = await getInitialRouteForOrganization({ organizationId });
return redirect(initialOrganizationRoute);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { OauthProviderName } from '~/models/git-credentials';
import type { GitCredentials } from '~/models/git-repository';
import { EMPTY_GIT_PROJECT_ID } from '~/models/project';
import type { WorkspaceMeta } from '~/models/workspace-meta';
import { reportGitProjectCount } from '~/routes/organization.$organizationId.project.new';
import { SegmentEvent } from '~/ui/analytics';
import { insomniaFetch } from '~/ui/insomniaFetch';
import { invariant } from '~/utils/invariant';
Expand Down Expand Up @@ -179,6 +180,8 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
}

await models.project.update(project, { name, remoteId: newCloudProject.id, gitRepositoryId: null });

project.gitRepositoryId && reportGitProjectCount(organizationId, sessionId);
return {
success: true,
};
Expand Down Expand Up @@ -278,6 +281,8 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
}
}

reportGitProjectCount(organizationId, sessionId);

return {
success: true,
};
Expand All @@ -290,6 +295,8 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
gitRepository && (await models.gitRepository.remove(gitRepository));
await models.project.update(project, { name, gitRepositoryId: null });

reportGitProjectCount(organizationId, sessionId);

return {
success: true,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { href, redirect } from 'react-router';

import { database } from '~/common/database';
import { isNotNullOrUndefined } from '~/common/misc';
import { projectLock } from '~/common/project';
import * as models from '~/models';
import type { GitCredentials, OauthProviderName } from '~/models/git-repository';
import { EMPTY_GIT_PROJECT_ID } from '~/models/project';
import { EMPTY_GIT_PROJECT_ID, type Project } from '~/models/project';
import { SegmentEvent } from '~/ui/analytics';
import { insomniaFetch } from '~/ui/insomniaFetch';
import { invariant } from '~/utils/invariant';
Expand All @@ -29,6 +31,34 @@ export interface CreateProjectData {
connectRepositoryLater?: boolean;
}

export const reportGitProjectCount = async (organizationId: string, sessionId: string, maxRetries = 3) => {
const projects = await database.find<Project>(models.project.type, {
parentId: organizationId,
});
const gitRepositoryIds = projects.map(p => p.gitRepositoryId).filter(isNotNullOrUndefined);
const gitProjectsCount = gitRepositoryIds.length;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await insomniaFetch({
method: 'PATCH',
path: `/v1/organizations/${organizationId}/git-projects`,
sessionId,
onlyResolveOnSuccess: true,
data: {
count: gitProjectsCount,
},
});
return;
} catch (err) {
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
}
}
}

console.warn('Report git project count failed');
};

export const createProject = async (organizationId: string, newProjectData: CreateProjectData) => {
const createProjectImpl = async (organizationId: string, newProjectData: CreateProjectData) => {
const user = await models.userSession.getOrCreate();
Expand All @@ -51,6 +81,7 @@ export const createProject = async (organizationId: string, newProjectData: Crea
parentId: organizationId,
gitRepositoryId: EMPTY_GIT_PROJECT_ID,
});
reportGitProjectCount(organizationId, sessionId);

return project._id;
}
Expand Down Expand Up @@ -91,6 +122,7 @@ export const createProject = async (organizationId: string, newProjectData: Crea
if (errors) {
throw new Error(errors.join(', '));
}
reportGitProjectCount(organizationId, sessionId);

return projectId;
}
Expand Down
15 changes: 10 additions & 5 deletions packages/insomnia/src/routes/organization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getLoginUrl } from '~/ui/auth-session-provider.client';
import { CommandPalette } from '~/ui/components/command-palette';
import { GitHubStarsButton } from '~/ui/components/github-stars-button';
import { HeaderInviteButton } from '~/ui/components/header-invite-button';
import { HeaderPlanIndicator } from '~/ui/components/header-plan-indicator';
import { HeaderUserButton } from '~/ui/components/header-user-button';
import { Hotkey } from '~/ui/components/hotkey';
import { Icon } from '~/ui/components/icon';
Expand Down Expand Up @@ -58,7 +59,6 @@ export async function clientLoader(_args: Route.ClientLoaderArgs) {
const organizations = JSON.parse(localStorage.getItem(`${accountId}:organizations`) || '[]') as Organization[];
const user = JSON.parse(localStorage.getItem(`${accountId}:user`) || '{}') as UserProfileResponse;
const currentPlan = JSON.parse(localStorage.getItem(`${accountId}:currentPlan`) || '{}') as CurrentPlan;

return {
organizations: sortOrganizations(accountId, organizations),
user,
Expand Down Expand Up @@ -240,12 +240,12 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
'organizationSidebarOpen',
true,
);
const [isMinimal, setIsMinimal] = reactUse.useLocalStorage('isMinimal', false);

useCloseConnection({
organizationId,
});

const [isMinimal, setIsMinimal] = reactUse.useLocalStorage('isMinimal', false);
return (
<InsomniaEventStreamProvider>
<InsomniaTabProvider>
Expand All @@ -262,11 +262,15 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
{!user ? <GitHubStarsButton /> : null}
</div>
<CommandPalette />
<div className="flex items-center justify-end gap-[--padding-sm] p-2">
<div className="flex min-w-min items-center justify-end gap-[--padding-sm] space-x-3 p-2">
{user ? (
<Fragment>
<PresentUsers />
<HeaderInviteButton className="border border-solid border-[--hl-md] bg-[rgba(var(--color-surprise-rgb),var(--tw-bg-opacity))] bg-opacity-100 font-semibold text-[--color-font-surprise]" />
<HeaderInviteButton
organizationId={organizationId}
className="border border-solid border-[--hl-md] bg-[rgba(var(--color-surprise-rgb),var(--tw-bg-opacity))] bg-opacity-100 font-semibold text-[--color-font-surprise]"
/>
<HeaderPlanIndicator isMinimal={isMinimal} />
<HeaderUserButton user={user} currentPlan={currentPlan} isMinimal={isMinimal} />
</Fragment>
) : (
Expand Down Expand Up @@ -583,7 +587,8 @@ const Component = ({ loaderData }: Route.ComponentProps) => {
{user ? (
<Fragment>
<PresentUsers />
<HeaderInviteButton className="text-[--color-font]" />
<HeaderInviteButton className="text-[--color-font]" organizationId={organizationId} />
<HeaderPlanIndicator isMinimal={isMinimal} />
<HeaderUserButton user={user} currentPlan={currentPlan} isMinimal={isMinimal} />
</Fragment>
) : (
Expand Down
Loading
Loading