Skip to content

Commit

Permalink
fix: Use SSE for storage control updates (#7661)
Browse files Browse the repository at this point in the history
* chore: Add sync-storage-rule fetcher for StorageRuleChanged event

* chore: Update error message for restricted project creation

* chore: Add organization storage rule and sync storage rule routes

* chore: Add syncStorageRule function for syncing organization storage rule

* chore: Add sync-storage-rule fetcher for StorageRuleChanged event

* chore: Remove console.log statement in project route file

* chore: Cache organization storage rule in syncStorageRule function

* chore: Update project dropdown to show storage restriction message

* chore: Refactor organization storage loader to handle cached response with empty object

* chore: Update project route to handle create new project error

* chore: Removed promise all

* chore: Update organization storage loader to use in-memory cache for storage rules

* chore: Update project route to handle create new project error

* chore: Update organization storage loader to use organizationId as key in in-memory cache

* chore: Update organization storage loader to use organizationId as key in in-memory cache

* chore: Update project dropdown to show storage restriction message

* chore: Add validation for project type selection in ProjectDropdown component

* chore: Add validation for project type selection in ProjectDropdown component

* chore: Refactor project route to use OrgAndProjectData interface for last loaded org and project ID

* Refactor project route to use OrgAndProjectData interface for last loaded org and project ID

* Refactor project route to use OrgAndProjectData interface for last loaded org and project ID

* Refactor project route to use OrgAndProjectData interface for last loaded org and project ID

* Removed unused dependencies

* Change useEffect

* Refactor organization storage loader to return storagePromise instead of storage

* fix: del await in the loader

* chore: Add validation for project type selection in ProjectDropdown component

---------

Co-authored-by: Curry Yang <1019yanglu@gmail.com>
  • Loading branch information
pavkout and CurryYangxx committed Jul 16, 2024
1 parent c026e51 commit f92d78a
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 31 deletions.
17 changes: 12 additions & 5 deletions packages/insomnia/src/ui/components/dropdowns/project-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export const ProjectDropdown: FC<Props> = ({ project, organizationId, storage })
const isRemoteProjectInconsistent = isRemoteProject(project) && storage === 'local_only';
const isLocalProjectInconsistent = !isRemoteProject(project) && storage === 'cloud_only';
const isProjectInconsistent = isRemoteProjectInconsistent || isLocalProjectInconsistent;
const showStorageRestrictionMessage = storage !== 'cloud_plus_local';

const projectActionList: ProjectActionItem[] = [
{
id: 'settings',
Expand Down Expand Up @@ -187,9 +189,6 @@ export const ProjectDropdown: FC<Props> = ({ project, organizationId, storage })
<Icon icon="x" />
</Button>
</div>
{isDefaultOrganizationProject(project) && <p>
<Icon icon="info-circle" /> This is the default project for your organization. You can not delete it or change its type.
</p>}
<form
className='flex flex-col gap-4'
onSubmit={e => {
Expand All @@ -203,6 +202,14 @@ export const ProjectDropdown: FC<Props> = ({ project, organizationId, storage })
} else if (type === 'local' && project.remoteId && !projectType) {
setProjectType('local');
} else {
if (!type) {
showAlert({
title: 'Project type not selected',
message: 'Please select a project type before continuing',
});
return;
}

updateProjectFetcher.submit(formData, {
action: `/organization/${organizationId}/project/${project._id}/update`,
method: 'post',
Expand All @@ -227,7 +234,7 @@ export const ProjectDropdown: FC<Props> = ({ project, organizationId, storage })
className="py-1 placeholder:italic w-full pl-2 pr-7 rounded-sm border border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors"
/>
</TextField>
<RadioGroup name="type" defaultValue={project.remoteId ? 'remote' : 'local'} className="flex flex-col gap-2">
<RadioGroup name="type" defaultValue={storage === 'cloud_plus_local' ? project.remoteId ? 'remote' : 'local' : storage !== 'cloud_only' ? 'local' : 'remote'} className="flex flex-col gap-2">
<Label className="text-sm text-[--hl]">
Project type
</Label>
Expand Down Expand Up @@ -304,7 +311,7 @@ export const ProjectDropdown: FC<Props> = ({ project, organizationId, storage })
<div className="flex items-center gap-2 text-sm">
<Icon icon="info-circle" />
<span>
{isProjectInconsistent && `The organization owner mandates that projects must be created and stored ${storage.split('_').join(' ')}.`} You can optionally enable Git Sync
{showStorageRestrictionMessage && `The organization owner mandates that projects must be created and stored ${storage.split('_').join(' ')}.`} You can optionally enable Git Sync
</span>
</div>
<div className='flex items-center gap-2'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { createContext, type FC, type PropsWithChildren, useContext, useEffect, useState } from 'react';
import { useFetcher, useParams, useRevalidator, useRouteLoaderData } from 'react-router-dom';
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';

import { insomniaFetch } from '../../../ui/insomniaFetch';
import type { ProjectIdLoaderData } from '../../routes/project';
Expand Down Expand Up @@ -62,7 +62,7 @@ export interface UserPresence {
}

interface UserPresenceEvent extends UserPresence {
type: 'PresentUserLeave' | 'PresentStateChanged' | 'OrganizationChanged';
type: 'PresentUserLeave' | 'PresentStateChanged' | 'OrganizationChanged' | 'StorageRuleChanged';
}

export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children }) => {
Expand All @@ -83,9 +83,9 @@ export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children })

const [presence, setPresence] = useState<UserPresence[]>([]);
const syncOrganizationsFetcher = useFetcher();
const syncStorageRuleFetcher = useFetcher();
const syncProjectsFetcher = useFetcher();
const syncDataFetcher = useFetcher();
const { revalidate } = useRevalidator();

// Update presence when the user switches org, projects, workspaces
useEffect(() => {
Expand Down Expand Up @@ -120,7 +120,7 @@ export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children })

useEffect(() => {
const sessionId = userSession.id;
if (sessionId && remoteId) {
if (sessionId) {
try {
const source = new EventSource(`insomnia-event-source://v1/teams/${sanitizeTeamId(organizationId)}/streams?sessionId=${sessionId}`);

Expand All @@ -147,17 +147,24 @@ export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children })
action: '/organization/sync',
method: 'POST',
});
} else if (event.type === 'StorageRuleChanged' && event.team && event.team.includes('org_')) {
const orgId = event.team;

syncStorageRuleFetcher.submit({}, {
action: `/organization/${orgId}/sync-storage-rule`,
method: 'POST',
});
} else if (event.type === 'TeamProjectChanged' && event.team === organizationId) {
syncProjectsFetcher.submit({}, {
action: `/organization/${organizationId}/sync-projects`,
method: 'POST',
});
} else if (event.type === 'FileDeleted' && event.team === organizationId && event.project === remoteId) {
} else if (event.type === 'FileDeleted' && event.team === organizationId && remoteId && event.project === remoteId) {
syncProjectsFetcher.submit({}, {
action: `/organization/${organizationId}/sync-projects`,
method: 'POST',
});
} else if (['BranchDeleted', 'FileChanged'].includes(event.type) && event.team === organizationId && event.project === remoteId) {
} else if (['BranchDeleted', 'FileChanged'].includes(event.type) && event.team === organizationId && remoteId && event.project === remoteId) {
syncDataFetcher.submit({}, {
method: 'POST',
action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/insomnia-sync/sync-data`,
Expand All @@ -176,7 +183,7 @@ export const InsomniaEventStreamProvider: FC<PropsWithChildren> = ({ children })
}
}
return;
}, [organizationId, projectId, remoteId, revalidate, syncDataFetcher, syncOrganizationsFetcher, syncProjectsFetcher, userSession.id, workspaceId]);
}, [organizationId, projectId, remoteId, syncDataFetcher, syncOrganizationsFetcher, syncProjectsFetcher, syncStorageRuleFetcher, userSession.id, workspaceId]);

return (
<InsomniaEventStreamContext.Provider
Expand Down
14 changes: 14 additions & 0 deletions packages/insomnia/src/ui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,20 @@ async function renderApp() {
).organizationPermissionsLoader(...args),
shouldRevalidate: data => data.currentParams.organizationId !== data.nextParams.organizationId,
},
{
path: 'storage-rule',
loader: async (...args) =>
(
await import('./routes/organization')
).organizationStorageLoader(...args),
},
{
path: 'sync-storage-rule',
action: async (...args) =>
(
await import('./routes/organization')
).syncOrganizationStorageRuleAction(...args),
},
{
path: 'sync-projects',
action: async (...args) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/ui/routes/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const createNewProjectAction: ActionFunction = async ({ request, params }
}

if (newCloudProject.error === 'PROJECT_STORAGE_RESTRICTION') {
error = 'The owner of the organization allows only Local Vault project creation, please try again.';
error = newCloudProject.message ?? 'The owner of the organization allows only Local Vault project creation.';
}

return {
Expand Down
82 changes: 72 additions & 10 deletions packages/insomnia/src/ui/routes/organization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,22 @@ async function migrateProjectsUnderOrganization(personalOrganizationId: string,
}
};

async function syncStorageRule(sessionId: string, organizationId: string) {
try {
const storageRule = await insomniaFetch<StorageRule | undefined>({
method: 'GET',
path: `/v1/organizations/${organizationId}/storage-rule`,
sessionId,
});

invariant(storageRule, 'Failed to load storageRule');

inMemoryStorageRuleCache.set(organizationId, storageRule);
} catch (error) {
console.log('[storageRule] Failed to load storage rules', error);
}
}

export const indexLoader: LoaderFunction = async () => {
const { id: sessionId, accountId } = await userSession.getOrCreate();
if (sessionId) {
Expand Down Expand Up @@ -282,6 +298,20 @@ export const syncOrganizationsAction: ActionFunction = async () => {
return null;
};

export const syncOrganizationStorageRuleAction: ActionFunction = async ({ params }) => {
const { organizationId } = params;

invariant(organizationId, 'Organization ID is required');

const { id: sessionId } = await userSession.getOrCreate();

if (sessionId) {
await syncStorageRule(sessionId, organizationId);
}

return null;
};

export interface OrganizationLoaderData {
organizations: Organization[];
user?: UserProfileResponse;
Expand Down Expand Up @@ -324,6 +354,7 @@ export interface Billing {
isActive: boolean;
}

export const DefaultStorage = 'cloud_plus_local';
export interface StorageRule {
storage: 'cloud_plus_local' | 'cloud_only' | 'local_only';
isOverridden: boolean;
Expand All @@ -332,9 +363,50 @@ export interface StorageRule {
export interface OrganizationFeatureLoaderData {
featuresPromise: Promise<FeatureList>;
billingPromise: Promise<Billing>;
}
export interface OrganizationStorageLoaderData {
storagePromise: Promise<'cloud_plus_local' | 'cloud_only' | 'local_only'>;
}

// Create an in-memory storage to store the storage rules
export const inMemoryStorageRuleCache: Map<string, StorageRule> = new Map<string, StorageRule>();

export const organizationStorageLoader: LoaderFunction = async ({ params }): Promise<OrganizationStorageLoaderData> => {
const { organizationId } = params as { organizationId: string };
const { id: sessionId } = await userSession.getOrCreate();

const storageRule = inMemoryStorageRuleCache.get(organizationId);

if (storageRule) {
return {
storagePromise: Promise.resolve(storageRule.storage),
};
}

// Otherwise fetch from the API
try {
const storageRuleResponse = insomniaFetch<StorageRule | undefined>({
method: 'GET',
path: `/v1/organizations/${organizationId}/storage-rule`,
sessionId,
});

// Return the value
return {
storagePromise: storageRuleResponse.then(res => {
if (res) {
inMemoryStorageRuleCache.set(organizationId, res);
}
return res?.storage || DefaultStorage;
}),
};
} catch (err) {
return {
storagePromise: Promise.resolve(DefaultStorage),
};
}
};

export const organizationPermissionsLoader: LoaderFunction = async ({ params }): Promise<OrganizationFeatureLoaderData> => {
const { organizationId } = params as { organizationId: string };
const { id: sessionId, accountId } = await userSession.getOrCreate();
Expand All @@ -348,13 +420,10 @@ export const organizationPermissionsLoader: LoaderFunction = async ({ params }):
isActive: true,
};

const fallbackStorage = 'cloud_plus_local';

if (isScratchpadOrganizationId(organizationId)) {
return {
featuresPromise: Promise.resolve(fallbackFeatures),
billingPromise: Promise.resolve(fallbackBilling),
storagePromise: Promise.resolve(fallbackStorage),
};
}

Expand All @@ -372,21 +441,14 @@ export const organizationPermissionsLoader: LoaderFunction = async ({ params }):
sessionId,
});

const ruleResponse = insomniaFetch<StorageRule | undefined>({
method: 'GET',
path: `/v1/organizations/${organizationId}/storage-rule`,
sessionId,
});
return {
featuresPromise: featuresResponse.then(res => res?.features || fallbackFeatures),
billingPromise: featuresResponse.then(res => res?.billing || fallbackBilling),
storagePromise: ruleResponse.then(res => res?.storage || fallbackStorage),
};
} catch (err) {
return {
featuresPromise: Promise.resolve(fallbackFeatures),
billingPromise: Promise.resolve(fallbackBilling),
storagePromise: Promise.resolve(fallbackStorage),
};
}
};
Expand Down
Loading

0 comments on commit f92d78a

Please sign in to comment.