Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

project loader unblock #7555

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/insomnia/src/models/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ export const isOwnerOfOrganization = ({
accountId: string;
}) =>
organization.metadata.ownerAccountId === accountId;

export const findPersonalOrganization = (organizations: Organization[], accountId: string) => {
return organizations.filter(isPersonalOrganization)
.find(organization =>
isOwnerOfOrganization({
organization,
accountId,
}));
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { database } from '../../common/database';
import * as models from '../../models';
import { Organization } from '../../models/organization';
import { Project, RemoteProject } from '../../models/project';

// Migration:
Expand Down Expand Up @@ -32,9 +31,9 @@ export const shouldMigrateProjectUnderOrganization = async () => {
};

export const migrateProjectsIntoOrganization = async ({
personalOrganization,
personalOrganizationId,
}: {
personalOrganization: Organization;
personalOrganizationId: string;
}) => {
// Legacy remote projects without organizations
// Local projects without organizations except scratchpad
Expand Down Expand Up @@ -65,7 +64,7 @@ export const migrateProjectsIntoOrganization = async ({
for (const localProject of localProjects) {
updatePromises.push(
models.project.update(localProject, {
parentId: personalOrganization.id,
parentId: personalOrganizationId,
})
);
}
Expand Down
24 changes: 24 additions & 0 deletions packages/insomnia/src/ui/hooks/use-loader-defer-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';

export const useLoaderDeferData = <T>(deferedDataPromise?: Promise<T>): [T | undefined, boolean, any] => {
const [data, setData] = useState<T>();
const [error, setError] = useState();
const [isPending, setIsPending] = useState(true);
useEffect(() => {
if (deferedDataPromise === undefined) {
return;
}
(async () => {
try {
const data = await deferedDataPromise;
setIsPending(false);
setData(data);
} catch (err) {
setError(err);
console.error('Failed to load defered data', err);
}
})();
}, [deferedDataPromise]);

return [data, isPending, error];
};
58 changes: 58 additions & 0 deletions packages/insomnia/src/ui/hooks/use-organization-async-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useEffect, useMemo } from 'react';
import { useFetcher, useLocation } from 'react-router-dom';

import { findPersonalOrganization, Organization } from '../../models/organization';
import { UserSession } from '../../models/user-session';
import { AsyncTask } from '../routes/organization';

interface OrganizationSync {
organizationId: string;
organizations: Organization[];
userSession: UserSession;
}

// this hook is used to run async task for organizations and projects (such as sync remote or migration)
// return status so that we can show loading or error message to user
export const useAsyncTask = ({
organizationId,
organizations,
userSession,
}: OrganizationSync) => {
const asyncTaskFetcher = useFetcher();
const location = useLocation();
console.log('location', location);

const asyncTaskStatus = useMemo(() => {
const data = asyncTaskFetcher.data;
console.log('fetcherdata', data, asyncTaskFetcher.state);
if (asyncTaskFetcher.state !== 'idle' && data?.error) {
return 'error';
}
return asyncTaskFetcher.state;
}, [asyncTaskFetcher.state, asyncTaskFetcher.data]);

const personalOrganization = findPersonalOrganization(organizations, userSession.accountId);

const asyncTaskList = location.state?.asyncTaskList as AsyncTask[];

useEffect(() => {
console.log('asyncTaskList', asyncTaskList);
if (asyncTaskList?.length) {
console.log('run async task in useEffect');
const submit = asyncTaskFetcher.submit;
submit({
sessionId: userSession.id,
accountId: userSession.accountId,
personalOrganizationId: personalOrganization?.id || '',
organizationId,
asyncTaskList: asyncTaskList,
}, {
action: '/organization/asyncTask',
method: 'POST',
encType: 'application/json',
});
}
}, [asyncTaskList, asyncTaskFetcher.submit, userSession.id, personalOrganization?.id, organizationId, userSession.accountId]);

return asyncTaskStatus;
};
33 changes: 5 additions & 28 deletions packages/insomnia/src/ui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { initNewOAuthSession } from '../network/o-auth-2/get-token';
import { init as initPlugins } from '../plugins';
import { applyColorScheme } from '../plugins/misc';
import { invariant } from '../utils/invariant';
import { getInitialEntry } from '../utils/router';
import { AppLoadingIndicator } from './components/app-loading-indicator';
import Auth from './routes/auth';
import Authorize from './routes/auth.authorize';
Expand Down Expand Up @@ -61,34 +62,6 @@ try {
console.log('Failed to parse session data', e);
}

async function getInitialEntry() {
// If the user has not seen the onboarding, then show it
// Otherwise if the user is not logged in and has not logged in before, then show the login
// Otherwise if the user is logged in, then show the organization
try {
const hasSeenOnboardingV9 = Boolean(window.localStorage.getItem('hasSeenOnboardingV9'));

if (!hasSeenOnboardingV9) {
return '/onboarding';
}

const hasUserLoggedInBefore = window.localStorage.getItem('hasUserLoggedInBefore');

const user = await models.userSession.getOrCreate();
if (user.id) {
return '/organization';
}

if (hasUserLoggedInBefore) {
return '/auth/login';
}

return '/organization/org_scratchpad/project/proj_scratchpad/workspace/wrk_scratchpad/debug';
} catch (e) {
return '/organization/org_scratchpad/project/proj_scratchpad/workspace/wrk_scratchpad/debug';
}
}

async function renderApp() {
await database.initClient();
await initPlugins();
Expand Down Expand Up @@ -201,6 +174,10 @@ async function renderApp() {
path: 'sync',
action: async (...args) => (await import('./routes/organization')).syncOrganizationsAction(...args),
},
{
path: 'asyncTask',
action: async (...args) => (await import('./routes/organization')).asyncTaskAction(...args),
},
{
path: ':organizationId',
id: ':organizationId',
Expand Down
135 changes: 98 additions & 37 deletions packages/insomnia/src/ui/routes/organization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { getAppWebsiteBaseURL } from '../../common/constants';
import { database } from '../../common/database';
import { userSession } from '../../models';
import { updateLocalProjectToRemote } from '../../models/helpers/project';
import { isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganizationId, Organization } from '../../models/organization';
import { findPersonalOrganization, isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganizationId, Organization } from '../../models/organization';
import { Project } from '../../models/project';
import { isDesign, isScratchpad } from '../../models/workspace';
import { VCSInstance } from '../../sync/vcs/insomnia-sync';
Expand All @@ -53,6 +53,8 @@ import { PresentUsers } from '../components/present-users';
import { Toast } from '../components/toast';
import { useAIContext } from '../context/app/ai-context';
import { InsomniaEventStreamProvider } from '../context/app/insomnia-event-stream-context';
import { useAsyncTask } from '../hooks/use-organization-async-task';
import { syncProjects } from './project';
import { useRootLoaderData } from './root';
import { UntrackedProjectsLoaderData } from './untracked-projects';
import { WorkspaceLoaderData } from './workspace';
Expand Down Expand Up @@ -108,6 +110,12 @@ interface CurrentPlan {
type: PersonalPlanType;
};

export const enum AsyncTask {
SyncOrganization,
MigrateProjects,
SyncProjects,
}

function sortOrganizations(accountId: string, organizations: Organization[]): Organization[] {
const home = organizations.find(organization => isPersonalOrganization(organization) && isOwnerOfOrganization({
organization,
Expand Down Expand Up @@ -164,52 +172,98 @@ async function syncOrganization(sessionId: string, accountId: string) {
}
}

interface AsyncTaskActionRequest {
sessionId: string;
accountId: string;
personalOrganizationId: string;
organizationId: string;
asyncTaskList: AsyncTask[];
}

// this action is used to run task that we dont want to block the UI
export const asyncTaskAction: ActionFunction = async ({ request }) => {
try {
const { sessionId, personalOrganizationId, organizationId, asyncTaskList, accountId } = await request.json() as AsyncTaskActionRequest;

const taskPromiseList = [];

for (const task of asyncTaskList) {
if (task === AsyncTask.SyncOrganization) {
invariant(sessionId, 'sessionId is required');
invariant(accountId, 'accountId is required');
taskPromiseList.push(syncOrganization(sessionId, accountId));
}

if (task === AsyncTask.MigrateProjects) {
invariant(personalOrganizationId, 'personalOrganizationId is required');
invariant(sessionId, 'sessionId is required');
taskPromiseList.push(migrateProjectsUnderOrganization(personalOrganizationId, sessionId));
}

if (task === AsyncTask.SyncProjects) {
invariant(organizationId, 'organizationId is required');
taskPromiseList.push(syncProjects(organizationId));
}
}

await Promise.all(taskPromiseList);

return {};
} catch (error) {
console.log('Failed to run async task', error);
return {
error: error.message,
};
}
};

async function migrateProjectsUnderOrganization(personalOrganizationId: string, sessionId: string) {
if (await shouldMigrateProjectUnderOrganization()) {
await migrateProjectsIntoOrganization({
personalOrganizationId,
});

const preferredProjectType = localStorage.getItem('prefers-project-type');
if (preferredProjectType === 'remote') {
const localProjects = await database.find<Project>('Project', {
parentId: personalOrganizationId,
remoteId: null,
});

// If any of those fail projects will still be under the organization as local projects
for (const project of localProjects) {
updateLocalProjectToRemote({
project,
organizationId: personalOrganizationId,
sessionId,
vcs: VCSInstance(),
});
}
}
}
};

export const indexLoader: LoaderFunction = async () => {
console.log('org index loader');
const { id: sessionId, accountId } = await userSession.getOrCreate();
if (sessionId) {
await syncOrganization(sessionId, accountId);

const organizations = JSON.parse(localStorage.getItem(`${accountId}:organizations`) || '[]') as Organization[];
invariant(organizations, 'Failed to fetch organizations.');

const personalOrganization = organizations.filter(isPersonalOrganization)
.find(organization =>
isOwnerOfOrganization({
organization,
accountId,
}));
invariant(personalOrganization, 'Failed to find personal organization your account appears to be in an invalid state. Please contact support if this is a recurring issue.');
if (await shouldMigrateProjectUnderOrganization()) {
await migrateProjectsIntoOrganization({
personalOrganization,
});
const personalOrganization = findPersonalOrganization(organizations, accountId);
invariant(personalOrganization, 'Failed to find personal organization your account appears to be in an invalid state. Please contact support if this is a recurring issue.');
const personalOrganizationId = personalOrganization.id;
await migrateProjectsUnderOrganization(personalOrganizationId, sessionId);

const preferredProjectType = localStorage.getItem('prefers-project-type');
if (preferredProjectType === 'remote') {
const localProjects = await database.find<Project>('Project', {
parentId: personalOrganization.id,
remoteId: null,
});

// If any of those fail projects will still be under the organization as local projects
for (const project of localProjects) {
updateLocalProjectToRemote({
project,
organizationId: personalOrganization.id,
sessionId,
vcs: VCSInstance(),
});
}
}
}

if (personalOrganization) {
return redirect(`/organization/${personalOrganization.id}`);
}
if (personalOrganization) {
return redirect(`/organization/${personalOrganizationId}`);
}

if (organizations.length > 0) {
return redirect(`/organization/${organizations[0].id}`);
}
if (organizations.length > 0) {
return redirect(`/organization/${organizations[0].id}`);
}
}

await session.logout();
Expand All @@ -233,6 +287,7 @@ export interface OrganizationLoaderData {
}

export const loader: LoaderFunction = async () => {
console.log('org loader');
const { id, accountId } = await userSession.getOrCreate();
if (id) {
const organizations = JSON.parse(localStorage.getItem(`${accountId}:organizations`) || '[]') as Organization[];
Expand Down Expand Up @@ -402,6 +457,12 @@ const OrganizationRoute = () => {
};
const [status, setStatus] = useState<'online' | 'offline'>('online');

useAsyncTask({
organizationId,
organizations,
userSession,
});

useEffect(() => {
const isIdleAndUninitialized = untrackedProjectsFetcher.state === 'idle' && !untrackedProjectsFetcher.data;
if (isIdleAndUninitialized) {
Expand Down
Loading
Loading