From 5b7f45e910de9e1a705922c8de32b81c27ecb5fd Mon Sep 17 00:00:00 2001 From: James Gatz Date: Wed, 5 Jul 2023 18:51:55 +0200 Subject: [PATCH] Universal Insomnia/Git Sync for all files (documents/collections) (#5945) * share design documents and tests with insomnia sync * share collection and documents with insomnia/git sync * checkpoint * redirect when cloning (pulling) a new remote project * backend workspace card * redirect logic * local changes styles * loading indicator * fix sync button styles * use interval to refetch git * open the repo modal first before switching to git sync * use gitRepositoryId from metadata * fix sync option update * cleanup fetcher check * fixes * cleanup pull-push * undo logo change * clean git-vcs * use cloud icon for insomnia sync * cleanup card * more cleanup * better return type for fetchfetcher * update git tests * fix tests * fix some prerelease tests * fix cloning without an api spec * fix some more tests that expect a file to exist * fix lint error --- .../cookie-editor-interactions.test.ts | 2 +- .../prerelease/dashboard-interactions.test.ts | 34 +- .../debug-sidebar-interactions.test.ts | 8 +- .../prerelease/design-interactions.test.ts | 4 +- .../environment-editor-interactions.test.ts | 4 +- .../prerelease/git-sync-interactions.test.ts | 29 +- .../prerelease/grpc-interactions.test.ts | 2 +- .../prerelease/plugins-interactions.test.ts | 4 +- .../tests/prerelease/request-pane-tab.test.ts | 8 + .../tests/smoke/git-sync.test.ts | 1 + .../tests/smoke/oauth-gitlab.test.ts | 1 + packages/insomnia/src/models/api-spec.ts | 2 +- .../initialize-backend-project.test.ts | 20 +- .../sync/vcs/initialize-backend-project.ts | 7 +- .../src/sync/vcs/migrate-collections.ts | 4 +- .../src/sync/vcs/pull-backend-project.ts | 29 +- .../dropdowns/git-sync-dropdown.tsx | 260 ++++++++++-- .../ui/components/dropdowns/sync-dropdown.tsx | 204 ++++++--- .../dropdowns/workspace-sync-dropdown.tsx | 41 ++ .../modals/workspace-settings-modal.tsx | 49 ++- .../src/ui/components/sidebar-layout.tsx | 4 + .../insomnia/src/ui/components/statusbar.tsx | 22 +- .../src/ui/components/workspace-header.tsx | 29 +- packages/insomnia/src/ui/index.tsx | 4 + packages/insomnia/src/ui/routes/actions.tsx | 6 +- packages/insomnia/src/ui/routes/debug.tsx | 4 + packages/insomnia/src/ui/routes/design.tsx | 19 +- .../insomnia/src/ui/routes/git-actions.tsx | 397 ++++++++++++------ .../src/ui/routes/remote-collections.tsx | 11 +- .../insomnia/src/ui/routes/test-results.tsx | 2 +- .../insomnia/src/ui/routes/test-suite.tsx | 2 +- packages/insomnia/src/ui/routes/unit-test.tsx | 6 +- 32 files changed, 864 insertions(+), 355 deletions(-) create mode 100644 packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx diff --git a/packages/insomnia-smoke-test/tests/prerelease/cookie-editor-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/cookie-editor-interactions.test.ts index fc8e76021..19cc2c474 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/cookie-editor-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/cookie-editor-interactions.test.ts @@ -10,7 +10,7 @@ test.describe('Cookie editor', async () => { await page.getByRole('menuitem', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('button', { name: 'Import' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('Collectionsimplejust now').click(); }); diff --git a/packages/insomnia-smoke-test/tests/prerelease/dashboard-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/dashboard-interactions.test.ts index 8b204e03b..3691598d2 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/dashboard-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/dashboard-interactions.test.ts @@ -7,7 +7,8 @@ test.describe('Dashboard', async () => { test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); test.describe('Projects', async () => { test('Can create, rename and delete new project', async ({ page }) => { - await expect(page.locator('.app')).toContainText('All Files (1)'); + await expect(page.locator('.app')).toContainText('All Files (0)'); + await expect(page.locator('.app')).not.toContainText('Git Sync'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); // Create new project @@ -41,14 +42,15 @@ test.describe('Dashboard', async () => { await expect(page.locator('.app')).toContainText('Insomnia'); await expect(page.locator('.app')).not.toContainText('My Project123'); await expect(page.locator('.app')).toContainText('New Document'); - await expect(page.locator('.app')).toContainText('All Files (1)'); + await expect(page.locator('.app')).toContainText('All Files (0)'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); }); }); test.describe('Interactions', async () => { // Not sure about the name here // TODO(INS-2504) - we don't support importing multiple collections at this time test.skip('Can filter through multiple collections', async ({ app, page }) => { - await expect(page.locator('.app')).toContainText('All Files (1)'); + await expect(page.locator('.app')).toContainText('All Files (0)'); + await expect(page.locator('.app')).not.toContainText('Git Sync'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); await page.getByRole('button', { name: 'Create' }).click(); @@ -57,7 +59,7 @@ test.describe('Dashboard', async () => { await page.getByRole('menuitem', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('button', { name: 'Import' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionSmoke testsjust now').click(); // Check that 10 new workspaces are imported besides the default one const workspaceCards = page.locator('.card-badge'); @@ -85,7 +87,8 @@ test.describe('Dashboard', async () => { }); test('Can create, rename and delete a document', async ({ page }) => { - await expect(page.locator('.app')).toContainText('All Files (1)'); + await expect(page.locator('.app')).toContainText('All Files (0)'); + await expect(page.locator('.app')).not.toContainText('Git Sync'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); // Create new document @@ -93,10 +96,10 @@ test.describe('Dashboard', async () => { await page.getByRole('menuitem', { name: 'Design Document' }).click(); await page.locator('text=Create').nth(1).click(); - // Return to dashboardawait expect(page.locator('.app')).toContainText('My Document'); + await page.getByTestId('proj_default-project').getByRole('link', { name: 'Insomnia' }).click(); // Rename document - await page.click('text=DocumentMy Documentjust now >> button'); + await page.click('text=DocumentNew Documentjust now >> button'); await page.getByRole('menuitem', { name: 'Rename' }).click(); await page.locator('text=Rename DocumentName Rename >> input[type="text"]').fill('test123'); await page.click('#root button:has-text("Rename")'); @@ -108,20 +111,21 @@ test.describe('Dashboard', async () => { await page.locator('input[name="name"]').fill('test123-duplicate'); await page.click('[role="dialog"] button:has-text("Duplicate")'); - // Return to dashboardawait expect(page.locator('.app')).toContainText('test123-duplicate'); + await page.getByTestId('proj_default-project').getByRole('link', { name: 'Insomnia' }).click(); const workspaceCards = page.locator('.card-badge'); - await expect(workspaceCards).toHaveCount(3); + await expect(workspaceCards).toHaveCount(2); // Delete document await page.click('text=Documenttest123just now >> button'); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.locator('text=Yes').click(); - await expect(workspaceCards).toHaveCount(2); + await expect(workspaceCards).toHaveCount(1); }); test('Can create, rename and delete a collection', async ({ page }) => { - await expect(page.locator('.app')).toContainText('All Files (1)'); + await expect(page.locator('.app')).toContainText('All Files (0)'); + await expect(page.locator('.app')).not.toContainText('Git Sync'); await expect(page.locator('.app')).not.toContainText('Setup Git Sync'); // Create new collection @@ -129,7 +133,7 @@ test.describe('Dashboard', async () => { await page.getByRole('menuitem', { name: 'Request Collection' }).click(); await page.locator('text=Create').nth(1).click(); - // Return to dashboardawait expect(page.locator('.app')).toContainText('My Collection'); + await page.getByTestId('proj_default-project').getByRole('link', { name: 'Insomnia' }).click(); // Rename collection await page.click('text=CollectionMy Collectionjust now >> button'); @@ -144,15 +148,15 @@ test.describe('Dashboard', async () => { await page.locator('input[name="name"]').fill('test123-duplicate'); await page.click('[role="dialog"] button:has-text("Duplicate")'); - // Return to dashboardawait expect(page.locator('.app')).toContainText('test123-duplicate'); + await page.getByTestId('proj_default-project').getByRole('link', { name: 'Insomnia' }).click(); const workspaceCards = page.locator('.card-badge'); - await expect(workspaceCards).toHaveCount(3); + await expect(workspaceCards).toHaveCount(2); // Delete collection await page.click('text=Collectiontest123just now >> button'); await page.getByRole('menuitem', { name: 'Delete' }).click(); await page.locator('text=Yes').click(); - await expect(workspaceCards).toHaveCount(2); + await expect(workspaceCards).toHaveCount(1); }); }); }); diff --git a/packages/insomnia-smoke-test/tests/prerelease/debug-sidebar-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/debug-sidebar-interactions.test.ts index 79d213667..05dd1f1b8 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/debug-sidebar-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/debug-sidebar-interactions.test.ts @@ -12,7 +12,7 @@ test.describe('Debug-Sidebar', async () => { await page.getByRole('menuitem', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('button', { name: 'Import' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('Collectionsimplejust now').click(); }); @@ -61,9 +61,9 @@ test.describe('Debug-Sidebar', async () => { }); test('Open properties of the collection', async ({ page }) => { - await page.getByRole('button', { name: 'Untitled' }).click(); - await page.getByRole('menuitem', { name: 'Document Settings' }).click(); - await page.getByText('Document Settings').click(); + await page.getByRole('button', { name: 'simple' }).click(); + await page.getByRole('menuitem', { name: 'Collection Settings' }).click(); + await page.getByText('Collection Settings').click(); }); test('Filter by request name', async ({ page }) => { diff --git a/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts index b6bfa780f..90fbefb63 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts @@ -14,8 +14,8 @@ test.describe('Design interactions', async () => { await page.getByRole('menuitem', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('button', { name: 'Import' }).click(); - await page.getByText('CollectionSmoke testsjust now').click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + await page.getByText('unit-test.yaml').click(); // Switch to Test tab await page.click('a:has-text("Test")'); diff --git a/packages/insomnia-smoke-test/tests/prerelease/environment-editor-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/environment-editor-interactions.test.ts index 10074eca4..1cb697490 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/environment-editor-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/environment-editor-interactions.test.ts @@ -10,8 +10,8 @@ test.describe('Environment Editor', async () => { await page.getByRole('menuitem', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('button', { name: 'Import' }).click(); - await page.getByText('CollectionSmoke testsjust now').click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + await page.getByText('Collectionenvironments').click(); }); test('create a new environment', async ({ page }) => { diff --git a/packages/insomnia-smoke-test/tests/prerelease/git-sync-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/git-sync-interactions.test.ts index 85d204725..4bdf63052 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/git-sync-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/git-sync-interactions.test.ts @@ -13,7 +13,7 @@ test('Clone Repo with bad values', async ({ page }) => { await page.getByText('Author Email').fill('test'); await page.getByText('Username').fill('test'); await page.getByText('Authentication Token').fill('test'); - await page.getByRole('button', { name: 'Clone' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Clone' }).click(); // Basic check repository data is loaded // Design doc @@ -50,31 +50,6 @@ test('Clone Repo with bad values', async ({ page }) => { await expect(page.locator('.app')).toContainText('Error Pushing Repository'); }); -test('Clone Bitbucket Repo with bad values', async ({ page }) => { - await page.getByRole('button', { name: 'Create' }).click(); - await page.getByRole('menuitem', { name: 'Git Clone' }).click(); - await page.getByRole('tab', { name: 'Git' }).nth(2).click(); - - // Fill in Git Sync details and clone repository - await page.getByText('Git URI (https)').fill('https://bitbucket.org/atlassian/bitbucket-example-plugin.git'); - await page.getByText('Author Name').fill('test'); - await page.getByText('Author Email').fill('test'); - await page.getByText('Username').fill('test'); - await page.getByText('Authentication Token').fill('test'); - await page.getByRole('button', { name: 'Clone' }).click(); - - // Create a branch and try to push with bad Git token - await page.getByRole('button', { name: 'master' }).click(); - await page.getByRole('menuitem', { name: 'Branches' }).click(); - await page.getByPlaceholder('testing-branch').fill('test123'); - await page.getByRole('button', { name: '+ Create' }).click(); - await page.getByRole('cell', { name: 'test123(current)' }).click(); - await page.getByRole('button', { name: 'Done' }).click(); - await page.getByRole('button', { name: 'test123' }).click(); - await page.getByRole('menuitem', { name: 'Push' }).click(); - await expect(page.locator('.app')).toContainText('Error Pushing Repository'); -}); - test('Clone Gitlab Repo with bad values', async ({ page }) => { await page.getByRole('button', { name: 'Create' }).click(); await page.getByRole('menuitem', { name: 'Git Clone' }).click(); @@ -86,7 +61,7 @@ test('Clone Gitlab Repo with bad values', async ({ page }) => { await page.getByText('Author Email').fill('test'); await page.getByText('Username').fill('test'); await page.getByText('Authentication Token').fill('test'); - await page.getByRole('button', { name: 'Clone' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Clone' }).click(); // Create a branch and try to push with bad Git token await page.getByRole('button', { name: 'master' }).click(); diff --git a/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts index fd67e968a..1018be758 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts @@ -19,7 +19,7 @@ test.describe('gRPC interactions', () => { await page.getByRole('menuitem', { name: 'Import' }).click(); await page.getByText('Clipboard').click(); await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('button', { name: 'Import' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionPreRelease gRPCjust now').click(); statusTag = page.locator('[data-testid="response-status-tag"]:visible'); responseBody = page.locator('[data-testid="response-pane"] >> [data-testid="CodeEditor"]:visible', { diff --git a/packages/insomnia-smoke-test/tests/prerelease/plugins-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/plugins-interactions.test.ts index 04bb95841..e8e6ca38b 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/plugins-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/plugins-interactions.test.ts @@ -18,8 +18,8 @@ test.describe('Plugins', async () => { }); test('Check Declarative Config and Kong Kubernetes config', async ({ page }) => { - // Switch to design tab - await page.click('text=Design'); + await page.getByRole('button', { name: 'New Document' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); // Set example OpenAPI spec await page.click('text=start from an example'); diff --git a/packages/insomnia-smoke-test/tests/prerelease/request-pane-tab.test.ts b/packages/insomnia-smoke-test/tests/prerelease/request-pane-tab.test.ts index 6ed7c01d5..b546de2c8 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/request-pane-tab.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/request-pane-tab.test.ts @@ -1,5 +1,13 @@ import { test } from '../../playwright/test'; +test.beforeEach(async ({ page }) => { + await page.getByRole('button', { name: 'New Collection' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); + + await page.getByRole('button', { name: ' ' }).press('ArrowDown'); + await page.getByRole('menuitem', { name: 'HTTP Request' }).press('Enter'); +}); + test('Select body dropdown', async ({ page }) => { await page.getByRole('button', { name: 'Body' }).click(); await page.getByRole('menuitem', { name: 'JSON' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts index a819e514f..0bf33ea4e 100644 --- a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts @@ -3,6 +3,7 @@ import { test } from '../../playwright/test'; test('Sign in with GitHub', async ({ app, page }) => { await page.getByRole('button', { name: 'New Document' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Git Sync' }).click(); await page.getByRole('button', { name: 'Setup Git Sync' }).click(); await page.getByRole('tab', { name: 'Github' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts b/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts index 0065c046e..6b5addd64 100644 --- a/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts @@ -3,6 +3,7 @@ import { test } from '../../playwright/test'; test('Sign in with Gitlab', async ({ app, page }) => { await page.getByRole('button', { name: 'New Document' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); + await page.getByRole('button', { name: 'Git Sync' }).click(); await page.getByRole('button', { name: 'Setup Git Sync' }).click(); await page.getByRole('tab', { name: 'GitLab' }).click(); diff --git a/packages/insomnia/src/models/api-spec.ts b/packages/insomnia/src/models/api-spec.ts index 781340978..4fddf0d43 100644 --- a/packages/insomnia/src/models/api-spec.ts +++ b/packages/insomnia/src/models/api-spec.ts @@ -10,7 +10,7 @@ export const prefix = 'spc'; export const canDuplicate = true; -export const canSync = false; +export const canSync = true; export interface BaseApiSpec { fileName: string; diff --git a/packages/insomnia/src/sync/vcs/__tests__/initialize-backend-project.test.ts b/packages/insomnia/src/sync/vcs/__tests__/initialize-backend-project.test.ts index 68e70e8d6..13cd8fa4b 100644 --- a/packages/insomnia/src/sync/vcs/__tests__/initialize-backend-project.test.ts +++ b/packages/insomnia/src/sync/vcs/__tests__/initialize-backend-project.test.ts @@ -3,30 +3,12 @@ import { afterAll, beforeEach, describe, expect, it, jest } from '@jest/globals' import { globalBeforeEach } from '../../../__jest__/before-each'; import * as models from '../../../models'; import MemoryDriver from '../../store/drivers/memory-driver'; -import { initializeLocalBackendProjectAndMarkForSync, pushSnapshotOnInitialize } from '../initialize-backend-project'; +import { pushSnapshotOnInitialize } from '../initialize-backend-project'; import { VCS } from '../vcs'; describe('initialize-backend-project', () => { beforeEach(globalBeforeEach); - describe('initializeLocalBackendProjectAndMarkForSync()', () => { - it('should do nothing if not request collection', async () => { - // Arrange - const workspace = await models.workspace.create({ scope: 'design' }); - const vcs = new VCS(new MemoryDriver()); - const switchAndCreateBackendProjectIfNotExistSpy = jest.spyOn(vcs, 'switchAndCreateBackendProjectIfNotExist'); - - // Act - await initializeLocalBackendProjectAndMarkForSync({ workspace, vcs }); - - // Assert - expect(switchAndCreateBackendProjectIfNotExistSpy).not.toHaveBeenCalled(); - // const workspaceMeta = await models.workspaceMeta.getByParentId(workspace._id); - // expect(workspaceMeta?.pushSnapshotOnInitialize).toBe(false); - switchAndCreateBackendProjectIfNotExistSpy.mockClear(); - }); - }); - describe('pushSnapshotOnInitialize()', () => { const vcs = new VCS(new MemoryDriver()); diff --git a/packages/insomnia/src/sync/vcs/initialize-backend-project.ts b/packages/insomnia/src/sync/vcs/initialize-backend-project.ts index 270e30003..603459bfa 100644 --- a/packages/insomnia/src/sync/vcs/initialize-backend-project.ts +++ b/packages/insomnia/src/sync/vcs/initialize-backend-project.ts @@ -2,18 +2,13 @@ import { database } from '../../common/database'; import * as models from '../../models'; import { getStatusCandidates } from '../../models/helpers/get-status-candidates'; import { Project } from '../../models/project'; -import { isCollection, Workspace } from '../../models/workspace'; +import { Workspace } from '../../models/workspace'; import { WorkspaceMeta } from '../../models/workspace-meta'; import { VCS } from './vcs'; const blankStage = {}; export const initializeLocalBackendProjectAndMarkForSync = async ({ vcs, workspace }: { vcs: VCS; workspace: Workspace }) => { - if (!isCollection(workspace)) { - // Don't initialize and mark for sync unless we're in a collection - return; - } - // Create local project await vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name); diff --git a/packages/insomnia/src/sync/vcs/migrate-collections.ts b/packages/insomnia/src/sync/vcs/migrate-collections.ts index 7439fe598..26f48ee38 100644 --- a/packages/insomnia/src/sync/vcs/migrate-collections.ts +++ b/packages/insomnia/src/sync/vcs/migrate-collections.ts @@ -1,6 +1,6 @@ import { isLoggedIn } from '../../account/session'; -import { asyncFilter } from '../../common/async-array-helpers'; import { database } from '../../common/database'; +import { isNotNullOrUndefined } from '../../common/misc'; import * as models from '../../models'; import { isRemoteProject, RemoteProject } from '../../models/project'; import { isCollection, Workspace } from '../../models/workspace'; @@ -36,7 +36,7 @@ export const migrateCollectionsIntoRemoteProject = async (vcs: VCS) => { const isNotInRemoteProject = (collection: Workspace) => !Boolean(remoteProjects.find(project => project._id === collection.parentId)); const hasLocalProject = (collection: Workspace) => vcs.hasBackendProjectForRootDocument(collection._id); - const needsMigration = await asyncFilter(collections, async coll => await hasLocalProject(coll) && isNotInRemoteProject(coll)); + const needsMigration = (await Promise.all(collections.map(async coll => await hasLocalProject(coll) && isNotInRemoteProject(coll) ? coll : null))).filter(isNotNullOrUndefined); // If nothing to migrate, exit if (!needsMigration.length) { diff --git a/packages/insomnia/src/sync/vcs/pull-backend-project.ts b/packages/insomnia/src/sync/vcs/pull-backend-project.ts index 2979588e8..575a96acf 100644 --- a/packages/insomnia/src/sync/vcs/pull-backend-project.ts +++ b/packages/insomnia/src/sync/vcs/pull-backend-project.ts @@ -2,7 +2,10 @@ import { DEFAULT_BRANCH_NAME } from '../../common/constants'; import { database } from '../../common/database'; import { RemoteProject } from '../../models/project'; import { isWorkspace } from '../../models/workspace'; -import { initializeProjectFromTeam, initializeWorkspaceFromBackendProject } from './initialize-model-from'; +import { + initializeProjectFromTeam, + initializeWorkspaceFromBackendProject, +} from './initialize-model-from'; import { BackendProjectWithTeam } from './normalize-backend-project-team'; import { interceptAccessError } from './util'; import { VCS } from './vcs'; @@ -13,7 +16,11 @@ interface Options { remoteProjects: RemoteProject[]; } -export const pullBackendProject = async ({ vcs, backendProject, remoteProjects }: Options) => { +export const pullBackendProject = async ({ + vcs, + backendProject, + remoteProjects, +}: Options) => { // Set backend project, checkout master, and pull await vcs.setBackendProject(backendProject); await vcs.checkout([], DEFAULT_BRANCH_NAME); @@ -26,25 +33,35 @@ export const pullBackendProject = async ({ vcs, backendProject, remoteProjects } const defaultBranchMissing = !remoteBranches.includes(DEFAULT_BRANCH_NAME); // Find or create the remote project locally - let project = remoteProjects.find(({ remoteId }) => remoteId === backendProject.team.id); + let project = remoteProjects.find( + ({ remoteId }) => remoteId === backendProject.team.id + ); if (!project) { project = await initializeProjectFromTeam(backendProject.team); await database.upsert(project); } + let workspaceId; + // The default branch does not exist, so we create it and the workspace locally if (defaultBranchMissing) { - const workspace = await initializeWorkspaceFromBackendProject(backendProject, project); + const workspace = await initializeWorkspaceFromBackendProject( + backendProject, + project + ); await database.upsert(workspace); + + workspaceId = workspace._id; } else { await vcs.pull([], project.remoteId); // There won't be any existing docs since it's a new pull const flushId = await database.bufferChanges(); // @ts-expect-error -- TSCONVERSION - for (const doc of (await vcs.allDocuments() || [])) { + for (const doc of (await vcs.allDocuments()) || []) { if (isWorkspace(doc)) { doc.parentId = project._id; + workspaceId = doc._id; } await database.upsert(doc); } @@ -52,5 +69,5 @@ export const pullBackendProject = async ({ vcs, backendProject, remoteProjects } await database.flushChanges(flushId); } - return project; + return { project, workspaceId }; }; diff --git a/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx index 6ae89a47a..775c87393 100644 --- a/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx @@ -1,13 +1,16 @@ import classnames from 'classnames'; import React, { FC, Fragment, useEffect, useRef, useState } from 'react'; -import { useFetcher, useParams } from 'react-router-dom'; +import { useFetcher, useParams, useRevalidator } from 'react-router-dom'; +import { useInterval } from 'react-use'; import { docsGitSync } from '../../../common/documentation'; import { GitRepository } from '../../../models/git-repository'; +import { deleteGitRepository } from '../../../models/helpers/git-repository-operations'; import { getOauth2FormatName } from '../../../sync/git/utils'; import { GitFetchLoaderData, GitRepoLoaderData, + GitStatusResult, PullFromGitRemoteResult, PushToGitRemoteResult, } from '../../routes/git-actions'; @@ -27,13 +30,15 @@ import { GitLogModal } from '../modals/git-log-modal'; import { GitRepositorySettingsModal } from '../modals/git-repository-settings-modal'; import { GitStagingModal } from '../modals/git-staging-modal'; import { Button } from '../themed-button'; +import { Tooltip } from '../tooltip'; interface Props { gitRepository: GitRepository | null; className?: string; + isInsomniaSyncEnabled: boolean; } -export const GitSyncDropdown: FC = ({ className, gitRepository }) => { +export const GitSyncDropdown: FC = ({ className, gitRepository, isInsomniaSyncEnabled }) => { const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; @@ -52,10 +57,12 @@ export const GitSyncDropdown: FC = ({ className, gitRepository }) => { const gitCheckoutFetcher = useFetcher(); const gitRepoDataFetcher = useFetcher(); const gitFetchFetcher = useFetcher(); + const gitStatusFetcher = useFetcher(); const loadingPush = gitPushFetcher.state === 'loading'; const loadingPull = gitPullFetcher.state === 'loading'; const loadingFetch = gitFetchFetcher.state === 'loading'; + const loadingStatus = gitStatusFetcher.state === 'loading'; useEffect(() => { if ( @@ -77,6 +84,17 @@ export const GitSyncDropdown: FC = ({ className, gitRepository }) => { workspaceId, ]); + // Only fetch the repo status if we have a repo uri and we don't have the status already + const shouldFetchGitRepoStatus = Boolean(gitRepository?.uri && gitRepository?._id && gitStatusFetcher.state === 'idle' && !gitStatusFetcher.data && gitRepoDataFetcher.data); + + useEffect(() => { + if (shouldFetchGitRepoStatus) { + gitStatusFetcher.load( + `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/status` + ); + } + }, [gitStatusFetcher, organizationId, projectId, shouldFetchGitRepoStatus, workspaceId]); + useEffect(() => { const errors = [...(gitPushFetcher.data?.errors ?? [])]; if (errors.length > 0) { @@ -133,7 +151,7 @@ export const GitSyncDropdown: FC = ({ className, gitRepository }) => { ); } - let iconClassName = ''; + let iconClassName = 'fa-brands fa-git-alt'; const providerName = getOauth2FormatName(gitRepository?.credentials); if (providerName === 'github') { iconClassName = 'fa fa-github'; @@ -148,10 +166,8 @@ export const GitSyncDropdown: FC = ({ className, gitRepository }) => { gitCheckoutFetcher.state === 'loading' || gitPushFetcher.state === 'loading' || gitPullFetcher.state === 'loading'; - const isButton = - !gitRepository || - (isLoading && !gitRepoDataFetcher.data) || - (gitRepoDataFetcher.data && 'errors' in gitRepoDataFetcher.data); + + const isSynced = Boolean(gitRepository?.uri && !isLoading && gitRepoDataFetcher.data && !('errors' in gitRepoDataFetcher.data)); const { branches, branch: currentBranch } = gitRepoDataFetcher.data && 'branches' in gitRepoDataFetcher.data @@ -159,6 +175,7 @@ export const GitSyncDropdown: FC = ({ className, gitRepository }) => { : { branches: [], branch: '' }; let dropdown: React.ReactNode = null; + const { revalidate } = useRevalidator(); const currentBranchActions = [ { @@ -212,51 +229,108 @@ export const GitSyncDropdown: FC = ({ className, gitRepository }) => { }, ]; - if (isButton) { - dropdown = ( - + useInterval(() => { + gitFetchFetcher.submit( + {}, + { + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/fetch`, + method: 'post', + } ); - } else { + }, 1000 * 60 * 5); + + const status = gitStatusFetcher.data?.status; + + const commitToolTipMsg = status?.localChanges ? 'Local changes made' : 'No local changes made'; + + if (isSynced) { dropdown = (
{ - gitFetchFetcher.submit( - {}, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/fetch`, - method: 'post', - } - ); - }} triggerButton={ - - {iconClassName && ( - - )} -
{currentBranch}
- + +
+ {iconClassName && ( + + )} +
{currentBranch}
+ +
+ + + +
+
+
} > + + {item => ( + + + + )} + @@ -345,6 +419,108 @@ export const GitSyncDropdown: FC = ({ className, gitRepository }) => {
); + } else { + dropdown = ( +
+ +
+ {iconClassName && ( + + )} + Git Sync +
+ + + } + > + + {item => ( + + + + )} + + + Git Sync + + Sync and collaborate with Git{' '} + + +
+ Documentation +
+ +
+ + } + > + + { + setIsGitRepoSettingsModalOpen(true); + }} + /> + +
+
+
+ ); } return ( diff --git a/packages/insomnia/src/ui/components/dropdowns/sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/sync-dropdown.tsx index 90eb4c483..b9c153f9d 100644 --- a/packages/insomnia/src/ui/components/dropdowns/sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/sync-dropdown.tsx @@ -24,11 +24,13 @@ import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } import { Link } from '../base/link'; import { HelpTooltip } from '../help-tooltip'; import { showError, showModal } from '../modals'; +import { GitRepositorySettingsModal } from '../modals/git-repository-settings-modal'; import { LoginModal } from '../modals/login-modal'; import { SyncBranchesModal } from '../modals/sync-branches-modal'; import { SyncDeleteModal } from '../modals/sync-delete-modal'; import { SyncHistoryModal } from '../modals/sync-history-modal'; import { SyncStagingModal } from '../modals/sync-staging-modal'; +import { Button } from '../themed-button'; import { Tooltip } from '../tooltip'; // TODO: handle refetching logic in one place not here in a component @@ -81,7 +83,6 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { const remoteProjects = useSelector(selectRemoteProjects); const syncItems = useSelector(selectSyncItems); const workspaceMeta = useSelector(selectActiveWorkspaceMeta); - const refetchRemoteBranch = useCallback(async () => { if (session.isLoggedIn()) { try { @@ -125,6 +126,8 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { refetchRemoteBranch(); }, REFRESH_PERIOD); + const [isGitRepoSettingsModalOpen, setIsGitRepoSettingsModalOpen] = useState(false); + useMount(async () => { setState(state => ({ ...state, @@ -162,10 +165,10 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { loadingProjectPull: true, })); const pulledIntoProject = await pullBackendProject({ vcs, backendProject, remoteProjects }); - if (pulledIntoProject._id !== project._id) { + if (pulledIntoProject.project._id !== project._id) { // If pulled into a different project, reactivate the workspace - await dispatch(activateWorkspace({ workspaceId: workspace._id })); - logCollectionMovedToProject(workspace, pulledIntoProject); + dispatch(activateWorkspace({ workspaceId: workspace._id })); + logCollectionMovedToProject(workspace, pulledIntoProject.project); } await refreshVCSAndRefetchRemote(); setState(state => ({ @@ -338,23 +341,61 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { return (
refreshVCSAndRefetchRemote()} aria-label="Select a project to sync with" triggerButton={ - Setup Sync +
+ Setup Sync +
} > + + + + + = ({ vcs, workspace, project }) => { {p => - + } @@ -387,6 +429,11 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { }
+ {isGitRepoSettingsModalOpen && ( + setIsGitRepoSettingsModalOpen(false)} + /> + )}
); } @@ -404,9 +451,8 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { return (
refreshVCSAndRefetchRemote()} closeOnSelect={false} isDisabled={initializing} @@ -414,56 +460,101 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { currentBranch === null ? Sync : -
- {' '} - {initializing ? 'Initializing...' : currentBranch} -
-
- - - +
+
+ {' '} + {initializing ? 'Initializing...' : currentBranch} +
+
+ + + - {/* Only show cloud icons if logged in */} - {session.isLoggedIn() && ( - - {loadingPull ? ( - loadIcon - ) : ( - - - - )} + {/* Only show cloud icons if logged in */} + {session.isLoggedIn() && ( + + {loadingPull ? ( + loadIcon + ) : ( + + + + )} - {loadingPush ? ( - loadIcon - ) : ( - - - - )} - - )} + {loadingPush ? ( + loadIcon + ) : ( + + + + )} + + )} +
+ } > + + + + + = ({ vcs, workspace, project }) => { + {isGitRepoSettingsModalOpen && ( + setIsGitRepoSettingsModalOpen(false)} + /> + )}
); }; diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx new file mode 100644 index 000000000..da98fc29b --- /dev/null +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx @@ -0,0 +1,41 @@ +import { FC } from 'react'; +import React from 'react'; +import { useRouteLoaderData } from 'react-router-dom'; + +import { isRemoteProject } from '../../../models/project'; +import { useVCS } from '../../hooks/use-vcs'; +import { WorkspaceLoaderData } from '../../routes/workspace'; +import { GitSyncDropdown } from './git-sync-dropdown'; +import { SyncDropdown } from './sync-dropdown'; + +export const WorkspaceSyncDropdown: FC = () => { + const { + activeProject, + activeWorkspace, + gitRepository, + activeWorkspaceMeta, + } = useRouteLoaderData( + ':workspaceId' + ) as WorkspaceLoaderData; + + const vcs = useVCS({ + workspaceId: activeWorkspace?._id, + }); + + if (isRemoteProject(activeProject) && vcs && !activeWorkspaceMeta?.gitRepositoryId) { + return ( + + ); + } + + if (activeWorkspaceMeta?.gitRepositoryId || !isRemoteProject(activeProject)) { + return ; + } + + return null; +}; diff --git a/packages/insomnia/src/ui/components/modals/workspace-settings-modal.tsx b/packages/insomnia/src/ui/components/modals/workspace-settings-modal.tsx index 6c04bc95c..a22d15e3d 100644 --- a/packages/insomnia/src/ui/components/modals/workspace-settings-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/workspace-settings-modal.tsx @@ -1,5 +1,6 @@ import React, { FC, forwardRef, ReactNode, useEffect, useImperativeHandle, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useRevalidator } from 'react-router-dom'; import styled from 'styled-components'; import { ACTIVITY_HOME } from '../../../common/constants'; @@ -12,7 +13,7 @@ import * as models from '../../../models/index'; import { isRequest } from '../../../models/request'; import { invariant } from '../../../utils/invariant'; import { setActiveActivity } from '../../redux/modules/global'; -import { selectActiveApiSpec, selectActiveWorkspace, selectActiveWorkspaceClientCertificates, selectActiveWorkspaceName } from '../../redux/selectors'; +import { selectActiveApiSpec, selectActiveWorkspace, selectActiveWorkspaceClientCertificates, selectActiveWorkspaceMeta, selectActiveWorkspaceName } from '../../redux/selectors'; import { FileInputButton } from '../base/file-input-button'; import { type ModalHandle, Modal, ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; @@ -57,6 +58,7 @@ const CertificateField: FC<{ ); }; + export interface WorkspaceSettingsModalOptions { showAddCertificateForm: boolean; host: string; @@ -86,10 +88,13 @@ export const WorkspaceSettingsModal = forwardRef(null); useEffect(() => { if (!workspace) { @@ -223,6 +228,7 @@ export const WorkspaceSettingsModal = forwardRef {workspace ? @@ -491,6 +497,47 @@ export const WorkspaceSettingsModal = forwardRef + + +
+ +

+ By enabling Git Sync, you can sync your workspace with a Git repository. This will disable the ability to sync with Insomnia Sync. +

+
+
+
: null} diff --git a/packages/insomnia/src/ui/components/sidebar-layout.tsx b/packages/insomnia/src/ui/components/sidebar-layout.tsx index 491dd9318..d45c23299 100644 --- a/packages/insomnia/src/ui/components/sidebar-layout.tsx +++ b/packages/insomnia/src/ui/components/sidebar-layout.tsx @@ -187,6 +187,10 @@ const Pane = forwardRef( } ); +export const SidebarFooter = styled.div({ + gridRowStart: 6, +}); + interface Props { renderPageSidebar?: ReactNode; renderPaneOne?: ReactNode; diff --git a/packages/insomnia/src/ui/components/statusbar.tsx b/packages/insomnia/src/ui/components/statusbar.tsx index f1c63cef4..50299ef8a 100644 --- a/packages/insomnia/src/ui/components/statusbar.tsx +++ b/packages/insomnia/src/ui/components/statusbar.tsx @@ -27,20 +27,12 @@ const KongLink = styled.a({ }); export const StatusBar: FC = () => { - return -
+ return ( + -
- - Made with   by Kong - -
; + + Made with   by Kong + + + ); }; diff --git a/packages/insomnia/src/ui/components/workspace-header.tsx b/packages/insomnia/src/ui/components/workspace-header.tsx index 90f6cb461..fd39e26ed 100644 --- a/packages/insomnia/src/ui/components/workspace-header.tsx +++ b/packages/insomnia/src/ui/components/workspace-header.tsx @@ -1,36 +1,27 @@ import React, { FC, Fragment } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { GitRepository } from '../../models/git-repository'; -import { isRemoteProject, Project } from '../../models/project'; -import { isCollection, isDesign, Workspace } from '../../models/workspace'; -import { useVCS } from '../hooks/use-vcs'; +import { Project } from '../../models/project'; +import { isDesign, Workspace } from '../../models/workspace'; import { ActivityToggle } from './activity-toggle'; import { Breadcrumb } from './breadcrumb'; -import { GitSyncDropdown } from './dropdowns/git-sync-dropdown'; -import { SyncDropdown } from './dropdowns/sync-dropdown'; import { WorkspaceDropdown } from './dropdowns/workspace-dropdown'; export const WorkspaceHeader: FC<{ - gitRepository: GitRepository | null; activeProject: Project; activeWorkspace: Workspace; -}> = ({ - gitRepository, - activeProject, - activeWorkspace, -}) => { - const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); - - const vcs = useVCS({ - workspaceId, - }); +}> = ({ activeProject, activeWorkspace }) => { + const { organizationId, projectId } = useParams<{ + organizationId: string; + projectId: string; + }>(); const navigate = useNavigate(); const crumbs = [ { - onClick: () => navigate(`/organization/${organizationId}/project/${projectId}`), + onClick: () => + navigate(`/organization/${organizationId}/project/${projectId}`), id: activeProject._id, label: activeProject.name, node: {activeProject.name}, @@ -46,8 +37,6 @@ export const WorkspaceHeader: FC<{ {isDesign(activeWorkspace) && } - {isDesign(activeWorkspace) && } - {isCollection(activeWorkspace) && isRemoteProject(activeProject) && vcs && } ); }; diff --git a/packages/insomnia/src/ui/index.tsx b/packages/insomnia/src/ui/index.tsx index 5e9af5c69..b9fd00a90 100644 --- a/packages/insomnia/src/ui/index.tsx +++ b/packages/insomnia/src/ui/index.tsx @@ -286,6 +286,10 @@ const router = createMemoryRouter( { path: 'git', children: [ + { + path: 'status', + loader: async (...args) => (await import('./routes/git-actions')).gitStatusLoader(...args), + }, { path: 'changes', loader: async (...args) => (await import('./routes/git-actions')).gitChangesLoader(...args), diff --git a/packages/insomnia/src/ui/routes/actions.tsx b/packages/insomnia/src/ui/routes/actions.tsx index a7f427b17..d905994e4 100644 --- a/packages/insomnia/src/ui/routes/actions.tsx +++ b/packages/insomnia/src/ui/routes/actions.tsx @@ -112,13 +112,13 @@ export const createNewWorkspaceAction: ActionFunction = async ({ // Create default env, cookie jar, and meta await models.environment.getOrCreateForParentId(workspace._id); await models.cookieJar.getOrCreateForParentId(workspace._id); - await models.workspaceMeta.getOrCreateByParentId(workspace._id); + const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); await database.flushChanges(flushId); - if (session.isLoggedIn() && isRemoteProject(project) && isCollection(workspace)) { + if (session.isLoggedIn() && isRemoteProject(project) && !workspaceMeta.gitRepositoryId) { const vcs = getVCS(); if (vcs) { - initializeLocalBackendProjectAndMarkForSync({ + await initializeLocalBackendProjectAndMarkForSync({ vcs, workspace, }); diff --git a/packages/insomnia/src/ui/routes/debug.tsx b/packages/insomnia/src/ui/routes/debug.tsx index 79eab0d2d..98a811fc2 100644 --- a/packages/insomnia/src/ui/routes/debug.tsx +++ b/packages/insomnia/src/ui/routes/debug.tsx @@ -15,6 +15,7 @@ import { isWebSocketRequest } from '../../models/websocket-request'; import { invariant } from '../../utils/invariant'; import { SegmentEvent, trackSegmentEvent } from '../analytics'; import { EnvironmentsDropdown } from '../components/dropdowns/environments-dropdown'; +import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown'; import { ErrorBoundary } from '../components/error-boundary'; import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; import { showModal } from '../components/modals'; @@ -61,6 +62,7 @@ export interface GrpcRequestState { methods: GrpcMethodInfo[]; reloadMethods: boolean; } + const INITIAL_GRPC_REQUEST_STATE = { running: false, requestMessages: [], @@ -70,6 +72,7 @@ const INITIAL_GRPC_REQUEST_STATE = { methods: [], reloadMethods: true, }; + export const Debug: FC = () => { const activeEnvironment = useSelector(selectActiveEnvironment); const activeRequest = useSelector(selectActiveRequest); @@ -264,6 +267,7 @@ export const Debug: FC = () => { + : null} renderPaneOne={activeWorkspace ? diff --git a/packages/insomnia/src/ui/routes/design.tsx b/packages/insomnia/src/ui/routes/design.tsx index e4922c299..b45c31288 100644 --- a/packages/insomnia/src/ui/routes/design.tsx +++ b/packages/insomnia/src/ui/routes/design.tsx @@ -24,6 +24,7 @@ import { CodeEditorHandle, } from '../components/codemirror/code-editor'; import { DesignEmptyState } from '../components/design-empty-state'; +import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown'; import { ErrorBoundary } from '../components/error-boundary'; import { Notice, NoticeTable } from '../components/notice-table'; import { SidebarLayout } from '../components/sidebar-layout'; @@ -256,9 +257,25 @@ const Design: FC = () => { apiSpec={apiSpec} handleSetSelection={handleScrollToSelection} /> +
+ +
) : ( - A spec navigator will render here + + A spec navigator will render here +
+ +
+
) } renderPaneTwo={showRightPane && } diff --git a/packages/insomnia/src/ui/routes/git-actions.tsx b/packages/insomnia/src/ui/routes/git-actions.tsx index b3af230d4..8a2ef6c4a 100644 --- a/packages/insomnia/src/ui/routes/git-actions.tsx +++ b/packages/insomnia/src/ui/routes/git-actions.tsx @@ -1,4 +1,3 @@ -import { invariant } from '@remix-run/router'; import { fromUrl } from 'hosted-git-info'; import { Errors } from 'isomorphic-git'; import path from 'path'; @@ -34,18 +33,27 @@ import { addDotGit, getOauth2FormatName, } from '../../sync/git/utils'; -import { SegmentEvent, trackSegmentEvent, vcsSegmentEventProperties } from '../analytics'; +import { invariant } from '../../utils/invariant'; +import { + SegmentEvent, + trackSegmentEvent, + vcsSegmentEventProperties, +} from '../analytics'; // Loaders -export type GitRepoLoaderData = { - branch: string; - branches: string[]; - gitRepository: GitRepository | null; -} | { - errors: string[]; -}; +export type GitRepoLoaderData = + | { + branch: string; + branches: string[]; + gitRepository: GitRepository | null; + } + | { + errors: string[]; + }; -export const gitRepoLoader: LoaderFunction = async ({ params }): Promise => { +export const gitRepoLoader: LoaderFunction = async ({ + params, +}): Promise => { try { const { workspaceId, projectId } = params; invariant(typeof workspaceId === 'string', 'Workspace Id is required'); @@ -54,89 +62,103 @@ export const gitRepoLoader: LoaderFunction = async ({ params }): Promise => { +export const gitBranchesLoader: LoaderFunction = async ({ + params, +}): Promise => { const { workspaceId, projectId } = params; invariant(typeof workspaceId === 'string', 'Workspace Id is required'); invariant(typeof projectId === 'string', 'Project Id is required'); @@ -144,13 +166,16 @@ export const gitBranchesLoader: LoaderFunction = async ({ params }): Promise => { +export const gitFetchAction: ActionFunction = async ({ + params, +}): Promise => { const { workspaceId, projectId } = params; invariant(typeof workspaceId === 'string', 'Workspace Id is required'); invariant(typeof projectId === 'string', 'Project Id is required'); @@ -175,17 +202,24 @@ export const gitFetchAction: ActionFunction = async ({ params }): Promise => { +export const gitLogLoader: LoaderFunction = async ({ + params, +}): Promise => { const { workspaceId, projectId } = params; invariant(typeof workspaceId === 'string', 'Workspace Id is required'); invariant(typeof projectId === 'string', 'Project Id is required'); @@ -210,13 +250,16 @@ export const gitLogLoader: LoaderFunction = async ({ params }): Promise; } -export const gitChangesLoader: LoaderFunction = async ({ params }): Promise => { +export const gitChangesLoader: LoaderFunction = async ({ + params, +}): Promise => { const { workspaceId } = params; invariant(typeof workspaceId === 'string', 'Workspace Id is required'); @@ -261,9 +306,11 @@ export const gitChangesLoader: LoaderFunction = async ({ params }): Promise => { +}): Promise => { const { organizationId, projectId } = params; invariant(typeof projectId === 'string', 'ProjectId is required.'); @@ -317,7 +364,10 @@ export const cloneGitRepoAction: ActionFunction = async ({ // Git Credentials const oauth2format = formData.get('oauth2format'); if (oauth2format) { - invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required'); + invariant( + oauth2format === 'gitlab' || oauth2format === 'github', + 'OAuth2 format is required' + ); const token = formData.get('token'); invariant(typeof token === 'string', 'Token is required'); const username = formData.get('username'); @@ -382,7 +432,9 @@ export const cloneGitRepoAction: ActionFunction = async ({ providerName, }); return { - errors: ['Error Cloning Repository: failed to clone with and without `.git` suffix'], + errors: [ + 'Error Cloning Repository: failed to clone with and without `.git` suffix', + ], }; } } @@ -419,6 +471,7 @@ export const cloneGitRepoAction: ActionFunction = async ({ parentId: project._id, description: `Insomnia Workspace for ${repoSettingsPatch.uri}}`, }); + await models.apiSpec.getOrCreateForParentId(workspace._id); trackSegmentEvent(SegmentEvent.vcsSyncComplete, { ...vcsSegmentEventProperties('git', 'clone', 'no directory found'), providerName, @@ -446,7 +499,11 @@ export const cloneGitRepoAction: ActionFunction = async ({ if (workspaces.length > 1) { trackSegmentEvent(SegmentEvent.vcsSyncComplete, { - ...vcsSegmentEventProperties('git', 'clone', 'multiple workspaces found'), + ...vcsSegmentEventProperties( + 'git', + 'clone', + 'multiple workspaces found' + ), providerName, }); @@ -464,11 +521,17 @@ export const cloneGitRepoAction: ActionFunction = async ({ if (existingWorkspace) { trackSegmentEvent(SegmentEvent.vcsSyncComplete, { - ...vcsSegmentEventProperties('git', 'clone', 'workspace already exists'), + ...vcsSegmentEventProperties( + 'git', + 'clone', + 'workspace already exists' + ), providerName, }); return { - errors: [`Workspace ${existingWorkspace.name} already exists. Please delete it before cloning.`], + errors: [ + `Workspace ${existingWorkspace.name} already exists. Please delete it before cloning.`, + ], }; } @@ -505,7 +568,9 @@ export const cloneGitRepoAction: ActionFunction = async ({ invariant(workspaceId, 'Workspace ID is required'); - return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_SPEC}`); + return redirect( + `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_SPEC}` + ); }; export const updateGitRepoAction: ActionFunction = async ({ @@ -541,7 +606,10 @@ export const updateGitRepoAction: ActionFunction = async ({ // Git Credentials const oauth2format = formData.get('oauth2format'); if (oauth2format) { - invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required'); + invariant( + oauth2format === 'gitlab' || oauth2format === 'github', + 'OAuth2 format is required' + ); const token = formData.get('token'); invariant(typeof token === 'string', 'Token is required'); const username = formData.get('username'); @@ -581,9 +649,7 @@ export const updateGitRepoAction: ActionFunction = async ({ return null; }; -export const resetGitRepoAction: ActionFunction = async ({ - params, -}) => { +export const resetGitRepoAction: ActionFunction = async ({ params }) => { const { workspaceId } = params; invariant(workspaceId, 'Workspace ID is required'); @@ -661,15 +727,21 @@ export const commitToGitRepoAction: ActionFunction = async ({ }); for (const item of changesToCommit) { - item.status.includes('deleted') ? await GitVCS.remove(item.path) : await GitVCS.add(item.path); + item.status.includes('deleted') + ? await GitVCS.remove(item.path) + : await GitVCS.add(item.path); } await GitVCS.commit(message); const providerName = getOauth2FormatName(repo?.credentials); - trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'commit'), providerName }); + trackSegmentEvent(SegmentEvent.vcsAction, { + ...vcsSegmentEventProperties('git', 'commit'), + providerName, + }); } catch (e) { - const message = e instanceof Error ? e.message : 'Error while committing changes'; + const message = + e instanceof Error ? e.message : 'Error while committing changes'; return { errors: [message] }; } @@ -677,10 +749,13 @@ export const commitToGitRepoAction: ActionFunction = async ({ }; export interface CreateNewGitBranchResult { - errors?: string[]; + errors?: string[]; } -export const createNewGitBranchAction: ActionFunction = async ({ request, params }): Promise => { +export const createNewGitBranchAction: ActionFunction = async ({ + request, + params, +}): Promise => { const { workspaceId } = params; invariant(workspaceId, 'Workspace ID is required'); @@ -703,9 +778,15 @@ export const createNewGitBranchAction: ActionFunction = async ({ request, params try { const providerName = getOauth2FormatName(repo?.credentials); await GitVCS.checkout(branch); - trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'create_branch'), providerName }); + trackSegmentEvent(SegmentEvent.vcsAction, { + ...vcsSegmentEventProperties('git', 'create_branch'), + providerName, + }); } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Something went wrong while creating a new branch'; + const errorMessage = + err instanceof Error + ? err.message + : 'Something went wrong while creating a new branch'; return { errors: [errorMessage], }; @@ -716,7 +797,7 @@ export const createNewGitBranchAction: ActionFunction = async ({ request, params export interface CheckoutGitBranchResult { errors?: string[]; - } +} export const checkoutGitBranchAction: ActionFunction = async ({ request, params, @@ -774,7 +855,10 @@ export interface MergeGitBranchResult { errors?: string[]; } -export const mergeGitBranchAction: ActionFunction = async ({ request, params }): Promise => { +export const mergeGitBranchAction: ActionFunction = async ({ + request, + params, +}): Promise => { const { workspaceId } = params; invariant(workspaceId, 'Workspace ID is required'); @@ -802,12 +886,18 @@ export const mergeGitBranchAction: ActionFunction = async ({ request, params }): const bufferId = await database.bufferChanges(); await GitVCS.checkout(branch); - trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'checkout_branch'), providerName }); + trackSegmentEvent(SegmentEvent.vcsAction, { + ...vcsSegmentEventProperties('git', 'checkout_branch'), + providerName, + }); await database.flushChanges(bufferId, true); - trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'merge_branch'), providerName }); + trackSegmentEvent(SegmentEvent.vcsAction, { + ...vcsSegmentEventProperties('git', 'merge_branch'), + providerName, + }); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; - return { errors: [errorMessage] }; + return { errors: [errorMessage] }; } return {}; @@ -817,7 +907,10 @@ export interface DeleteGitBranchResult { errors?: string[]; } -export const deleteGitBranchAction: ActionFunction = async ({ request, params }): Promise => { +export const deleteGitBranchAction: ActionFunction = async ({ + request, + params, +}): Promise => { const { workspaceId } = params; invariant(workspaceId, 'Workspace ID is required'); @@ -842,10 +935,13 @@ export const deleteGitBranchAction: ActionFunction = async ({ request, params }) try { const providerName = getOauth2FormatName(repo?.credentials); await GitVCS.deleteBranch(branch); - trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'delete_branch'), providerName }); + trackSegmentEvent(SegmentEvent.vcsAction, { + ...vcsSegmentEventProperties('git', 'delete_branch'), + providerName, + }); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; - return { errors: [errorMessage] }; + return { errors: [errorMessage] }; } return {}; @@ -858,7 +954,7 @@ export interface PushToGitRemoteResult { export const pushToGitRemoteAction: ActionFunction = async ({ request, params, -}): Promise => { +}): Promise => { const { workspaceId } = params; invariant(workspaceId, 'Workspace ID is required'); const workspace = await models.workspace.getById(workspaceId); @@ -896,11 +992,17 @@ export const pushToGitRemoteAction: ActionFunction = async ({ const providerName = getOauth2FormatName(gitRepository.credentials); try { await GitVCS.push(gitRepository.credentials); - trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', force ? 'force_push' : 'push'), providerName }); + trackSegmentEvent(SegmentEvent.vcsAction, { + ...vcsSegmentEventProperties('git', force ? 'force_push' : 'push'), + providerName, + }); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Unknown Error'; - trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'push', errorMessage), providerName }); + trackSegmentEvent(SegmentEvent.vcsAction, { + ...vcsSegmentEventProperties('git', 'push', errorMessage), + providerName, + }); if (err instanceof Errors.PushRejectedError) { return { @@ -919,7 +1021,7 @@ export const pushToGitRemoteAction: ActionFunction = async ({ }; export interface PullFromGitRemoteResult { - errors?: string[]; + errors?: string[]; } export const pullFromGitRemoteAction: ActionFunction = async ({ @@ -945,17 +1047,27 @@ export const pullFromGitRemoteAction: ActionFunction = async ({ const providerName = getOauth2FormatName(gitRepository.credentials); try { - await GitVCS.fetch({ singleBranch: true, depth: 1, credentials: gitRepository?.credentials }); + await GitVCS.fetch({ + singleBranch: true, + depth: 1, + credentials: gitRepository?.credentials, + }); } catch (e) { console.warn('Error fetching from remote'); } try { await GitVCS.pull(gitRepository.credentials); - trackSegmentEvent(SegmentEvent.vcsAction, { ...vcsSegmentEventProperties('git', 'pull'), providerName }); + trackSegmentEvent(SegmentEvent.vcsAction, { + ...vcsSegmentEventProperties('git', 'pull'), + providerName, + }); } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Unknown Error'; - trackSegmentEvent(SegmentEvent.vcsAction, vcsSegmentEventProperties('git', 'pull', errorMessage)); + trackSegmentEvent( + SegmentEvent.vcsAction, + vcsSegmentEventProperties('git', 'pull', errorMessage) + ); return { errors: [`${errorMessage}`], @@ -1006,8 +1118,10 @@ async function getGitChanges(vcs: typeof GitVCS, workspace: Workspace) { const statusNames: Record = {}; for (const doc of docs) { const name = (isApiSpec(doc) && doc.fileName) || doc.name || ''; - statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.json`)] = name; - statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.yml`)] = name; + statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.json`)] = + name; + statusNames[path.join(GIT_INSOMNIA_DIR_NAME, doc.type, `${doc._id}.yml`)] = + name; } // Create status items const items: Record = {}; @@ -1063,7 +1177,10 @@ export interface GitRollbackChangesResult { errors?: string[]; } -export const gitRollbackChangesAction: ActionFunction = async ({ params, request }): Promise => { +export const gitRollbackChangesAction: ActionFunction = async ({ + params, + request, +}): Promise => { const { workspaceId } = params; invariant(typeof workspaceId === 'string', 'Workspace Id is required'); @@ -1088,7 +1205,11 @@ export const gitRollbackChangesAction: ActionFunction = async ({ params, request const { changes } = await getGitChanges(GitVCS, workspace); const files = changes - .filter(i => changeType === 'modified' ? !i.status.includes('added') : i.status.includes('added')) + .filter(i => + changeType === 'modified' + ? !i.status.includes('added') + : i.status.includes('added') + ) // only rollback if editable .filter(i => i.editable) // only rollback if in selected path or for all paths @@ -1100,7 +1221,8 @@ export const gitRollbackChangesAction: ActionFunction = async ({ params, request await gitRollback(GitVCS, files); } catch (e) { - const errorMessage = e instanceof Error ? e.message : 'Error while rolling back changes'; + const errorMessage = + e instanceof Error ? e.message : 'Error while rolling back changes'; return { errors: [errorMessage], }; @@ -1108,3 +1230,28 @@ export const gitRollbackChangesAction: ActionFunction = async ({ params, request return {}; }; + +export interface GitStatusResult { + status: { + localChanges: number; + }; +} + +export const gitStatusLoader: LoaderFunction = async ({ + params, +}): Promise => { + const { workspaceId } = params; + invariant(typeof workspaceId === 'string', 'Workspace Id is required'); + + const workspace = await models.workspace.getById(workspaceId); + invariant(workspace, 'Workspace not found'); + + const { changes } = await getGitChanges(GitVCS, workspace); + const localChanges = changes.filter(i => i.editable).length; + + return { + status: { + localChanges, + }, + }; +}; diff --git a/packages/insomnia/src/ui/routes/remote-collections.tsx b/packages/insomnia/src/ui/routes/remote-collections.tsx index 04325f82f..c2a2d841c 100644 --- a/packages/insomnia/src/ui/routes/remote-collections.tsx +++ b/packages/insomnia/src/ui/routes/remote-collections.tsx @@ -1,4 +1,4 @@ -import { ActionFunction, LoaderFunction } from 'react-router-dom'; +import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'; import { database } from '../../common/database'; import { isNotNullOrUndefined } from '../../common/misc'; @@ -9,7 +9,8 @@ import { pullBackendProject } from '../../sync/vcs/pull-backend-project'; import { getVCS } from '../../sync/vcs/vcs'; import { invariant } from '../../utils/invariant'; -export const pullRemoteCollectionAction: ActionFunction = async ({ request }) => { +export const pullRemoteCollectionAction: ActionFunction = async ({ request, params }) => { + const { organizationId, projectId } = params; const formData = await request.formData(); const backendProjectId = formData.get('backendProjectId'); @@ -37,7 +38,11 @@ export const pullRemoteCollectionAction: ActionFunction = async ({ request }) => // Remove all backend projects for workspace first await newVCS.removeBackendProjectsForRoot(backendProject.rootDocumentId); - await pullBackendProject({ vcs: newVCS, backendProject, remoteProjects }); + const { workspaceId } = await pullBackendProject({ vcs: newVCS, backendProject, remoteProjects }); + + if (workspaceId) { + return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`); + } return null; }; diff --git a/packages/insomnia/src/ui/routes/test-results.tsx b/packages/insomnia/src/ui/routes/test-results.tsx index 762b118b9..dab77fe7a 100644 --- a/packages/insomnia/src/ui/routes/test-results.tsx +++ b/packages/insomnia/src/ui/routes/test-results.tsx @@ -23,7 +23,7 @@ export const indexLoader: LoaderFunction = async ({ params }) => { return redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${testSuiteId}/test-result/${testResult._id}`); } - return; + return null; }; export const loader: LoaderFunction = async ({ diff --git a/packages/insomnia/src/ui/routes/test-suite.tsx b/packages/insomnia/src/ui/routes/test-suite.tsx index f5211ca86..71b916211 100644 --- a/packages/insomnia/src/ui/routes/test-suite.tsx +++ b/packages/insomnia/src/ui/routes/test-suite.tsx @@ -221,7 +221,7 @@ export const indexLoader: LoaderFunction = async ({ params }) => { `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuites[0]._id}` ); } - return; + return null; }; interface LoaderData { diff --git a/packages/insomnia/src/ui/routes/unit-test.tsx b/packages/insomnia/src/ui/routes/unit-test.tsx index d9a19a519..ff0b33859 100644 --- a/packages/insomnia/src/ui/routes/unit-test.tsx +++ b/packages/insomnia/src/ui/routes/unit-test.tsx @@ -15,9 +15,10 @@ import * as models from '../../models'; import type { UnitTestSuite } from '../../models/unit-test-suite'; import { invariant } from '../../utils/invariant'; import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../components/base/dropdown'; +import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown'; import { ErrorBoundary } from '../components/error-boundary'; import { showPrompt } from '../components/modals'; -import { SidebarLayout } from '../components/sidebar-layout'; +import { SidebarFooter, SidebarLayout } from '../components/sidebar-layout'; import { Button } from '../components/themed-button'; import { TestRunStatus } from './test-results'; import TestSuiteRoute from './test-suite'; @@ -161,6 +162,9 @@ const TestRoute: FC = () => { ))}
+ + + } renderPaneOne={