mirror of
https://github.com/Kong/insomnia.git
synced 2025-12-23 22:28:58 -05:00
chore(Architecture): react router spa mode (#8902)
* first pass fix tests move all react things in dev try-package build stuff use http protocol instead of file handle refresh fix tests and routeloaderdata apths fix npm run dev fix sorts fix hidden browser window cleanup files Typesafe /auth/* routes typesafe commands route git-credentials typesafe routes import typesafe routes fix types fix hidden browser window invite and collaborators typesafe routes fix types remove workarounds fix dashboard test more types git typesafe routes fix runner test typesafe scratchpad navigation fix remove unused project route fix test routes add space request typesafe routes git credentials typescript conspiracy git typesafe routes typecheck debug bundles for inso fix test fix request group tab workspace typesafe routes feedback All routes use generated types Add typed fetchers to actions and loader Use typed fetchers in the app move git actions to the root * fix react-use usage update import source field Spawning npm fails the build Add ~ module resolution to vitest add initialEntry functionality fix update environment name requirement fix settings patch use loader for fetching the vault key and fix process.env.PLAYWRIGHT issue fix missing type Centralize useRouteLoaderData to routes Use environment for vitest tests that run browser code Update remaining fetchers to typesafe versions remove unused fetcher and add callback to sync Wrap load/submit in useCallback to keep them stable between re-renders Update deps lists with stable submit functions fix lint issue * fix ts issues * Add toaster to root * Use shell for running scripts with spawn on Windows * Move renderer bundling out of the build script * Fix request-pane test flakiness * update the url we use for internal purposes * Increase timeout for release workflow * fix flaky bundling test --------- Co-authored-by: jackkav <jackkav@gmail.com>
This commit is contained in:
2
.github/workflows/release-publish.yml
vendored
2
.github/workflows/release-publish.yml
vendored
@@ -20,7 +20,7 @@ env:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
NOTARY_REPOSITORY: ${{ env.NOTARY_REPOSITORY }}
|
||||
|
||||
4
.github/workflows/release-recurring.yml
vendored
4
.github/workflows/release-recurring.yml
vendored
@@ -17,7 +17,7 @@ env:
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
jobs:
|
||||
build-and-upload-artifacts:
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 30
|
||||
# Skip jobs for release PRs
|
||||
# windows on recurring should be portable
|
||||
if: ${{ !startsWith(github.head_ref, 'release/') }}
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
shell: bash
|
||||
run: NODE_OPTIONS='--max_old_space_size=6144' BUILD_TARGETS='${{ matrix.build-targets }}' npm run app-package
|
||||
|
||||
- name: Verify secure wrapper (Windows)
|
||||
- name: Build and verify secure wrapper (Windows)
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
shell: bash
|
||||
run: NODE_OPTIONS='--max_old_space_size=6144' ./build-secure-wrapper.sh
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"json.schemas": [],
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"files.associations": {
|
||||
"*.db": "ndjson",
|
||||
"*.jsonl": "ndjson",
|
||||
|
||||
@@ -157,6 +157,7 @@ export default tseslint.config(
|
||||
'**/*.config.js',
|
||||
'**/*.d.ts',
|
||||
'**/*.min.js',
|
||||
'**/*.js.map',
|
||||
'**/bin/*',
|
||||
'**/build/*',
|
||||
'**/coverage/*',
|
||||
@@ -165,6 +166,7 @@ export default tseslint.config(
|
||||
'**/docker/*',
|
||||
'**/electron/index.js',
|
||||
'**/fixtures',
|
||||
'**/hidden-window.js',
|
||||
'**/hidden-window-preload.js',
|
||||
'**/node_modules/*',
|
||||
'**/preload.js',
|
||||
@@ -172,6 +174,7 @@ export default tseslint.config(
|
||||
'**/traces/*',
|
||||
'**/verify-pkg.js',
|
||||
'**/__mocks__/*',
|
||||
'**/.react-router/*',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
1408
package-lock.json
generated
1408
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-unicorn": "^59.0.1",
|
||||
"globals": "^16.3.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -106,3 +106,13 @@ inso -w <INSO_NEDB_PATH>
|
||||
# using a binary
|
||||
./packages/insomnia-inso/binaries/insomnia-inso -w <INSO_NEDB_PATH>
|
||||
```
|
||||
|
||||
## How to debug the bundled assets
|
||||
|
||||
```bash
|
||||
DEBUG=1 npm run build
|
||||
```
|
||||
|
||||
This will generate an `artifacts` directory containing information about the bundled assets.
|
||||
The meta.json can be uploaded to https://esbuild.github.io/analyze/ to visualize the bundle.
|
||||
The bundle-analysis.log can be used to see the dependency tree of the bundle.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { build, type BuildOptions, context } from 'esbuild';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import { analyzeMetafile, build, type BuildOptions, context } from 'esbuild';
|
||||
const isProd = Boolean(process.env.NODE_ENV === 'production');
|
||||
const watch = Boolean(process.env.ESBUILD_WATCH);
|
||||
const isDebug = Boolean(process.env.DEBUG);
|
||||
const version = process.env.VERSION || 'dev';
|
||||
const config: BuildOptions = {
|
||||
outfile: './dist/index.js',
|
||||
bundle: true,
|
||||
metafile: isDebug,
|
||||
platform: 'node',
|
||||
minify: isProd,
|
||||
target: 'node22',
|
||||
@@ -44,5 +47,18 @@ if (watch) {
|
||||
}
|
||||
watch();
|
||||
} else {
|
||||
if (isDebug) {
|
||||
async function buildWithDebug() {
|
||||
const result = await build(config);
|
||||
|
||||
if (result.metafile) {
|
||||
fs.mkdirSync('./artifacts', { recursive: true });
|
||||
fs.writeFileSync('./artifacts/meta.json', JSON.stringify(result.metafile));
|
||||
fs.writeFileSync('./artifacts/bundle-analysis.log', await analyzeMetafile(result.metafile));
|
||||
}
|
||||
}
|
||||
|
||||
buildWithDebug();
|
||||
}
|
||||
build(config);
|
||||
}
|
||||
|
||||
@@ -10,15 +10,20 @@
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"electron": ["../insomnia/send-request/electron"]
|
||||
"~/*": [
|
||||
"../insomnia/src/*"
|
||||
],
|
||||
"electron": [
|
||||
"../insomnia/send-request/electron"
|
||||
]
|
||||
},
|
||||
/* remove this once react AlertModal is out of the plugins code path */
|
||||
"jsx": "react",
|
||||
/* Transpiling Options */
|
||||
"module": "CommonJS",
|
||||
"sourceMap": true,
|
||||
/* Runs in the DOM NOTE: this is inconsistent with reality */
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"ES2023",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
/* Strictness */
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
@@ -27,7 +32,13 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"include": [".eslintrc.js", "esbuild.ts", "package.json", "src", "../insomnia/types"],
|
||||
"include": [
|
||||
".eslintrc.js",
|
||||
"esbuild.ts",
|
||||
"package.json",
|
||||
"src",
|
||||
"../insomnia/types",
|
||||
],
|
||||
"exclude": [
|
||||
"**/*.test.ts",
|
||||
"**/__mocks__/*",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { fakerFunctions } from 'insomnia/src/templating/faker-functions';
|
||||
import { configure, type ConfigureOptions, type Environment as NunjuncksEnv } from 'nunjucks';
|
||||
import nunjucks, { type ConfigureOptions, type Environment as NunjuncksEnv } from 'nunjucks';
|
||||
|
||||
/** @ignore */
|
||||
class Interpolator {
|
||||
private engine: NunjuncksEnv;
|
||||
|
||||
constructor(config: ConfigureOptions) {
|
||||
this.engine = configure(config);
|
||||
this.engine = nunjucks.configure(config);
|
||||
}
|
||||
|
||||
render = (template: string, context: object) => {
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"../insomnia/src/*"
|
||||
],
|
||||
},
|
||||
/* Strictness */
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
@@ -19,8 +24,17 @@
|
||||
"verbatimModuleSyntax": true,
|
||||
"jsx": "react",
|
||||
/* If your code runs in the DOM: */
|
||||
"lib": ["es2023", "dom", "dom.iterable"]
|
||||
"lib": [
|
||||
"es2023",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
},
|
||||
"include": ["../insomnia/types"],
|
||||
"exclude": ["**/__tests__", "node_modules"]
|
||||
"include": [
|
||||
"../insomnia/types"
|
||||
],
|
||||
"exclude": [
|
||||
"**/__tests__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ test('can use bundled plugins, node-libcurl, httpsnippet, hidden browser window'
|
||||
await page.getByRole('button', { name: 'Done' }).click();
|
||||
|
||||
await page.getByLabel('Request Collection').getByTestId('sends request with pre-request script').press('Enter');
|
||||
await expect
|
||||
.soft(page.getByTestId('request-pane').getByTestId('OneLineEditor').getByText(`http://127.0.0.1:4010/echo`))
|
||||
.toBeVisible();
|
||||
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
await expect.soft(statusTag).toContainText('200 OK');
|
||||
await page.getByRole('tab', { name: 'Console' }).click();
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { test } from '../../playwright/test';
|
||||
|
||||
interface SegmentRequestData {
|
||||
batch: {
|
||||
timestamp: string;
|
||||
integrations: {};
|
||||
type: string;
|
||||
properties: {};
|
||||
name?: string;
|
||||
context: {
|
||||
app: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
os: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
library: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
};
|
||||
anonymousId: string;
|
||||
userId: string;
|
||||
messageId: string;
|
||||
_metadata: {
|
||||
nodeVersion: string;
|
||||
jsRuntime: string;
|
||||
};
|
||||
event?: string;
|
||||
}[];
|
||||
writeKey: string;
|
||||
sentAt: string;
|
||||
}
|
||||
|
||||
interface SegmentLog {
|
||||
url: string;
|
||||
data: SegmentRequestData[];
|
||||
}
|
||||
|
||||
test('analytics events are sent', async ({ page, app }) => {
|
||||
await app.evaluate(async ({ session }) => {
|
||||
// Capture segment requests to a global variable in main process
|
||||
globalThis.segmentLogs = [];
|
||||
|
||||
session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
|
||||
if (details.url.includes('segment')) {
|
||||
globalThis.segmentLogs.push({ url: details.url, data: details.uploadData });
|
||||
}
|
||||
callback({ cancel: false });
|
||||
});
|
||||
});
|
||||
|
||||
// Create a collection and requests that cause analytics events:
|
||||
await page.getByRole('button', { name: 'Create document', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
await page.getByTestId('workspace-debug').click();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await page.getByLabel('Create in collection').click();
|
||||
await page.getByRole('menuitemradio', { name: 'HTTP Request' }).press('Enter');
|
||||
}
|
||||
|
||||
const segmentLogs = await app.evaluate(() => globalThis.segmentLogs);
|
||||
|
||||
const decodedLogs: SegmentLog[] = segmentLogs.map(
|
||||
(log: { url: string; data: { type: string; bytes: number[] }[] }) => {
|
||||
return {
|
||||
url: log.url,
|
||||
data: log.data.map(data => JSON.parse(Buffer.from(Object.values(data.bytes)).toString('utf-8'))),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const analyticsBatch = decodedLogs[0].data[0].batch;
|
||||
const [appStartEvent, ...restEvents] = analyticsBatch;
|
||||
|
||||
// Analytics need at least 15 events to be sent
|
||||
expect.soft(analyticsBatch.length).toBeGreaterThanOrEqual(5);
|
||||
|
||||
// App start event
|
||||
expect.soft(appStartEvent.anonymousId).toBeTruthy();
|
||||
expect.soft(appStartEvent.event).toBe('App Started');
|
||||
|
||||
// First event should have userId and anonymousId
|
||||
expect.soft(restEvents[0].anonymousId).toBeTruthy();
|
||||
expect.soft(restEvents[0].userId).toBeTruthy();
|
||||
|
||||
// Last event should have userId and anonymousId
|
||||
expect.soft(restEvents.at(-1)?.anonymousId).toBeTruthy();
|
||||
expect.soft(restEvents.at(-1)?.userId).toBeTruthy();
|
||||
});
|
||||
@@ -69,7 +69,7 @@ test.describe('Dashboard', () => {
|
||||
await page.getByLabel('Files').getByLabel('My Design Document').getByRole('button').click();
|
||||
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")');
|
||||
await page.getByRole('button', { name: 'Rename' }).click();
|
||||
await expect.soft(page.locator('.app')).toContainText('test123');
|
||||
|
||||
// Duplicate document
|
||||
@@ -92,7 +92,7 @@ test.describe('Dashboard', () => {
|
||||
await page.click('text=CollectionMy Collectionjust now >> button');
|
||||
await page.getByRole('menuitem', { name: 'Rename' }).click();
|
||||
await page.locator('text=Rename CollectionName Rename >> input[type="text"]').fill('collection123');
|
||||
await page.click('#root button:has-text("Rename")');
|
||||
await page.getByRole('button', { name: 'Rename' }).click();
|
||||
await expect.soft(page.locator('.app')).toContainText('collection123');
|
||||
|
||||
// Duplicate collection
|
||||
|
||||
@@ -15,7 +15,6 @@ test.describe('Debug-Sidebar', () => {
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
|
||||
await page.getByLabel('simple').click();
|
||||
//Open Properties in Request Sidebar
|
||||
const requestLocator = page.getByLabel('Request Collection').getByRole('row', { name: 'example http' });
|
||||
await page.getByLabel('Request Collection').getByRole('row', { name: 'example http' }).click();
|
||||
await page
|
||||
.getByLabel('Request Collection')
|
||||
|
||||
@@ -32,13 +32,13 @@ test.describe('test hidden window handling', () => {
|
||||
await page.click('text=Request was cancelled');
|
||||
|
||||
await page.getByText('Special template tag format').click();
|
||||
await expect.soft(page.getByText(`{{ _['examplehost']}}`)).toBeVisible();
|
||||
await expect.soft(page.getByText(`_['examplehost']`)).toBeVisible();
|
||||
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
await page.getByText('200 OK').click();
|
||||
|
||||
await page.getByText('Multiple template tags format').click();
|
||||
await expect.soft(page.getByText(`{{_['a']['b']['c']['url']}}`)).toBeVisible();
|
||||
await expect.soft(page.getByText(`_['a']['b']['c']['url']`)).toBeVisible();
|
||||
|
||||
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
|
||||
await page.getByText('200 OK').click();
|
||||
|
||||
@@ -4,8 +4,6 @@ test('Request tabs', async ({ page }) => {
|
||||
// Create new collection
|
||||
await page.getByRole('button', { name: 'Create request collection', exact: true }).click();
|
||||
|
||||
await page.getByLabel('Create in collection').click();
|
||||
await page.getByRole('menuitemradio', { name: 'HTTP Request' }).press('Enter');
|
||||
await page.getByRole('tab', { name: 'Body' }).click();
|
||||
await page.getByRole('button', { name: 'Body' }).click();
|
||||
await page.getByRole('option', { name: 'JSON' }).click();
|
||||
|
||||
2
packages/insomnia/.gitignore
vendored
2
packages/insomnia/.gitignore
vendored
@@ -4,4 +4,4 @@ build
|
||||
# Generated
|
||||
src/*.js
|
||||
src/*.js.map
|
||||
|
||||
.react-router/
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function build(options: Options) {
|
||||
|
||||
const env: Record<string, string> = __DEV__
|
||||
? {
|
||||
'process.env.APP_RENDER_URL': JSON.stringify(`http://localhost:${PORT}/index.html`),
|
||||
'process.env.APP_RENDER_URL': JSON.stringify(`http://localhost:${PORT}`),
|
||||
'process.env.HIDDEN_BROWSER_WINDOW_URL': JSON.stringify(`http://localhost:${PORT}/hidden-window.html`),
|
||||
'process.env.NODE_ENV': JSON.stringify('development'),
|
||||
'process.env.INSOMNIA_ENV': JSON.stringify('development'),
|
||||
@@ -59,6 +59,22 @@ export default async function build(options: Options) {
|
||||
},
|
||||
};
|
||||
|
||||
const hiddenBrowserWindowBuildOptions: BuildOptions = {
|
||||
entryPoints: ['./src/hidden-window.ts'],
|
||||
// TODO: make all of these outputs use a .min.js convention to simplify ignore files
|
||||
outfile: path.join(outdir, 'hidden-window.js'),
|
||||
target: 'esnext',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
sourcemap: true,
|
||||
format: 'cjs',
|
||||
// TODO: remove below, This indicates that libcurl is being imported when it shouldn't be
|
||||
external: ['electron'],
|
||||
loader: {
|
||||
'.node': 'copy',
|
||||
},
|
||||
};
|
||||
|
||||
const mainBuildOptions: BuildOptions = {
|
||||
entryPoints: ['./src/main.development.ts'],
|
||||
outfile: path.join(outdir, 'main.min.js'),
|
||||
@@ -114,6 +130,10 @@ export default async function build(options: Options) {
|
||||
...preloadBuildOptions,
|
||||
plugins: [restartElectronPlugin('preload')],
|
||||
});
|
||||
const hiddenBrowserWindowContext = await esbuild.context({
|
||||
...hiddenBrowserWindowBuildOptions,
|
||||
plugins: [restartElectronPlugin('hidden-browser-window')],
|
||||
});
|
||||
const mainContext = await esbuild.context({
|
||||
...mainBuildOptions,
|
||||
plugins: [restartElectronPlugin('main')],
|
||||
@@ -138,14 +158,16 @@ export default async function build(options: Options) {
|
||||
};
|
||||
|
||||
const preloadWatch = await preloadContext.watch();
|
||||
const hiddenWindowWatch = await hiddenBrowserWindowContext.watch();
|
||||
const mainWatch = await mainContext.watch();
|
||||
const hiddenWindowWatch = await hiddenPreloadContext.watch();
|
||||
return Promise.all([preloadWatch, mainWatch, hiddenWindowWatch]);
|
||||
const hiddenWindowPreloadWatch = await hiddenPreloadContext.watch();
|
||||
return Promise.all([preloadWatch, hiddenWindowPreloadWatch, mainWatch, hiddenWindowWatch]);
|
||||
}
|
||||
const preload = esbuild.build(preloadBuildOptions);
|
||||
const hiddenBrowserWindow = esbuild.build(hiddenBrowserWindowBuildOptions);
|
||||
const hiddenBrowserWindowPreload = esbuild.build(hiddenBrowserWindowPreloadBuildOptions);
|
||||
const main = esbuild.build(mainBuildOptions);
|
||||
return Promise.all([main, preload, hiddenBrowserWindowPreload]).catch(err => {
|
||||
return Promise.all([main, preload, hiddenBrowserWindow, hiddenBrowserWindowPreload]).catch(err => {
|
||||
console.error('[Build] Build failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"main": "src/main.min.js",
|
||||
"scripts": {
|
||||
"verify-bundle-plugins": "esr --cache ./scripts/verify-bundle-plugins.ts",
|
||||
"build": "esr --cache ./scripts/build.ts --noErrorTruncation",
|
||||
"build": "react-router build && esr --cache ./scripts/build.ts --noErrorTruncation",
|
||||
"build:react-router": "react-router build",
|
||||
"generate:schema": "esr ./src/schema.ts",
|
||||
"build:main.min.js": "cross-env NODE_ENV=development esr esbuild.main.ts",
|
||||
"lint": "eslint . --ext .js,.ts,.tsx --cache",
|
||||
@@ -31,8 +32,7 @@
|
||||
"start:electron": "cross-env NODE_ENV=development esr esbuild.main.ts && electron --inspect=5858 .",
|
||||
"start:electron:autoRestart": "cross-env NODE_ENV=development esr esbuild.main.ts --autoRestart",
|
||||
"test": "vitest run",
|
||||
"electron:dev-build": "electron ./build/main.min.js",
|
||||
"type-check": "tsc --noEmit --project tsconfig.json",
|
||||
"type-check": "react-router typegen && tsc --noEmit --project tsconfig.json",
|
||||
"type-check:watch": "npm run type-check -- --watch",
|
||||
"convert-svg": "npm_config_yes=true npx @svgr/cli@6.4.0 --no-index --config-file svgr.config.js --out-dir src/ui/components/assets/svgr src/ui/components/assets/"
|
||||
},
|
||||
@@ -78,6 +78,7 @@
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"httpsnippet": "^2.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"isbot": "^5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"jshint": "^2.13.6",
|
||||
@@ -106,6 +107,10 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@getinsomnia/api-client": "0.0.10",
|
||||
"@getinsomnia/srp-js": "1.0.0-alpha.1",
|
||||
"@react-router/node": "^7.7.0",
|
||||
"@react-router/serve": "^7.7.0",
|
||||
"@react-router/dev": "^7.7.0",
|
||||
"@react-router/fs-routes": "^7.7.0",
|
||||
"@sentry/electron": "^6.5.0",
|
||||
"@stoplight/spectral-core": "^1.20.0",
|
||||
"@stoplight/spectral-formats": "^1.8.2",
|
||||
@@ -180,11 +185,11 @@
|
||||
"react-aria-components": "^1.8.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router": "^7.6.0",
|
||||
"react-router": "^7.7.0",
|
||||
"react-stately": "3.37.0",
|
||||
"react-use": "^17.6.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tinykeys": "^2.1.0",
|
||||
"tinykeys": "^3.0.0",
|
||||
"type-fest": "^4.40.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.1",
|
||||
|
||||
7
packages/insomnia/react-router.config.ts
Normal file
7
packages/insomnia/react-router.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { type Config } from '@react-router/dev/config';
|
||||
|
||||
export default {
|
||||
appDirectory: 'src',
|
||||
ssr: false,
|
||||
serverModuleFormat: 'cjs',
|
||||
} satisfies Config;
|
||||
@@ -1,9 +1,6 @@
|
||||
import childProcess from 'node:child_process';
|
||||
import { cp, mkdir, rm } from 'node:fs/promises';
|
||||
import { cp, mkdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import * as vite from 'vite';
|
||||
|
||||
import buildMainAndPreload from '../esbuild.main';
|
||||
|
||||
// Start build if ran from CLI
|
||||
@@ -21,8 +18,7 @@ if (require.main === module) {
|
||||
export const start = async () => {
|
||||
console.log('[build] Starting build');
|
||||
|
||||
console.log(`[build] npm: ${childProcess.spawnSync('npm', ['--version']).stdout}`.trim());
|
||||
console.log(`[build] node: ${childProcess.spawnSync('node', ['--version']).stdout}`.trim());
|
||||
console.log(`[build] node: ${process.version}`.trim());
|
||||
|
||||
if (process.version.indexOf('v22.') !== 0) {
|
||||
console.log('[build] Node 22.x.x is required to build');
|
||||
@@ -31,21 +27,11 @@ export const start = async () => {
|
||||
|
||||
const buildFolder = path.join('../build');
|
||||
|
||||
// Remove folders first
|
||||
console.log('[build] Removing existing directories');
|
||||
await rm(path.resolve(__dirname, buildFolder), { recursive: true, force: true });
|
||||
|
||||
console.log('[build] Building main.min.js and preload');
|
||||
await buildMainAndPreload({
|
||||
mode: 'production',
|
||||
});
|
||||
|
||||
console.log('[build] Building renderer');
|
||||
|
||||
await vite.build({
|
||||
configFile: path.join(__dirname, '..', 'vite.config.ts'),
|
||||
});
|
||||
|
||||
// Copy necessary files
|
||||
console.log('[build] Copying files');
|
||||
const copyFiles = async (relSource: string, relDest: string) => {
|
||||
@@ -58,6 +44,7 @@ export const start = async () => {
|
||||
await copyFiles('../src/static', path.join(buildFolder, 'static'));
|
||||
await copyFiles('../src/icons', buildFolder);
|
||||
await copyFiles('../src/main/lint-process.mjs', path.join(buildFolder, 'main/lint-process.mjs'));
|
||||
await copyFiles('../src/hidden-window.html', path.join(buildFolder, 'hidden-window.html'));
|
||||
|
||||
console.log('[build] Complete!');
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ export async function logout() {
|
||||
}
|
||||
}
|
||||
|
||||
_unsetSessionData();
|
||||
await _unsetSessionData();
|
||||
window.main.loginStateChange();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { MockServer } from '../../models/mock-server';
|
||||
import {
|
||||
ACTIVITY_DEBUG,
|
||||
ACTIVITY_HOME,
|
||||
ACTIVITY_SPEC,
|
||||
ACTIVITY_UNIT_TEST,
|
||||
FLEXIBLE_URL_REGEX,
|
||||
getContentTypeName,
|
||||
getMockServiceBinURL,
|
||||
@@ -41,22 +37,22 @@ describe('URL Regex', () => {
|
||||
|
||||
describe('isWorkspaceActivity', () => {
|
||||
it('should return true', () => {
|
||||
expect(isWorkspaceActivity(ACTIVITY_SPEC)).toBe(true);
|
||||
expect(isWorkspaceActivity(ACTIVITY_DEBUG)).toBe(true);
|
||||
expect(isWorkspaceActivity(ACTIVITY_UNIT_TEST)).toBe(true);
|
||||
expect(isWorkspaceActivity('spec')).toBe(true);
|
||||
expect(isWorkspaceActivity('debug')).toBe(true);
|
||||
expect(isWorkspaceActivity('unittest')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
expect(isWorkspaceActivity(ACTIVITY_HOME)).toBe(false);
|
||||
expect(isWorkspaceActivity('home')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidActivity', () => {
|
||||
it('should return true', () => {
|
||||
expect(isValidActivity(ACTIVITY_SPEC)).toBe(true);
|
||||
expect(isValidActivity(ACTIVITY_DEBUG)).toBe(true);
|
||||
expect(isValidActivity(ACTIVITY_UNIT_TEST)).toBe(true);
|
||||
expect(isValidActivity(ACTIVITY_HOME)).toBe(true);
|
||||
expect(isValidActivity('spec')).toBe(true);
|
||||
expect(isValidActivity('debug')).toBe(true);
|
||||
expect(isValidActivity('unittest')).toBe(true);
|
||||
expect(isValidActivity('home')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false', () => {
|
||||
|
||||
@@ -176,23 +176,19 @@ export const DEFAULT_SIDEBAR_SIZE = 25;
|
||||
|
||||
// Activities
|
||||
export type GlobalActivity = 'spec' | 'debug' | 'unittest' | 'home';
|
||||
export const ACTIVITY_SPEC: GlobalActivity = 'spec';
|
||||
export const ACTIVITY_DEBUG: GlobalActivity = 'debug';
|
||||
export const ACTIVITY_UNIT_TEST: GlobalActivity = 'unittest';
|
||||
export const ACTIVITY_HOME: GlobalActivity = 'home';
|
||||
|
||||
export const isWorkspaceActivity = (activity?: string): activity is GlobalActivity =>
|
||||
isDesignActivity(activity) || isCollectionActivity(activity);
|
||||
|
||||
export const isDesignActivity = (activity?: string): activity is GlobalActivity => {
|
||||
switch (activity) {
|
||||
case ACTIVITY_SPEC:
|
||||
case ACTIVITY_DEBUG:
|
||||
case ACTIVITY_UNIT_TEST: {
|
||||
case 'spec':
|
||||
case 'debug':
|
||||
case 'unittest': {
|
||||
return true;
|
||||
}
|
||||
|
||||
case ACTIVITY_HOME:
|
||||
case 'home':
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
@@ -201,13 +197,13 @@ export const isDesignActivity = (activity?: string): activity is GlobalActivity
|
||||
|
||||
export const isCollectionActivity = (activity?: string): activity is GlobalActivity => {
|
||||
switch (activity) {
|
||||
case ACTIVITY_DEBUG: {
|
||||
case 'debug': {
|
||||
return true;
|
||||
}
|
||||
|
||||
case ACTIVITY_SPEC:
|
||||
case ACTIVITY_UNIT_TEST:
|
||||
case ACTIVITY_HOME:
|
||||
case 'spec':
|
||||
case 'unittest':
|
||||
case 'home':
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
@@ -216,10 +212,10 @@ export const isCollectionActivity = (activity?: string): activity is GlobalActiv
|
||||
|
||||
export const isValidActivity = (activity: string): activity is GlobalActivity => {
|
||||
switch (activity) {
|
||||
case ACTIVITY_SPEC:
|
||||
case ACTIVITY_DEBUG:
|
||||
case ACTIVITY_UNIT_TEST:
|
||||
case ACTIVITY_HOME: {
|
||||
case 'spec':
|
||||
case 'debug':
|
||||
case 'unittest':
|
||||
case 'home': {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
import type { CurrentPlan } from '~/models/organization';
|
||||
|
||||
import { type ApiSpec, isApiSpec } from '../models/api-spec';
|
||||
import { type CookieJar, isCookieJar } from '../models/cookie-jar';
|
||||
import { type BaseEnvironment, type Environment, isEnvironment } from '../models/environment';
|
||||
@@ -15,7 +17,6 @@ import { isUnitTest, type UnitTest } from '../models/unit-test';
|
||||
import { isUnitTestSuite, type UnitTestSuite } from '../models/unit-test-suite';
|
||||
import { isWebSocketRequest, type WebSocketRequest } from '../models/websocket-request';
|
||||
import { isWorkspace, type Workspace } from '../models/workspace';
|
||||
import type { CurrentPlan } from '../ui/organization-utils';
|
||||
import { convert, type InsomniaImporter } from '../utils/importers/convert';
|
||||
import type { ImportEntry } from '../utils/importers/entities';
|
||||
import { id as postmanEnvImporterId } from '../utils/importers/importers/postman-env';
|
||||
|
||||
104
packages/insomnia/src/entry.client.tsx
Normal file
104
packages/insomnia/src/entry.client.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import './ui/rendererListeners';
|
||||
|
||||
import { startTransition, StrictMode } from 'react';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
import type { SessionData } from 'react-router';
|
||||
import { HydratedRouter } from 'react-router/dom';
|
||||
|
||||
import { migrateFromLocalStorage, setSessionData, setVaultSessionData } from './account/session';
|
||||
import { getInsomniaSession, getInsomniaVaultKey, getInsomniaVaultSalt, getSkipOnboarding } from './common/constants';
|
||||
import { database } from './common/database';
|
||||
import { initializeLogging } from './common/log';
|
||||
import { settings } from './models';
|
||||
import { initNewOAuthSession } from './network/o-auth-2/get-token';
|
||||
import { init as initPlugins } from './plugins';
|
||||
import { applyColorScheme } from './plugins/misc';
|
||||
import { HtmlElementWrapper } from './ui/components/html-element-wrapper';
|
||||
import { showModal } from './ui/components/modals';
|
||||
import { AlertModal } from './ui/components/modals/alert-modal';
|
||||
import { PromptModal } from './ui/components/modals/prompt-modal';
|
||||
import { WrapperModal } from './ui/components/modals/wrapper-modal';
|
||||
import { initializeSentry } from './ui/sentry';
|
||||
import { getInitialEntry } from './utils/router';
|
||||
|
||||
initializeSentry();
|
||||
initializeLogging();
|
||||
|
||||
try {
|
||||
window.showAlert = options => showModal(AlertModal, options);
|
||||
window.showPrompt = options =>
|
||||
showModal(PromptModal, {
|
||||
...options,
|
||||
title: options?.title || '',
|
||||
});
|
||||
window.showWrapper = options =>
|
||||
showModal(WrapperModal, {
|
||||
...options,
|
||||
title: options?.title || '',
|
||||
body: <HtmlElementWrapper el={options?.body} onUnmount={options?.onHide} />,
|
||||
});
|
||||
|
||||
// In order to run playwight tests that simulate a logged in user
|
||||
// we need to inject state into localStorage
|
||||
const skipOnboarding = getSkipOnboarding();
|
||||
if (skipOnboarding) {
|
||||
window.localStorage.setItem('hasSeenOnboardingV11', skipOnboarding.toString());
|
||||
window.localStorage.setItem('hasUserLoggedInBefore', skipOnboarding.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[onboarding] Failed to parse session data', e);
|
||||
}
|
||||
|
||||
await database.initClient();
|
||||
await initPlugins();
|
||||
|
||||
await migrateFromLocalStorage();
|
||||
|
||||
// Check if there is a Session provided by an env variable and use this
|
||||
const insomniaSession = getInsomniaSession();
|
||||
const insomniaVaultKey = getInsomniaVaultKey() || '';
|
||||
const insomniaVaultSalt = getInsomniaVaultSalt() || '';
|
||||
if (insomniaSession) {
|
||||
try {
|
||||
const session = JSON.parse(insomniaSession) as SessionData;
|
||||
await setSessionData(
|
||||
session.id,
|
||||
session.accountId,
|
||||
session.firstName,
|
||||
session.lastName,
|
||||
session.email,
|
||||
session.symmetricKey,
|
||||
session.publicKey,
|
||||
session.encPrivateKey,
|
||||
);
|
||||
if (insomniaVaultSalt || insomniaVaultKey) {
|
||||
await setVaultSessionData(insomniaVaultSalt, insomniaVaultKey);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[init] Failed to parse session data', e);
|
||||
}
|
||||
}
|
||||
|
||||
const appSettings = await settings.getOrCreate();
|
||||
|
||||
if (appSettings.clearOAuth2SessionOnRestart) {
|
||||
initNewOAuthSession();
|
||||
}
|
||||
|
||||
await applyColorScheme(appSettings);
|
||||
|
||||
const initialEntry = await getInitialEntry();
|
||||
|
||||
if (typeof initialEntry === 'string' && window.location.pathname !== initialEntry) {
|
||||
console.log('[entry.client] Initial entry:', initialEntry);
|
||||
window.location.pathname = initialEntry;
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<HydratedRouter />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
65
packages/insomnia/src/entry.server.tsx
Normal file
65
packages/insomnia/src/entry.server.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { PassThrough } from 'node:stream';
|
||||
|
||||
import { createReadableStreamFromReadable } from '@react-router/node';
|
||||
import { isbot } from 'isbot';
|
||||
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
|
||||
import { renderToPipeableStream } from 'react-dom/server';
|
||||
import type { EntryContext } from 'react-router';
|
||||
import { ServerRouter } from 'react-router';
|
||||
|
||||
export const streamTimeout = 5_000;
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
routerContext: EntryContext,
|
||||
// loadContext: AppLoadContext
|
||||
// If you have middleware enabled:
|
||||
// loadContext: unstable_RouterContextProvider
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const userAgent = request.headers.get('user-agent');
|
||||
|
||||
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
|
||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
||||
const readyOption: keyof RenderToPipeableStreamOptions =
|
||||
(userAgent && isbot(userAgent)) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady';
|
||||
|
||||
const { pipe, abort } = renderToPipeableStream(<ServerRouter context={routerContext} url={request.url} />, {
|
||||
[readyOption]() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
}),
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Abort the rendering stream after the `streamTimeout` so it has time to
|
||||
// flush down the rejected boundaries
|
||||
setTimeout(abort, streamTimeout + 1000);
|
||||
});
|
||||
}
|
||||
@@ -11,6 +11,6 @@
|
||||
|
||||
<body>
|
||||
<h1>Hidden Browser Window</h1>
|
||||
<script src="./hidden-window.ts" type="module"></script>
|
||||
<script src="./hidden-window.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US" class="h-full w-full overflow-hidden">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
font-src
|
||||
'self'
|
||||
data:
|
||||
;
|
||||
connect-src
|
||||
'self'
|
||||
data:
|
||||
insomnia-event-source:
|
||||
insomnia-templating-worker-database:
|
||||
https:
|
||||
http:
|
||||
;
|
||||
default-src
|
||||
*
|
||||
insomnia://*
|
||||
;
|
||||
img-src
|
||||
blob:
|
||||
data:
|
||||
*
|
||||
insomnia://*
|
||||
;
|
||||
script-src
|
||||
'self'
|
||||
'unsafe-eval'
|
||||
;
|
||||
style-src
|
||||
'self'
|
||||
'unsafe-inline'
|
||||
;
|
||||
media-src
|
||||
blob:
|
||||
data:
|
||||
mediastream:
|
||||
*
|
||||
insomnia://*
|
||||
;
|
||||
"
|
||||
/>
|
||||
<link rel="stylesheet" href="./ui/css/styles.css" />
|
||||
</head>
|
||||
|
||||
<body class="h-full w-full">
|
||||
<div id="root" class="h-full w-full">
|
||||
<div id="app-loading-indicator" class="fixed left-0 top-0 flex h-full w-full items-center justify-center">
|
||||
<div class="relative">
|
||||
<svg
|
||||
viewBox="0 0 378 378"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
width="100"
|
||||
>
|
||||
<circle
|
||||
cx="36"
|
||||
cy="36"
|
||||
r="36"
|
||||
fill="none"
|
||||
stroke="var(--hl, rgb(130, 130, 130))"
|
||||
stroke-opacity="0.1"
|
||||
stroke-width="4px"
|
||||
transform="translate(-323 -111) translate(359.016 147.016) scale(4.24956)"
|
||||
></circle>
|
||||
<circle
|
||||
cx="36"
|
||||
cy="36"
|
||||
r="36"
|
||||
fill="none"
|
||||
stroke="var(--hl, rgb(130, 130, 130))"
|
||||
stroke-opacity="0.8"
|
||||
stroke-width="4px"
|
||||
stroke-dasharray="56,172,0,0"
|
||||
transform="translate(-323 -111) translate(359.016 147.016) scale(4.24956)"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 36 36"
|
||||
to="360 36 36"
|
||||
dur="0.8s"
|
||||
repeatCount="indefinite"
|
||||
additive="sum"
|
||||
/>
|
||||
</circle>
|
||||
<path
|
||||
d="M19 37.033c9.96 0 18.033-8.073 18.033-18.033S28.96.967 19 .967.967 9.04.967 19 9.04 37.033 19 37.033z"
|
||||
fill="#fff"
|
||||
fill-rule="nonzero"
|
||||
transform="translate(-323 -111) translate(431.258 219.258) scale(4.24956)"
|
||||
></path>
|
||||
<path
|
||||
d="M19 0C8.506 0 0 8.506 0 19s8.506 19 19 19 19-8.506 19-19S29.494 0 19 0zm0 1.932c9.426 0 17.068 7.642 17.068 17.068 0 9.426-7.642 17.068-17.068 17.068-9.426 0-17.068-7.642-17.068-17.068C1.932 9.574 9.574 1.932 19 1.932z"
|
||||
fill="#4000bf"
|
||||
fill-rule="nonzero"
|
||||
transform="translate(-323 -111) translate(431.258 219.258) scale(4.24956)"
|
||||
></path>
|
||||
<path
|
||||
d="M19.214 5.474c7.47 0 13.525 6.057 13.525 13.526 0 7.469-6.055 13.526-13.525 13.526-7.47 0-13.526-6.057-13.526-13.526 0-1.825.362-3.567 1.019-5.156a5.266 5.266 0 004.243 2.15c2.885 0 5.26-2.375 5.26-5.261a5.263 5.263 0 00-2.15-4.242 13.5 13.5 0 015.154-1.017z"
|
||||
fill="url(#_Linear1)"
|
||||
transform="translate(-323 -111) translate(431.258 219.258) scale(4.24956)"
|
||||
></path>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="_Linear1"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(-90 25.87 6.655) scale(27.0508)"
|
||||
>
|
||||
<stop offset="0" stop-color="#7400e1"></stop>
|
||||
<stop offset="1" stop-color="#4000bf"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="graphql-explorer-container"></div>
|
||||
<div id="hints-container" class="theme--dropdown__menu"></div>
|
||||
<script src="./renderer.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,4 @@
|
||||
import path from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { parse as urlParse } from 'node:url';
|
||||
|
||||
@@ -178,6 +179,15 @@ export async function registerInsomniaProtocols() {
|
||||
|
||||
if (!protocol.isProtocolHandled(httpsScheme)) {
|
||||
protocol.handle(httpsScheme, async request => {
|
||||
const url = new URL(request.url);
|
||||
if (url.hostname === 'insomnia-app.local') {
|
||||
const rootDir = path.resolve(__dirname, 'client');
|
||||
const filePath = path.join(rootDir, url.pathname.startsWith('/assets') ? url.pathname : 'index.html');
|
||||
console.log(`Loading index for: ${url.pathname} from: ${filePath}`);
|
||||
|
||||
return await net.fetch(`file://${filePath}`, { bypassCustomProtocolHandlers: true });
|
||||
}
|
||||
|
||||
return net.fetch(request, { bypassCustomProtocolHandlers: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Errors, type HeadStatus, type PromiseFsClient, type StageStatus, type W
|
||||
import { v4 } from 'uuid';
|
||||
import YAML, { parse } from 'yaml';
|
||||
|
||||
import { type GitCredentials } from '~/models/git-repository';
|
||||
|
||||
import {
|
||||
getApiBaseURL,
|
||||
getAppWebsiteBaseURL,
|
||||
@@ -31,7 +33,6 @@ import GitVCS, {
|
||||
GIT_INSOMNIA_DIR,
|
||||
GIT_INSOMNIA_DIR_NAME,
|
||||
GIT_INTERNAL_DIR,
|
||||
type GitCredentials,
|
||||
MergeConflictError,
|
||||
} from '../sync/git/git-vcs';
|
||||
import { MemClient } from '../sync/git/mem-client';
|
||||
@@ -593,18 +594,14 @@ export const initGitRepoCloneAction = async ({
|
||||
uri,
|
||||
authorName,
|
||||
authorEmail,
|
||||
token,
|
||||
username,
|
||||
oauth2format,
|
||||
credentials,
|
||||
ref,
|
||||
}: {
|
||||
organizationId: string;
|
||||
uri: string;
|
||||
authorName: string;
|
||||
authorEmail: string;
|
||||
token: string;
|
||||
username: string;
|
||||
oauth2format?: string;
|
||||
credentials: GitCredentials;
|
||||
ref?: string;
|
||||
}): Promise<
|
||||
| {
|
||||
@@ -630,24 +627,20 @@ export const initGitRepoCloneAction = async ({
|
||||
};
|
||||
|
||||
// Git Credentials
|
||||
if (oauth2format) {
|
||||
invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required');
|
||||
|
||||
repoSettingsPatch.credentials = {
|
||||
username,
|
||||
token,
|
||||
oauth2format,
|
||||
};
|
||||
if ('oauth2format' in credentials) {
|
||||
invariant(
|
||||
credentials.oauth2format === 'gitlab' || credentials.oauth2format === 'github',
|
||||
'OAuth2 format is required',
|
||||
);
|
||||
} else if ('password' in credentials) {
|
||||
invariant(typeof credentials.username === 'string', 'Username is required');
|
||||
invariant(typeof credentials.password === 'string', 'Password is required');
|
||||
} else {
|
||||
invariant(typeof token === 'string', 'Token is required');
|
||||
invariant(typeof username === 'string', 'Username is required');
|
||||
|
||||
repoSettingsPatch.credentials = {
|
||||
password: token,
|
||||
username,
|
||||
};
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
|
||||
repoSettingsPatch.credentials = credentials;
|
||||
|
||||
repoSettingsPatch.needsFullClone = true;
|
||||
|
||||
const inMemoryFsClient = MemClient.createClient();
|
||||
@@ -704,53 +697,41 @@ export const cloneGitRepoAction = async ({
|
||||
cloneIntoProjectId,
|
||||
name,
|
||||
uri,
|
||||
authorName,
|
||||
authorEmail,
|
||||
token,
|
||||
username,
|
||||
oauth2format,
|
||||
author,
|
||||
credentials,
|
||||
ref,
|
||||
}: {
|
||||
organizationId: string;
|
||||
projectId?: string;
|
||||
cloneIntoProjectId?: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
credentials: GitCredentials;
|
||||
name?: string;
|
||||
uri: string;
|
||||
authorName: string;
|
||||
authorEmail: string;
|
||||
token: string;
|
||||
username: string;
|
||||
oauth2format?: string;
|
||||
ref?: string;
|
||||
}) => {
|
||||
try {
|
||||
const repoSettingsPatch: Partial<GitRepository> = {};
|
||||
repoSettingsPatch.uri = parseGitToHttpsURL(uri);
|
||||
repoSettingsPatch.author = author;
|
||||
|
||||
// Git Credentials
|
||||
if ('oauth2format' in credentials) {
|
||||
invariant(
|
||||
credentials.oauth2format === 'gitlab' || credentials.oauth2format === 'github',
|
||||
'OAuth2 format is required',
|
||||
);
|
||||
} else if ('password' in credentials) {
|
||||
invariant(typeof credentials.password === 'string', 'Password is required');
|
||||
invariant(typeof credentials.username === 'string', 'Username is required');
|
||||
}
|
||||
|
||||
repoSettingsPatch.credentials = credentials;
|
||||
|
||||
if (!projectId) {
|
||||
const repoSettingsPatch: Partial<GitRepository> = {};
|
||||
repoSettingsPatch.uri = parseGitToHttpsURL(uri);
|
||||
repoSettingsPatch.author = {
|
||||
name: authorName,
|
||||
email: authorEmail,
|
||||
};
|
||||
|
||||
// Git Credentials
|
||||
if (oauth2format) {
|
||||
invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required');
|
||||
|
||||
repoSettingsPatch.credentials = {
|
||||
username,
|
||||
token,
|
||||
oauth2format,
|
||||
};
|
||||
} else {
|
||||
invariant(typeof token === 'string', 'Token is required');
|
||||
invariant(typeof username === 'string', 'Username is required');
|
||||
|
||||
repoSettingsPatch.credentials = {
|
||||
password: token,
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'clone'));
|
||||
repoSettingsPatch.needsFullClone = true;
|
||||
|
||||
@@ -881,32 +862,6 @@ export const cloneGitRepoAction = async ({
|
||||
const project = await models.project.getById(projectId);
|
||||
invariant(project, 'Project not found');
|
||||
|
||||
const repoSettingsPatch: Partial<GitRepository> = {};
|
||||
repoSettingsPatch.uri = parseGitToHttpsURL(uri);
|
||||
repoSettingsPatch.author = {
|
||||
name: authorName,
|
||||
email: authorEmail,
|
||||
};
|
||||
|
||||
// Git Credentials
|
||||
if (oauth2format) {
|
||||
invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required');
|
||||
|
||||
repoSettingsPatch.credentials = {
|
||||
username,
|
||||
token,
|
||||
oauth2format,
|
||||
};
|
||||
} else {
|
||||
invariant(typeof token === 'string', 'Token is required');
|
||||
invariant(typeof username === 'string', 'Username is required');
|
||||
|
||||
repoSettingsPatch.credentials = {
|
||||
password: token,
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
||||
trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'clone'));
|
||||
repoSettingsPatch.needsFullClone = true;
|
||||
|
||||
@@ -1099,22 +1054,19 @@ export const cloneGitRepoAction = async ({
|
||||
export const updateGitRepoAction = async ({
|
||||
projectId,
|
||||
workspaceId,
|
||||
authorEmail,
|
||||
authorName,
|
||||
author,
|
||||
credentials,
|
||||
uri,
|
||||
oauth2format,
|
||||
username,
|
||||
token,
|
||||
ref,
|
||||
}: {
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
authorName: string;
|
||||
authorEmail: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
credentials: GitCredentials;
|
||||
uri: string;
|
||||
oauth2format?: string;
|
||||
username: string;
|
||||
token: string;
|
||||
ref?: string;
|
||||
}) => {
|
||||
try {
|
||||
@@ -1138,27 +1090,22 @@ export const updateGitRepoAction = async ({
|
||||
repoSettingsPatch.uri = parseGitToHttpsURL(uri);
|
||||
|
||||
// Author
|
||||
repoSettingsPatch.author = {
|
||||
name: authorName,
|
||||
email: authorEmail,
|
||||
};
|
||||
repoSettingsPatch.author = author;
|
||||
|
||||
// Git Credentials
|
||||
if (oauth2format) {
|
||||
invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required');
|
||||
|
||||
repoSettingsPatch.credentials = {
|
||||
username,
|
||||
token,
|
||||
oauth2format,
|
||||
};
|
||||
} else {
|
||||
repoSettingsPatch.credentials = {
|
||||
password: token,
|
||||
username,
|
||||
};
|
||||
// Git Credentials
|
||||
if ('oauth2format' in credentials) {
|
||||
invariant(
|
||||
credentials.oauth2format === 'gitlab' || credentials.oauth2format === 'github',
|
||||
'OAuth2 format is required',
|
||||
);
|
||||
} else if ('password' in credentials) {
|
||||
invariant(typeof credentials.password === 'string', 'Password is required');
|
||||
invariant(typeof credentials.username === 'string', 'Username is required');
|
||||
}
|
||||
|
||||
repoSettingsPatch.credentials = credentials;
|
||||
|
||||
async function setupGitRepository() {
|
||||
if (gitRepositoryId && gitRepositoryId !== 'empty') {
|
||||
const gitRepository = await models.gitRepository.getById(gitRepositoryId);
|
||||
@@ -1494,7 +1441,7 @@ export const checkoutGitBranchAction = async ({
|
||||
};
|
||||
}
|
||||
|
||||
const errorMessage = err instanceof Error ? err.message : err;
|
||||
const errorMessage = err instanceof Error ? err.message : err.toString();
|
||||
|
||||
return {
|
||||
errors: [errorMessage],
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface RendererToMainBridgeAPI {
|
||||
updateLatestStepName: (options: { requestId: string; stepName: string }) => void;
|
||||
extractJsonFileFromPostmanDataDumpArchive: (archivePath: string) => Promise<any>;
|
||||
}
|
||||
|
||||
export function registerMainHandlers() {
|
||||
ipcMainOn('addExecutionStep', (_, options: { requestId: string; stepName: string }) => {
|
||||
addExecutionStep(options.requestId, options.stepName);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from 'node:fs';
|
||||
import * as os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import {
|
||||
app,
|
||||
@@ -82,6 +81,7 @@ export async function createHiddenBrowserWindow() {
|
||||
// if window crashed
|
||||
const windowWasClosedUnexpectedly = hiddenWindowIsBusy && !isRunning;
|
||||
if (windowWasClosedUnexpectedly) {
|
||||
console.log('[main] hidden window was closed unexpectedly');
|
||||
hiddenWindowIsBusy = false;
|
||||
}
|
||||
|
||||
@@ -94,11 +94,12 @@ export async function createHiddenBrowserWindow() {
|
||||
// if window froze
|
||||
const isRunningButUnhealthy = isRunning && !isHealthy;
|
||||
if (isRunningButUnhealthy) {
|
||||
console.log('[main] hidden window is busy, stopping it');
|
||||
// stop and wait for window close event and sync the map and busy status
|
||||
await stopAndWaitForHiddenBrowserWindow(runningHiddenBrowserWindow);
|
||||
}
|
||||
|
||||
console.log('[main] hidden window is down, restarting');
|
||||
console.log('[main] hidden window is not running, starting it');
|
||||
const hiddenBrowserWindow = new BrowserWindow({
|
||||
show: false,
|
||||
title: 'HiddenBrowserWindow',
|
||||
@@ -126,9 +127,8 @@ export async function createHiddenBrowserWindow() {
|
||||
});
|
||||
|
||||
const hiddenBrowserWindowPath = path.resolve(__dirname, 'hidden-window.html');
|
||||
const hiddenBrowserWindowUrl = process.env.HIDDEN_BROWSER_WINDOW_URL || pathToFileURL(hiddenBrowserWindowPath).href;
|
||||
hiddenBrowserWindow.loadURL(hiddenBrowserWindowUrl);
|
||||
console.log(`[main] Loading ${hiddenBrowserWindowUrl}`);
|
||||
hiddenBrowserWindow.loadFile(hiddenBrowserWindowPath);
|
||||
console.log(`[main] Loading ${hiddenBrowserWindowPath}`);
|
||||
|
||||
ipcMain.removeHandler('renderer-listener-ready');
|
||||
const hiddenWinListenerReady = new Promise<void>(resolve => {
|
||||
@@ -254,8 +254,7 @@ export function createWindow(): ElectronBrowserWindow {
|
||||
});
|
||||
|
||||
// Load the html of the app.
|
||||
const appPath = path.resolve(__dirname, './index.html');
|
||||
const appUrl = process.env.APP_RENDER_URL || pathToFileURL(appPath).href;
|
||||
const appUrl = process.env.APP_RENDER_URL || 'https://insomnia-app.local';
|
||||
|
||||
console.log(`[main] Loading ${appUrl}`);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CONTENT_TYPE_GRAPHQL } from '../../common/constants';
|
||||
|
||||
@@ -144,7 +144,7 @@ export const decryptSecretValue = (encryptedValue: string, symmetricKey: JsonWeb
|
||||
return encryptedValue;
|
||||
}
|
||||
try {
|
||||
const jsonWebKey = base64decode(encryptedValue, true);
|
||||
const jsonWebKey = base64decode(encryptedValue, true) as crypt.AESMessage;
|
||||
return crypt.decryptAES(symmetricKey, jsonWebKey);
|
||||
} catch (error) {
|
||||
// return origin value if failed to decrypt
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { database as db } from '../common/database';
|
||||
import type { GitCredentials } from '../sync/git/git-vcs';
|
||||
import type { BaseModel } from './index';
|
||||
|
||||
export type OauthProviderName = 'gitlab' | 'github' | 'custom';
|
||||
@@ -78,3 +77,32 @@ export function remove(repo: GitRepository) {
|
||||
export function all() {
|
||||
return db.all<GitRepository>(type);
|
||||
}
|
||||
export interface GitAuthor {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface GitRemoteConfig {
|
||||
remote: string;
|
||||
url: string;
|
||||
}
|
||||
interface GitCredentialsBase {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
interface GitCredentialsOAuth {
|
||||
/**
|
||||
* Supported OAuth formats.
|
||||
* This is needed by isomorphic-git to be able to push/pull using an oauth2 token.
|
||||
* https://isomorphic-git.org/docs/en/authentication.html
|
||||
*/
|
||||
oauth2format?: 'github' | 'gitlab';
|
||||
username: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export type GitCredentials = GitCredentialsBase | GitCredentialsOAuth;
|
||||
|
||||
export const isGitCredentialsOAuth = (credentials: GitCredentials): credentials is GitCredentialsOAuth => {
|
||||
return 'oauth2format' in credentials;
|
||||
};
|
||||
|
||||
@@ -14,6 +14,14 @@ export interface Organization {
|
||||
branding?: Branding;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
export interface StorageRules {
|
||||
enableCloudSync: boolean;
|
||||
enableLocalVault: boolean;
|
||||
enableGitSync: boolean;
|
||||
isOverridden: boolean;
|
||||
}
|
||||
|
||||
export const SCRATCHPAD_ORGANIZATION_ID = 'org_scratchpad';
|
||||
export const isScratchpadOrganizationId = (organizationId: string) => organizationId === SCRATCHPAD_ORGANIZATION_ID;
|
||||
export const isPersonalOrganization = (organization: Organization) =>
|
||||
@@ -30,3 +38,60 @@ export const findPersonalOrganization = (organizations: Organization[], accountI
|
||||
}),
|
||||
);
|
||||
};
|
||||
export interface OrganizationsResponse {
|
||||
start: number;
|
||||
limit: number;
|
||||
length: number;
|
||||
total: number;
|
||||
next: string;
|
||||
organizations: Organization[];
|
||||
}
|
||||
|
||||
export interface UserProfileResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
picture: string;
|
||||
bio: string;
|
||||
github: string;
|
||||
linkedin: string;
|
||||
twitter: string;
|
||||
identities: any;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
}
|
||||
|
||||
export type PersonalPlanType = 'free' | 'individual' | 'team' | 'enterprise' | 'enterprise-member';
|
||||
export const formatCurrentPlanType = (type: PersonalPlanType) => {
|
||||
switch (type) {
|
||||
case 'free': {
|
||||
return 'Hobby';
|
||||
}
|
||||
case 'individual': {
|
||||
return 'Individual';
|
||||
}
|
||||
case 'team': {
|
||||
return 'Pro';
|
||||
}
|
||||
case 'enterprise': {
|
||||
return 'Enterprise';
|
||||
}
|
||||
case 'enterprise-member': {
|
||||
return 'Enterprise Member';
|
||||
}
|
||||
default: {
|
||||
return 'Free';
|
||||
}
|
||||
}
|
||||
};
|
||||
type PaymentSchedules = 'month' | 'year';
|
||||
|
||||
export interface CurrentPlan {
|
||||
isActive: boolean;
|
||||
period: PaymentSchedules;
|
||||
planId: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
type: PersonalPlanType;
|
||||
planName: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { StorageRules } from '~/models/organization';
|
||||
|
||||
import { database as db } from '../common/database';
|
||||
import { generateId } from '../common/misc';
|
||||
import type { StorageRules } from '../ui/organization-utils';
|
||||
import { type BaseModel } from './index';
|
||||
|
||||
export const name = 'Project';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { RequestTestResult } from '../../../insomnia-scripting-environment/src/objects';
|
||||
import { database as db } from '../common/database';
|
||||
import type { RunnerSource } from '../ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
|
||||
import type { BaseModel } from './index';
|
||||
|
||||
export const name = 'Runner Test Result';
|
||||
@@ -29,7 +28,7 @@ export interface ResponseInfo {
|
||||
export type RunnerResultPerRequestPerIteration = RunnerResultPerRequest[][];
|
||||
|
||||
export interface BaseRunnerTestResult {
|
||||
source: RunnerSource;
|
||||
source: 'runner';
|
||||
iterations: number;
|
||||
duration: number; // millisecond
|
||||
avgRespTime: number; // millisecond
|
||||
|
||||
@@ -108,9 +108,9 @@ export async function update(settings: Settings, patch: Partial<Settings>) {
|
||||
return updatedSettings;
|
||||
}
|
||||
|
||||
export async function patch(patch: Partial<Settings>) {
|
||||
export async function patch(settingsPatch: Partial<Settings>) {
|
||||
const settings = await getOrCreate();
|
||||
const updatedSettings = await db.docUpdate<Settings>(settings, patch);
|
||||
const updatedSettings = await db.docUpdate<Settings>(settings, settingsPatch);
|
||||
return updatedSettings;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Merge } from 'type-fest';
|
||||
|
||||
import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../common/constants';
|
||||
import { database as db } from '../common/database';
|
||||
import { strings } from '../common/strings';
|
||||
import type { BaseModel } from './index';
|
||||
@@ -169,10 +168,10 @@ export function isScratchpad(workspace?: Workspace) {
|
||||
export const scopeToActivity = (scope: WorkspaceScope) => {
|
||||
switch (scope) {
|
||||
case WorkspaceScopeKeys.collection: {
|
||||
return ACTIVITY_DEBUG;
|
||||
return 'debug';
|
||||
}
|
||||
case WorkspaceScopeKeys.design: {
|
||||
return ACTIVITY_SPEC;
|
||||
return 'spec';
|
||||
}
|
||||
case WorkspaceScopeKeys.mockServer: {
|
||||
return 'mock-server';
|
||||
@@ -181,7 +180,7 @@ export const scopeToActivity = (scope: WorkspaceScope) => {
|
||||
return 'environment';
|
||||
}
|
||||
default: {
|
||||
return ACTIVITY_DEBUG;
|
||||
return 'debug';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,7 +53,6 @@ import * as plugins from '../plugins/index';
|
||||
import { RenderError } from '../templating/render-error';
|
||||
import type { RenderedRequest, RenderPurpose } from '../templating/types';
|
||||
import { maskOrDecryptVaultDataIfNecessary } from '../templating/utils';
|
||||
import { type SendActionRuntime } from '../ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
|
||||
import { invariant } from '../utils/invariant';
|
||||
import { serializeNDJSON } from '../utils/ndjson';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString, smartEncodeUrl } from '../utils/url/querystring';
|
||||
@@ -63,6 +62,10 @@ import { filterClientCertificates } from './certificate';
|
||||
import { runScriptConcurrently, type TransformedExecuteScriptContext } from './concurrency';
|
||||
import { addSetCookiesToToughCookieJar } from './set-cookie-util';
|
||||
|
||||
export interface SendActionRuntime {
|
||||
appendTimeline: (timelinePath: string, logs: string[]) => Promise<void>;
|
||||
}
|
||||
|
||||
export const getOrInheritAuthentication = ({
|
||||
request,
|
||||
requestGroups,
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as plugin from '../app';
|
||||
describe('init()', () => {
|
||||
it('initializes correctly', async () => {
|
||||
const result = plugin.init();
|
||||
expect(Object.keys(result)).toEqual(['app', '__private']);
|
||||
expect(Object.keys(result)).toEqual(['app']);
|
||||
expect(Object.keys(result.app).sort()).toEqual(
|
||||
['alert', 'clipboard', 'dialog', 'getPath', 'getInfo', 'prompt', 'showSaveDialog'].sort(),
|
||||
);
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import { getAppPlatform, getAppVersion } from 'insomnia/src/common/constants';
|
||||
import type { AppContext, RenderPurpose } from 'insomnia/src/templating/types';
|
||||
import { invariant } from 'insomnia/src/utils/invariant';
|
||||
import type React from 'react';
|
||||
import type ReactDOM from 'react-dom';
|
||||
|
||||
export interface PrivateProperties {
|
||||
loadRendererModules: () => Promise<
|
||||
| {
|
||||
ReactDOM: typeof ReactDOM;
|
||||
React: typeof React;
|
||||
}
|
||||
| {}
|
||||
>;
|
||||
}
|
||||
// TODO: consider how this would work in a webworker context
|
||||
const isRenderer = process.type === 'renderer';
|
||||
|
||||
export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContext; __private: PrivateProperties } => ({
|
||||
export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContext } => ({
|
||||
app: {
|
||||
alert: (title: string, message?: string) => {
|
||||
if (isRenderer) {
|
||||
@@ -32,9 +21,9 @@ export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContex
|
||||
});
|
||||
}
|
||||
},
|
||||
prompt: (title, options = {}) => {
|
||||
prompt: (title, options) => {
|
||||
if (!isRenderer) {
|
||||
return Promise.resolve(options.defaultValue || '');
|
||||
return Promise.resolve(options?.defaultValue || '');
|
||||
}
|
||||
// This custom promise converts the prompt modal from being callback-based to reject when the modal is cancelled and resolve when the modal is submitted and hidden
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
@@ -78,20 +67,4 @@ export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContex
|
||||
clear: () => window.clipboard.clear(),
|
||||
},
|
||||
},
|
||||
__private: {
|
||||
// Provide modules that can be used in the renderer process
|
||||
async loadRendererModules() {
|
||||
if (globalThis.document === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ReactDOM = await import('react-dom');
|
||||
const React = await import('react');
|
||||
|
||||
return {
|
||||
ReactDOM,
|
||||
React,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
556
packages/insomnia/src/root.tsx
Normal file
556
packages/insomnia/src/root.tsx
Normal file
@@ -0,0 +1,556 @@
|
||||
import '~/ui/css/styles.css';
|
||||
|
||||
import type { IpcRendererEvent } from 'electron';
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import {
|
||||
href,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
useNavigate,
|
||||
useParams,
|
||||
useRouteLoaderData,
|
||||
} from 'react-router';
|
||||
import { isRouteErrorResponse, useNavigation } from 'react-router';
|
||||
|
||||
import { EXTERNAL_VAULT_PLUGIN_NAME, isDevelopment } from '~/common/constants';
|
||||
import * as models from '~/models';
|
||||
import type { Settings } from '~/models/settings';
|
||||
import type { UserSession } from '~/models/user-session';
|
||||
import { executePluginMainAction, reloadPlugins } from '~/plugins';
|
||||
import { createPlugin } from '~/plugins/create';
|
||||
import { setTheme } from '~/plugins/misc';
|
||||
import { useAuthorizeActionFetcher } from '~/routes/auth.authorize';
|
||||
import { useDefaultBrowserRedirectActionFetcher } from '~/routes/auth.default-browser-redirect';
|
||||
import { useLogoutFetcher } from '~/routes/auth.logout';
|
||||
import { useCreateCloudCredentialActionFetcher } from '~/routes/cloud-credentials.create';
|
||||
import { useGithubCompleteSignInFetcher } from '~/routes/git-credentials.github.complete-sign-in';
|
||||
import { useGitLabCompleteSignInFetcher } from '~/routes/git-credentials.gitlab.complete-sign-in';
|
||||
import { SegmentEvent } from '~/ui/analytics';
|
||||
import { getLoginUrl } from '~/ui/auth-session-provider.client';
|
||||
import { CopyButton } from '~/ui/components/base/copy-button';
|
||||
import { Link } from '~/ui/components/base/link';
|
||||
import { ErrorBoundary as ErrorView } from '~/ui/components/error-boundary';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { showError, showModal } from '~/ui/components/modals';
|
||||
import { AlertModal } from '~/ui/components/modals/alert-modal';
|
||||
import { AskModal } from '~/ui/components/modals/ask-modal';
|
||||
import { ImportModal } from '~/ui/components/modals/import-modal/import-modal';
|
||||
import {
|
||||
SettingsModal,
|
||||
TAB_CLOUD_CREDENTIAL,
|
||||
TAB_INDEX_PLUGINS,
|
||||
TAB_INDEX_THEMES,
|
||||
} from '~/ui/components/modals/settings-modal';
|
||||
import { Toaster } from '~/ui/components/toast-notification';
|
||||
import { AppHooks } from '~/ui/containers/app-hooks';
|
||||
import { NunjucksEnabledProvider } from '~/ui/context/nunjucks/nunjucks-enabled-context';
|
||||
import { useThemeChange } from '~/ui/hooks/use-theme-change';
|
||||
import Modals from '~/ui/modals';
|
||||
|
||||
import type { Route } from './+types/root';
|
||||
|
||||
export const links: Route.LinksFunction = () => {
|
||||
return [
|
||||
{ rel: 'icon', href: '/favicon.ico' },
|
||||
{ rel: 'apple-touch-icon', href: '/apple-touch-icon.png' },
|
||||
{ rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#5bbad5' },
|
||||
];
|
||||
};
|
||||
|
||||
export const ErrorBoundary: FC<Route.ErrorBoundaryProps> = ({ error }) => {
|
||||
useThemeChange();
|
||||
const getErrorMessage = (err: any) => {
|
||||
if (isRouteErrorResponse(err)) {
|
||||
return err.data;
|
||||
}
|
||||
|
||||
if (err?.message) {
|
||||
return err?.message;
|
||||
}
|
||||
|
||||
return 'Unknown error';
|
||||
};
|
||||
|
||||
const getErrorStack = (err: any) => {
|
||||
if ('error' in err) {
|
||||
return err.error?.stack;
|
||||
}
|
||||
|
||||
return err?.stack;
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigation = useNavigation();
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const logoutFetcher = useLogoutFetcher();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2 overflow-hidden">
|
||||
<h1 className="flex items-center gap-2 text-2xl text-[--color-font]">
|
||||
<Icon className="text-[--color-danger]" icon="exclamation-triangle" /> Application Error
|
||||
</h1>
|
||||
<p className="text-[--color-font]">
|
||||
Failed to render. Please report to{' '}
|
||||
<a className="font-bold underline" href="https://github.com/Kong/insomnia/issues">
|
||||
our Github Issues
|
||||
</a>
|
||||
</p>
|
||||
<div className="p-6 text-[--color-font]">
|
||||
<code className="break-words p-2">{errorMessage}</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center justify-center gap-2 rounded-sm border border-solid border-[--hl-md] px-4 py-1 text-base font-semibold text-[--color-font] ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md] aria-pressed:bg-[--hl-sm]"
|
||||
onPress={() => navigate('/organization')}
|
||||
>
|
||||
Try to reload the app{' '}
|
||||
<span>{navigation.state === 'loading' ? <Icon icon="spinner" className="animate-spin" /> : null}</span>
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center justify-center gap-2 rounded-sm border border-solid border-[--hl-md] px-4 py-1 text-base font-semibold text-[--color-font] ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md] aria-pressed:bg-[--hl-sm]"
|
||||
onPress={() => logoutFetcher.submit()}
|
||||
>
|
||||
Logout{' '}
|
||||
<span>{logoutFetcher.state === 'loading' ? <Icon icon="spinner" className="animate-spin" /> : null}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-6 text-[--color-font]">
|
||||
<code className="break-all p-2">{getErrorStack(error)}</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface RootLoaderData {
|
||||
settings: Settings;
|
||||
workspaceCount: number;
|
||||
userSession: UserSession;
|
||||
}
|
||||
|
||||
export const useRootLoaderData = () => {
|
||||
return useRouteLoaderData<typeof clientLoader>('root');
|
||||
};
|
||||
|
||||
export async function clientLoader(_args: Route.ClientLoaderArgs) {
|
||||
const settings = await models.settings.get();
|
||||
const workspaceCount = await models.workspace.count();
|
||||
const userSession = await models.userSession.getOrCreate();
|
||||
const cloudCredentials = await models.cloudCredential.all();
|
||||
|
||||
return {
|
||||
settings,
|
||||
workspaceCount,
|
||||
userSession,
|
||||
cloudCredentials,
|
||||
};
|
||||
}
|
||||
|
||||
export const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<html lang="en" className="size-full">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
httpEquiv="Content-Security-Policy"
|
||||
content="
|
||||
font-src
|
||||
'self'
|
||||
data:
|
||||
;
|
||||
connect-src
|
||||
'self'
|
||||
data:
|
||||
insomnia-event-source:
|
||||
insomnia-templating-worker-database:
|
||||
https:
|
||||
http:
|
||||
;
|
||||
default-src
|
||||
*
|
||||
insomnia://*
|
||||
;
|
||||
img-src
|
||||
blob:
|
||||
data:
|
||||
*
|
||||
insomnia://*
|
||||
;
|
||||
script-src
|
||||
'self'
|
||||
'unsafe-eval'
|
||||
'unsafe-inline'
|
||||
;
|
||||
style-src
|
||||
'self'
|
||||
'unsafe-inline'
|
||||
;
|
||||
media-src
|
||||
blob:
|
||||
data:
|
||||
mediastream:
|
||||
*
|
||||
insomnia://*
|
||||
;
|
||||
"
|
||||
/>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="size-full">
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<div id="graphql-explorer-container" />
|
||||
<div id="hints-container" className="theme--dropdown__menu" />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export const HydrateFallback = () => {
|
||||
return (
|
||||
<div id="app-loading-indicator" className="fixed left-0 top-0 flex h-full w-full items-center justify-center">
|
||||
<div className="relative">
|
||||
<svg viewBox="0 0 378 378" xmlns="http://www.w3.org/2000/svg" fillRule="evenodd" clipRule="evenodd" width="100">
|
||||
<circle
|
||||
cx="36"
|
||||
cy="36"
|
||||
r="36"
|
||||
fill="none"
|
||||
stroke="var(--hl, rgb(130, 130, 130))"
|
||||
strokeOpacity="0.1"
|
||||
strokeWidth="4px"
|
||||
transform="translate(-323 -111) translate(359.016 147.016) scale(4.24956)"
|
||||
/>
|
||||
<circle
|
||||
cx="36"
|
||||
cy="36"
|
||||
r="36"
|
||||
fill="none"
|
||||
stroke="var(--hl, rgb(130, 130, 130))"
|
||||
strokeOpacity="0.8"
|
||||
strokeWidth="4px"
|
||||
strokeDasharray="56,172,0,0"
|
||||
transform="translate(-323 -111) translate(359.016 147.016) scale(4.24956)"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 36 36"
|
||||
to="360 36 36"
|
||||
dur="0.8s"
|
||||
repeatCount="indefinite"
|
||||
additive="sum"
|
||||
/>
|
||||
</circle>
|
||||
<path
|
||||
d="M19 37.033c9.96 0 18.033-8.073 18.033-18.033S28.96.967 19 .967.967 9.04.967 19 9.04 37.033 19 37.033z"
|
||||
fill="#fff"
|
||||
fillRule="nonzero"
|
||||
transform="translate(-323 -111) translate(431.258 219.258) scale(4.24956)"
|
||||
/>
|
||||
<path
|
||||
d="M19 0C8.506 0 0 8.506 0 19s8.506 19 19 19 19-8.506 19-19S29.494 0 19 0zm0 1.932c9.426 0 17.068 7.642 17.068 17.068 0 9.426-7.642 17.068-17.068 17.068-9.426 0-17.068-7.642-17.068-17.068C1.932 9.574 9.574 1.932 19 1.932z"
|
||||
fill="#4000bf"
|
||||
fillRule="nonzero"
|
||||
transform="translate(-323 -111) translate(431.258 219.258) scale(4.24956)"
|
||||
/>
|
||||
<path
|
||||
d="M19.214 5.474c7.47 0 13.525 6.057 13.525 13.526 0 7.469-6.055 13.526-13.525 13.526-7.47 0-13.526-6.057-13.526-13.526 0-1.825.362-3.567 1.019-5.156a5.266 5.266 0 004.243 2.15c2.885 0 5.26-2.375 5.26-5.261a5.263 5.263 0 00-2.15-4.242 13.5 13.5 0 015.154-1.017z"
|
||||
fill="url(#_Linear1)"
|
||||
transform="translate(-323 -111) translate(431.258 219.258) scale(4.24956)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="_Linear1"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="1"
|
||||
y2="0"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="rotate(-90 25.87 6.655) scale(27.0508)"
|
||||
>
|
||||
<stop offset="0" stopColor="#7400e1" />
|
||||
<stop offset="1" stopColor="#4000bf" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Root = () => {
|
||||
const { organizationId, projectId } = useParams() as {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
const [importUri, setImportUri] = useState('');
|
||||
const { submit: createCloudCredentials } = useCreateCloudCredentialActionFetcher();
|
||||
const { submit: authorizeSubmit } = useAuthorizeActionFetcher();
|
||||
const { submit: logoutSubmit } = useLogoutFetcher();
|
||||
const { submit: githubCompleteSignInSubmit } = useGithubCompleteSignInFetcher();
|
||||
const { submit: gitLabCompleteSignInSubmit } = useGitLabCompleteSignInFetcher();
|
||||
const { submit: redirectToDefaultBrowserSubmit } = useDefaultBrowserRedirectActionFetcher();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
return window.main.on('shell:open', async (_: IpcRendererEvent, url: string) => {
|
||||
// Get the url without params
|
||||
let parsedUrl;
|
||||
try {
|
||||
parsedUrl = new URL(url);
|
||||
} catch {
|
||||
console.log('[deep-link] Invalid args, expected insomnia://x/y/z', url);
|
||||
return;
|
||||
}
|
||||
let urlWithoutParams = url.slice(0, Math.max(0, url.indexOf('?'))) || url;
|
||||
const params = Object.fromEntries(parsedUrl.searchParams);
|
||||
// Change protocol for dev redirects to match switch case
|
||||
if (isDevelopment()) {
|
||||
urlWithoutParams = urlWithoutParams.replace('insomniadev://', 'insomnia://');
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://app/alert') {
|
||||
return showModal(AlertModal, {
|
||||
title: params.title,
|
||||
message: params.message,
|
||||
});
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://app/auth/login') {
|
||||
if (params.message) {
|
||||
window.localStorage.setItem('logoutMessage', params.message);
|
||||
}
|
||||
|
||||
return logoutSubmit();
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://app/import') {
|
||||
window.main.trackSegmentEvent({
|
||||
event: SegmentEvent.importStarted,
|
||||
properties: {
|
||||
source: 'import-url',
|
||||
},
|
||||
});
|
||||
|
||||
return setImportUri(params.uri);
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://plugins/install') {
|
||||
if (!params.name || params.name.trim() === '') {
|
||||
return showError({
|
||||
title: 'Plugin Install',
|
||||
message: 'Plugin name is required',
|
||||
});
|
||||
}
|
||||
|
||||
return showModal(AskModal, {
|
||||
title: 'Plugin Install',
|
||||
message: (
|
||||
<p className="text-[--hl]">
|
||||
Do you want to install <i className="font-bold text-[--hl]">{params.name}</i>?
|
||||
</p>
|
||||
),
|
||||
yesText: 'Install',
|
||||
noText: 'Cancel',
|
||||
onDone: async (isYes: boolean) => {
|
||||
if (isYes) {
|
||||
try {
|
||||
// TODO (pavkout): Remove second parameter when we will decide about the @scoped packages name validation
|
||||
await window.main.installPlugin(params.name.trim(), true);
|
||||
showModal(SettingsModal, { tab: TAB_INDEX_PLUGINS });
|
||||
} catch (err) {
|
||||
showError({
|
||||
title: 'Plugin Install',
|
||||
message: 'Failed to install plugin',
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://plugins/theme') {
|
||||
const parsedTheme = JSON.parse(decodeURIComponent(params.theme));
|
||||
showModal(AskModal, {
|
||||
title: 'Install Theme',
|
||||
message: (
|
||||
<>
|
||||
Do you want to install <code>{parsedTheme.displayName}</code>?
|
||||
</>
|
||||
),
|
||||
yesText: 'Install',
|
||||
noText: 'Cancel',
|
||||
onDone: async (isYes: boolean) => {
|
||||
if (isYes) {
|
||||
const mainJsContent = `module.exports.themes = [${JSON.stringify(parsedTheme, null, 2)}];`;
|
||||
await createPlugin(`theme-${parsedTheme.name}`, mainJsContent);
|
||||
const settings = await models.settings.get();
|
||||
await models.settings.update(settings, {
|
||||
theme: parsedTheme.name,
|
||||
});
|
||||
await reloadPlugins();
|
||||
await setTheme(parsedTheme.name);
|
||||
showModal(SettingsModal, { tab: TAB_INDEX_THEMES });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
urlWithoutParams === 'insomnia://oauth/github/authenticate' ||
|
||||
urlWithoutParams === 'insomnia://oauth/github-app/authenticate'
|
||||
) {
|
||||
const { code, state } = params;
|
||||
return githubCompleteSignInSubmit({
|
||||
code,
|
||||
state,
|
||||
});
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://oauth/gitlab/authenticate') {
|
||||
const { code, state } = params;
|
||||
return gitLabCompleteSignInSubmit({
|
||||
code,
|
||||
state,
|
||||
});
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://app/auth/finish') {
|
||||
return authorizeSubmit({
|
||||
code: params.box,
|
||||
});
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://app/open/organization') {
|
||||
// if user is logged out, navigate to authorize instead
|
||||
// gracefully handle open org in app from browser
|
||||
const userSession = await models.userSession.getOrCreate();
|
||||
if (!userSession.id || userSession.id === '') {
|
||||
const url = new URL(getLoginUrl());
|
||||
window.main.openInBrowser(url.toString());
|
||||
window.localStorage.setItem('specificOrgRedirectAfterAuthorize', params.organizationId);
|
||||
return navigate(href('/auth/authorize'));
|
||||
}
|
||||
return navigate(`/organization/${params.organizationId}`);
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://system-browser-oauth/redirect') {
|
||||
const { url: redirectUrl } = params;
|
||||
return redirectToDefaultBrowserSubmit({
|
||||
redirectUrl,
|
||||
});
|
||||
}
|
||||
if (urlWithoutParams === 'insomnia://oauth/azure/authenticate') {
|
||||
const { code, ...restParams } = params;
|
||||
if (code && typeof code === 'string') {
|
||||
const authResult = await executePluginMainAction({
|
||||
pluginName: EXTERNAL_VAULT_PLUGIN_NAME,
|
||||
actionName: 'exchangeCode',
|
||||
params: { provider: 'azure', code },
|
||||
});
|
||||
const { success, result, error } = authResult;
|
||||
if (success) {
|
||||
const { account, uniqueId } = result!;
|
||||
const name = account?.username || uniqueId;
|
||||
createCloudCredentials({
|
||||
name,
|
||||
credentials: result,
|
||||
provider: 'azure',
|
||||
isAuthenticated: true,
|
||||
});
|
||||
const closeModalBtn = document.getElementById('close-add-cloud-credential-modal');
|
||||
if (closeModalBtn) {
|
||||
// close the modal to hint user Azure oauth url if exists
|
||||
closeModalBtn.click();
|
||||
}
|
||||
showModal(SettingsModal, { tab: TAB_CLOUD_CREDENTIAL });
|
||||
} else {
|
||||
showError({
|
||||
title: 'Azure Authorization Failed',
|
||||
message: error?.errorMessage,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const errorDetailKeys = Object.keys(restParams);
|
||||
const { error, error_description, error_uri } = restParams;
|
||||
if (error && error_description) {
|
||||
showError({
|
||||
title: 'Azure Authorization Failed',
|
||||
message: (
|
||||
<div className="flex flex-col gap-1 text-left">
|
||||
<span className="text-lg font-bold">{error}</span>
|
||||
<span className="whitespace-normal">{error_description}</span>
|
||||
{error_uri && (
|
||||
<div className="mt-2 flex items-center justify-center">
|
||||
<Link button className="btn btn--clicky w-80" href={error_uri}>
|
||||
View Document <i className="fa fa-external-link" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<CopyButton
|
||||
size="small"
|
||||
className="absolute right-[--padding-sm] top-[--padding-sm]"
|
||||
content={error_description}
|
||||
title="Copy Description"
|
||||
style={{ borderWidth: 0 }}
|
||||
>
|
||||
<i className="fa fa-copy" />
|
||||
</CopyButton>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
showError({
|
||||
title: 'Azure Authorization Failed',
|
||||
message: (
|
||||
<div className="flex flex-col gap-1 text-left">
|
||||
{errorDetailKeys.length > 0
|
||||
? errorDetailKeys.map(k => (
|
||||
<span key={k} className="whitespace-normal">
|
||||
{k}: {restParams[k]}
|
||||
</span>
|
||||
))
|
||||
: 'Unknown error'}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Unknown deep link: ${url}`);
|
||||
});
|
||||
}, [
|
||||
authorizeSubmit,
|
||||
createCloudCredentials,
|
||||
gitLabCompleteSignInSubmit,
|
||||
githubCompleteSignInSubmit,
|
||||
logoutSubmit,
|
||||
navigate,
|
||||
redirectToDefaultBrowserSubmit,
|
||||
]);
|
||||
|
||||
return (
|
||||
<NunjucksEnabledProvider>
|
||||
<ErrorView>
|
||||
<div className="app">
|
||||
<Outlet />
|
||||
<Toaster />
|
||||
</div>
|
||||
<Modals />
|
||||
<AppHooks />
|
||||
{/* triggered by insomnia://app/import */}
|
||||
{importUri && (
|
||||
<ImportModal
|
||||
onHide={() => setImportUri('')}
|
||||
projectName="Insomnia"
|
||||
defaultProjectId={projectId}
|
||||
organizationId={organizationId}
|
||||
from={{ type: 'uri', defaultValue: importUri }}
|
||||
/>
|
||||
)}
|
||||
</ErrorView>
|
||||
</NunjucksEnabledProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Root;
|
||||
3
packages/insomnia/src/routes.ts
Normal file
3
packages/insomnia/src/routes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { flatRoutes } from '@react-router/fs-routes';
|
||||
|
||||
export default flatRoutes();
|
||||
5
packages/insomnia/src/routes/_index.tsx
Normal file
5
packages/insomnia/src/routes/_index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from 'react-router';
|
||||
|
||||
export async function clientLoader() {
|
||||
return redirect('/organization');
|
||||
}
|
||||
@@ -1,26 +1,29 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Fragment, useCallback } from 'react';
|
||||
import { Button, Heading } from 'react-aria-components';
|
||||
import { type ActionFunction, redirect, useFetcher, useFetchers, useNavigate } from 'react-router';
|
||||
import { href, redirect, useFetcher, useFetchers, useNavigate } from 'react-router';
|
||||
|
||||
import { userSession as sessionModel } from '../../models';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { getVaultKeyFromStorage } from '../../utils/vault';
|
||||
import { SegmentEvent } from '../analytics';
|
||||
import { getLoginUrl, submitAuthCode } from '../auth-session-provider';
|
||||
import { Icon } from '../components/icon';
|
||||
import { insomniaFetch } from '../insomniaFetch';
|
||||
import { validateVaultKey } from '../vault-key';
|
||||
import { userSession as sessionModel } from '~/models';
|
||||
import { SegmentEvent } from '~/ui/analytics';
|
||||
import { getLoginUrl, submitAuthCode } from '~/ui/auth-session-provider.client';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { insomniaFetch } from '~/ui/insomniaFetch';
|
||||
import { validateVaultKey } from '~/ui/vault-key.client';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
import { getVaultKeyFromStorage } from '~/utils/vault';
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
import type { Route } from './+types/auth.authorize';
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = await request.json();
|
||||
|
||||
invariant(typeof data?.code === 'string', 'Expected code to be a string');
|
||||
const error = await submitAuthCode(data.code);
|
||||
if (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
||||
const humanReadableError =
|
||||
error?.message === 'Failed to fetch'
|
||||
errorMessage === 'Failed to fetch'
|
||||
? 'Network failed, please try again. If the problem persists, check your network and proxy settings.'
|
||||
: error?.message;
|
||||
: errorMessage;
|
||||
return {
|
||||
errors: {
|
||||
message: humanReadableError,
|
||||
@@ -64,19 +67,39 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
}
|
||||
|
||||
return redirect('/organization');
|
||||
};
|
||||
}
|
||||
|
||||
const Authorize = () => {
|
||||
export function useAuthorizeActionFetcher(args: { key?: string } = {}) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: { code: string }) => {
|
||||
fetcherSubmit(data, {
|
||||
action: href('/auth/authorize'),
|
||||
method: 'POST',
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
|
||||
const Component = () => {
|
||||
const url = getLoginUrl();
|
||||
const copyUrl = () => {
|
||||
window.clipboard.writeText(url);
|
||||
};
|
||||
|
||||
const authorizeFetcher = useFetcher();
|
||||
const authorizeFetcher = useAuthorizeActionFetcher();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const allFetchers = useFetchers();
|
||||
const authFetchers = allFetchers.filter(f => f.formAction === '/auth/authorize');
|
||||
const authFetchers = allFetchers.filter(f => f.formAction === href('/auth/authorize'));
|
||||
|
||||
const isAuthenticating = authFetchers.some(f => f.state !== 'idle');
|
||||
// 1 first time sign up
|
||||
@@ -124,15 +147,9 @@ const Authorize = () => {
|
||||
|
||||
const code = data.get('code');
|
||||
invariant(typeof code === 'string', 'Expected code to be a string');
|
||||
authorizeFetcher.submit(
|
||||
{
|
||||
code,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
encType: 'application/json',
|
||||
},
|
||||
);
|
||||
authorizeFetcher.submit({
|
||||
code,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="form-control form-control--outlined no-pad-top" style={{ display: 'flex' }}>
|
||||
@@ -163,7 +180,7 @@ const Authorize = () => {
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onPress={() => {
|
||||
navigate('/auth/login');
|
||||
navigate(href('/auth/login'));
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrow-left" />
|
||||
@@ -174,4 +191,4 @@ const Authorize = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Authorize;
|
||||
export default Component;
|
||||
@@ -1,12 +1,15 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import type { ActionFunctionArgs } from 'react-router';
|
||||
import electron from 'electron';
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { userSession as sessionModel } from '../../models';
|
||||
import { removeAllSecrets } from '../../models/environment';
|
||||
import type { ToastNotification } from '../components/toast';
|
||||
import { insomniaFetch } from '../insomniaFetch';
|
||||
import { userSession as sessionModel } from '~/models';
|
||||
import { removeAllSecrets } from '~/models/environment';
|
||||
import type { ToastNotification } from '~/ui/components/toast';
|
||||
import { insomniaFetch } from '~/ui/insomniaFetch';
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
import type { Route } from './+types/auth.clear-vault-key';
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const { organizations = [], sessionId: resetVaultClientSessionId } = await request.json();
|
||||
|
||||
const userSession = await sessionModel.getOrCreate();
|
||||
@@ -33,8 +36,27 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
key: 'Vault key reset',
|
||||
message: 'Your vault key has been reset, all you local secrets have been deleted.',
|
||||
};
|
||||
ipcRenderer.emit('show-notification', null, notification);
|
||||
electron.ipcRenderer.emit('show-notification', null, notification);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function useClearVaultKeyFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: { organizations: string[]; sessionId: string }) => {
|
||||
fetcherSubmit(data, {
|
||||
action: href('/auth/clear-vault-key'),
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
23
packages/insomnia/src/routes/auth.create-vault-key.tsx
Normal file
23
packages/insomnia/src/routes/auth.create-vault-key.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { createVaultKey } from '~/ui/vault-key.client';
|
||||
|
||||
import type { Route } from './+types/auth.create-vault-key';
|
||||
|
||||
export async function clientAction(_args: Route.ClientActionArgs) {
|
||||
return createVaultKey('create');
|
||||
}
|
||||
|
||||
export function useCreateVaultKeyFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
fetcherSubmit({}, { action: href('/auth/create-vault-key'), method: 'POST' });
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/auth.default-browser-redirect';
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const { redirectUrl } = (await request.json()) as { redirectUrl: string };
|
||||
window.main.onDefaultBrowserOAuthRedirect({
|
||||
url: redirectUrl,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useDefaultBrowserRedirectActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const fetcher = useFetcher<typeof clientAction>(args);
|
||||
function submit(data: { redirectUrl: string }) {
|
||||
return fetcher.submit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/auth/default-browser-redirect'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...fetcher,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Button } from 'react-aria-components';
|
||||
import { type ActionFunction, redirect, useFetcher, useNavigate } from 'react-router';
|
||||
import { href, redirect, useFetcher, useNavigate } from 'react-router';
|
||||
|
||||
import { SegmentEvent } from '../analytics';
|
||||
import { getLoginUrl } from '../auth-session-provider';
|
||||
import { Icon } from '../components/icon';
|
||||
import { SCRATCHPAD_ORGANIZATION_ID } from '~/models/organization';
|
||||
import { SCRATCHPAD_PROJECT_ID } from '~/models/project';
|
||||
import { SCRATCHPAD_WORKSPACE_ID } from '~/models/workspace';
|
||||
import { SegmentEvent } from '~/ui/analytics';
|
||||
import { getLoginUrl } from '~/ui/auth-session-provider.client';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
|
||||
import type { Route } from './+types/auth.login';
|
||||
|
||||
const GoogleIcon = (props: React.ReactSVGElement['props']) => {
|
||||
return (
|
||||
@@ -29,7 +34,7 @@ const GoogleIcon = (props: React.ReactSVGElement['props']) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = await request.formData();
|
||||
const provider = data.get('provider');
|
||||
const url = new URL(getLoginUrl());
|
||||
@@ -40,24 +45,36 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
|
||||
window.main.openInBrowser(url.toString());
|
||||
|
||||
return redirect('/auth/authorize');
|
||||
};
|
||||
return redirect(href('/auth/authorize'));
|
||||
}
|
||||
|
||||
const Login = () => {
|
||||
const loginFetcher = useFetcher();
|
||||
export function useLoginActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
const submit = useCallback(
|
||||
(data: { provider: string }) => {
|
||||
fetcherSubmit(data, {
|
||||
action: href('/auth/login'),
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
|
||||
const Component = () => {
|
||||
const loginFetcher = useLoginActionFetcher();
|
||||
const navigate = useNavigate();
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const login = (provider: string) => {
|
||||
loginFetcher.submit(
|
||||
{
|
||||
provider,
|
||||
},
|
||||
{
|
||||
action: '/auth/login',
|
||||
method: 'POST',
|
||||
},
|
||||
);
|
||||
loginFetcher.submit({
|
||||
provider,
|
||||
});
|
||||
};
|
||||
|
||||
const logoutMessage = window.localStorage.getItem('logoutMessage');
|
||||
@@ -161,7 +178,13 @@ const Login = () => {
|
||||
window.main.trackSegmentEvent({
|
||||
event: SegmentEvent.selectScratchpad,
|
||||
});
|
||||
navigate('/organization/org_scratchpad/project/proj_scratchpad/workspace/wrk_scratchpad/debug');
|
||||
navigate(
|
||||
href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', {
|
||||
organizationId: SCRATCHPAD_ORGANIZATION_ID,
|
||||
projectId: SCRATCHPAD_PROJECT_ID,
|
||||
workspaceId: SCRATCHPAD_WORKSPACE_ID,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
aria-label="Use the Scratch Pad"
|
||||
className="flex justify-center gap-[--padding-xs] text-sm text-[rgba(var(--color-font-rgb),0.8)] outline-none transition-colors hover:text-[--color-font] focus:text-[--color-font]"
|
||||
@@ -176,4 +199,4 @@ const Login = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
export default Component;
|
||||
24
packages/insomnia/src/routes/auth.logout.tsx
Normal file
24
packages/insomnia/src/routes/auth.logout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, redirect, useFetcher } from 'react-router';
|
||||
|
||||
import { logout } from '~/account/session';
|
||||
|
||||
import type { Route } from './+types/auth.logout';
|
||||
|
||||
export async function clientAction(_args: Route.ClientActionArgs) {
|
||||
await logout();
|
||||
return redirect(href('/auth/login'));
|
||||
}
|
||||
|
||||
export function useLogoutFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
return fetcherSubmit({}, { action: href('/auth/logout'), method: 'POST' });
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
21
packages/insomnia/src/routes/auth.reset-vault-key.tsx
Normal file
21
packages/insomnia/src/routes/auth.reset-vault-key.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useCallback } from 'react';
|
||||
import { type ActionFunctionArgs, href, useFetcher } from 'react-router';
|
||||
|
||||
import { createVaultKey } from '~/ui/vault-key.client';
|
||||
|
||||
export async function clientAction(_args: ActionFunctionArgs) {
|
||||
return createVaultKey('reset');
|
||||
}
|
||||
|
||||
export function useResetVaultKeyFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
fetcherSubmit({}, { action: href('/auth/reset-vault-key'), method: 'POST' });
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Link, Tooltip, TooltipTrigger } from 'react-aria-components';
|
||||
import { Outlet } from 'react-router';
|
||||
|
||||
import { Hotkey } from '../components/hotkey';
|
||||
import { Icon } from '../components/icon';
|
||||
import { InsomniaLogo } from '../components/insomnia-icon';
|
||||
import { showSettingsModal } from '../components/modals/settings-modal';
|
||||
import { TrailLinesContainer } from '../components/trail-lines-container';
|
||||
import { useRootLoaderData } from './root';
|
||||
import { useRootLoaderData } from '~/root';
|
||||
import { Hotkey } from '~/ui/components/hotkey';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { InsomniaLogo } from '~/ui/components/insomnia-icon';
|
||||
import { showSettingsModal } from '~/ui/components/modals/settings-modal';
|
||||
import { TrailLinesContainer } from '~/ui/components/trail-lines-container';
|
||||
|
||||
const Auth = () => {
|
||||
const { userSession, settings } = useRootLoaderData();
|
||||
const Component = () => {
|
||||
const { userSession, settings } = useRootLoaderData()!;
|
||||
const [status, setStatus] = useState<'online' | 'offline'>('online');
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setStatus('online');
|
||||
@@ -118,4 +118,4 @@ const Auth = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
export default Component;
|
||||
35
packages/insomnia/src/routes/auth.update-vault-salt.tsx
Normal file
35
packages/insomnia/src/routes/auth.update-vault-salt.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react';
|
||||
import { type ActionFunctionArgs, href, useFetcher } from 'react-router';
|
||||
|
||||
import { userSession as sessionModel } from '~/models';
|
||||
import { insomniaFetch } from '~/ui/insomniaFetch';
|
||||
|
||||
export async function clientAction(_args: ActionFunctionArgs) {
|
||||
const userSession = await sessionModel.getOrCreate();
|
||||
const { id: sessionId } = userSession;
|
||||
const { salt: vaultSalt } = await insomniaFetch<{
|
||||
salt?: string;
|
||||
error?: string;
|
||||
}>({
|
||||
method: 'GET',
|
||||
path: '/v1/user/vault',
|
||||
sessionId,
|
||||
});
|
||||
if (vaultSalt) {
|
||||
await sessionModel.update(userSession, { vaultSalt });
|
||||
}
|
||||
return vaultSalt;
|
||||
}
|
||||
|
||||
export function useUpdateVaultSaltFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
return fetcherSubmit({}, { action: href('/auth/update-vault-salt'), method: 'POST' });
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
52
packages/insomnia/src/routes/auth.validate-vault-key.tsx
Normal file
52
packages/insomnia/src/routes/auth.validate-vault-key.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useCallback } from 'react';
|
||||
import { type ActionFunctionArgs, href, useFetcher } from 'react-router';
|
||||
|
||||
import { userSession as sessionModel } from '~/models';
|
||||
import { saveVaultKey, validateVaultKey } from '~/ui/vault-key.client';
|
||||
|
||||
export async function clientAction({ request }: ActionFunctionArgs) {
|
||||
const { vaultKey, saveVaultKey: saveVaultKeyLocally = false } = await request.json();
|
||||
const userSession = await sessionModel.getOrCreate();
|
||||
const { vaultSalt, accountId } = userSession;
|
||||
|
||||
if (!vaultSalt) {
|
||||
return { error: 'Please generate a vault key from preference first' };
|
||||
}
|
||||
|
||||
try {
|
||||
const validateResult = await validateVaultKey(userSession, vaultKey, vaultSalt);
|
||||
if (!validateResult) {
|
||||
return { error: 'Invalid vault key, please check and input again' };
|
||||
}
|
||||
if (saveVaultKeyLocally) {
|
||||
await saveVaultKey(accountId, vaultKey);
|
||||
}
|
||||
return { vaultKey, srpK: validateResult };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
|
||||
|
||||
return { error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
export function useValidateVaultKeyActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
({ vaultKey, saveVaultKey = false }: { vaultKey: string; saveVaultKey?: boolean }) => {
|
||||
const url = href('/auth/validate-vault-key');
|
||||
|
||||
return fetcherSubmit(JSON.stringify({ vaultKey, saveVaultKey }), {
|
||||
action: url,
|
||||
method: 'POST',
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import * as models from '~/models';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import type { Route } from './+types/cloud-credentials.$cloudCredentialId.delete';
|
||||
|
||||
export async function clientAction({ params }: Route.ClientActionArgs) {
|
||||
const { cloudCredentialId } = params;
|
||||
invariant(typeof cloudCredentialId === 'string', 'Cloud Credential ID is required');
|
||||
const cloudCredential = await models.cloudCredential.getById(cloudCredentialId);
|
||||
invariant(cloudCredential, 'Cloud Credential not found');
|
||||
await models.cloudCredential.remove(cloudCredential);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useDeleteCloudCredentialActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcher } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
function submit({ cloudCredentialId }: { cloudCredentialId: string }) {
|
||||
return fetcherSubmit(
|
||||
{},
|
||||
{
|
||||
method: 'POST',
|
||||
action: href('/cloud-credentials/:cloudCredentialId/delete', {
|
||||
cloudCredentialId,
|
||||
}),
|
||||
encType: 'application/json',
|
||||
},
|
||||
);
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcher,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { EXTERNAL_VAULT_PLUGIN_NAME } from '~/common/constants';
|
||||
import * as models from '~/models';
|
||||
import type { BaseCloudCredential } from '~/models/cloud-credential';
|
||||
import { executePluginMainAction } from '~/plugins';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import type { Route } from './+types/cloud-credentials.$cloudCredentialId.update';
|
||||
|
||||
export async function clientAction({ params, request }: Route.ClientActionArgs) {
|
||||
const { cloudCredentialId } = params;
|
||||
invariant(typeof cloudCredentialId === 'string', 'Credential ID is required');
|
||||
const patch = (await request.json()) as BaseCloudCredential;
|
||||
const { name, provider, credentials } = patch;
|
||||
invariant(name && typeof name === 'string', 'Name is required');
|
||||
invariant(provider, 'Cloud Provider name is required');
|
||||
invariant(credentials, 'Credentials are required');
|
||||
const authenticateResponse = await executePluginMainAction({
|
||||
pluginName: EXTERNAL_VAULT_PLUGIN_NAME,
|
||||
actionName: 'authenticate',
|
||||
params: { provider, credentials },
|
||||
});
|
||||
const { success, error, result } = authenticateResponse;
|
||||
if (error) {
|
||||
return {
|
||||
error: `${error.errorMessage}`,
|
||||
};
|
||||
}
|
||||
if (success) {
|
||||
const originCredential = await models.cloudCredential.getById(cloudCredentialId);
|
||||
invariant(originCredential, 'No Cloud Credential found');
|
||||
if (provider === 'hashicorp') {
|
||||
// update access token and expires_at
|
||||
const { access_token, expires_at } = result as { access_token: string; expires_at: number };
|
||||
patch.credentials['access_token'] = access_token;
|
||||
patch.credentials['expires_at'] = expires_at;
|
||||
}
|
||||
await models.cloudCredential.update(originCredential, patch);
|
||||
return result as { access_token: string; expires_at: number };
|
||||
}
|
||||
return { error: 'Unexpected response from ' + provider };
|
||||
}
|
||||
|
||||
export function useUpdateCloudCredentialActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcher } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
function submit({ cloudCredentialId, patch }: { cloudCredentialId: string; patch: BaseCloudCredential }) {
|
||||
return fetcherSubmit(JSON.stringify(patch), {
|
||||
method: 'POST',
|
||||
action: href('/cloud-credentials/:cloudCredentialId/update', {
|
||||
cloudCredentialId,
|
||||
}),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcher,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
72
packages/insomnia/src/routes/cloud-credentials.create.tsx
Normal file
72
packages/insomnia/src/routes/cloud-credentials.create.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { EXTERNAL_VAULT_PLUGIN_NAME } from '~/common/constants';
|
||||
import * as models from '~/models';
|
||||
import type { BaseCloudCredential } from '~/models/cloud-credential';
|
||||
import { executePluginMainAction } from '~/plugins';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import type { Route } from './+types/cloud-credentials.create';
|
||||
|
||||
type CreateCloudCredentialsData = BaseCloudCredential & { isAuthenticated?: boolean; provider: string };
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const patch = await request.json();
|
||||
const { name, provider, credentials, isAuthenticated } = patch as CreateCloudCredentialsData;
|
||||
invariant(name && typeof name === 'string', 'Name is required');
|
||||
invariant(provider, 'Cloud Provider name is required');
|
||||
invariant(credentials, 'Credentials are required');
|
||||
if (isAuthenticated) {
|
||||
// find credential with same name for oauth authenticated cloud service
|
||||
const existingCredential = await models.cloudCredential.getByName(name, provider);
|
||||
if (existingCredential.length === 0) {
|
||||
await models.cloudCredential.create(patch);
|
||||
} else {
|
||||
await models.cloudCredential.update(existingCredential[0], patch);
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
const authenticateResponse = await executePluginMainAction({
|
||||
pluginName: EXTERNAL_VAULT_PLUGIN_NAME,
|
||||
actionName: 'authenticate',
|
||||
params: { provider, credentials },
|
||||
});
|
||||
const { success, error, result } = authenticateResponse!;
|
||||
if (error) {
|
||||
return {
|
||||
error: `${error.errorMessage}`,
|
||||
};
|
||||
}
|
||||
if (success) {
|
||||
if (provider === 'hashicorp') {
|
||||
// update access token and expires_at
|
||||
const { access_token, expires_at } = result as { access_token: string; expires_at: number };
|
||||
patch.credentials['access_token'] = access_token;
|
||||
patch.credentials['expires_at'] = expires_at;
|
||||
}
|
||||
await models.cloudCredential.create(patch);
|
||||
return result as { access_token: string; expires_at: number };
|
||||
}
|
||||
return { error: 'Unexpected response from ' + provider };
|
||||
}
|
||||
|
||||
export function useCreateCloudCredentialActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcher } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
function submit(data: CreateCloudCredentialsData) {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/cloud-credentials/create'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcher,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { LoaderFunction } from 'react-router';
|
||||
import { useCallback } from 'react';
|
||||
import { useFetcher } from 'react-router';
|
||||
|
||||
import { database } from '../../common/database';
|
||||
import { fuzzyMatch } from '../../common/misc';
|
||||
import { database } from '~/common/database';
|
||||
import { fuzzyMatch } from '~/common/misc';
|
||||
import {
|
||||
environment,
|
||||
grpcRequest,
|
||||
@@ -11,40 +12,20 @@ import {
|
||||
userSession,
|
||||
webSocketRequest,
|
||||
workspace,
|
||||
} from '../../models';
|
||||
import type { Environment } from '../../models/environment';
|
||||
import type { GrpcRequest } from '../../models/grpc-request';
|
||||
import { isScratchpadOrganizationId, type Organization } from '../../models/organization';
|
||||
import { isRemoteProject, type Project } from '../../models/project';
|
||||
import type { Request } from '../../models/request';
|
||||
import type { RequestGroup } from '../../models/request-group';
|
||||
import type { WebSocketRequest } from '../../models/websocket-request';
|
||||
import { scopeToActivity, type Workspace } from '../../models/workspace';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
} from '~/models';
|
||||
import type { Environment } from '~/models/environment';
|
||||
import type { GrpcRequest } from '~/models/grpc-request';
|
||||
import { isScratchpadOrganizationId, type Organization } from '~/models/organization';
|
||||
import { isRemoteProject, type Project } from '~/models/project';
|
||||
import type { Request } from '~/models/request';
|
||||
import type { RequestGroup } from '~/models/request-group';
|
||||
import type { WebSocketRequest } from '~/models/websocket-request';
|
||||
import { scopeToActivity, type Workspace } from '~/models/workspace';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
export interface CommandItem<TItem> {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
organizationName: string;
|
||||
projectName: string;
|
||||
workspaceName?: string;
|
||||
item: TItem;
|
||||
}
|
||||
import type { Route } from './+types/commands';
|
||||
|
||||
export interface LoaderResult {
|
||||
current: {
|
||||
requests: CommandItem<Request | GrpcRequest | WebSocketRequest>[];
|
||||
files: CommandItem<Workspace & { teamProjectId: string }>[];
|
||||
environments: Environment[];
|
||||
};
|
||||
other: {
|
||||
requests: CommandItem<Request | GrpcRequest | WebSocketRequest>[];
|
||||
files: CommandItem<Workspace & { teamProjectId: string }>[];
|
||||
};
|
||||
}
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
export async function clientLoader(args: Route.ClientLoaderArgs) {
|
||||
const searchParams = new URL(args.request.url).searchParams;
|
||||
const organizationId = searchParams.get('organizationId');
|
||||
invariant(organizationId, 'organizationId is required');
|
||||
@@ -304,4 +285,42 @@ export const loader: LoaderFunction = async args => {
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function useCommandsLoaderFetcher() {
|
||||
const { load: fetcherLoad, ...fetcherRest } = useFetcher<typeof clientLoader>();
|
||||
|
||||
const load = useCallback(
|
||||
({
|
||||
organizationId,
|
||||
projectId,
|
||||
workspaceId,
|
||||
filter,
|
||||
}: {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
filter?: string;
|
||||
}) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('organizationId', organizationId);
|
||||
params.set('projectId', projectId);
|
||||
if (workspaceId) {
|
||||
params.set('workspaceId', workspaceId);
|
||||
}
|
||||
if (filter) {
|
||||
params.set('filter', filter);
|
||||
}
|
||||
|
||||
return fetcherLoad(`/commands?${params.toString()}`, {
|
||||
flushSync: true,
|
||||
});
|
||||
},
|
||||
[fetcherLoad],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git-credentials.github.complete-sign-in';
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const { code, state } = (await request.json()) as { code: string; state: string; path: string };
|
||||
await window.main.git.completeSignInToGitHub({
|
||||
code,
|
||||
state,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useGithubCompleteSignInFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: { code: string; state: string }) => {
|
||||
return fetcherSubmit(data, { action: href('/git-credentials/github/complete-sign-in'), method: 'POST' });
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git-credentials.github.init-sign-in';
|
||||
|
||||
export async function clientAction(_args: Route.ClientActionArgs) {
|
||||
await window.main.git.initSignInToGitHub();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useInitSignInToGitHubFetcher() {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>();
|
||||
|
||||
const submit = useCallback(() => {
|
||||
return fetcherSubmit({}, { action: href('/git-credentials/github/init-sign-in'), method: 'POST' });
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git-credentials.github.sign-out';
|
||||
|
||||
export async function clientAction(_args: Route.ClientActionArgs) {
|
||||
await window.main.git.signOutOfGitHub();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useGithubSignOutFetcher() {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>();
|
||||
|
||||
const submit = useCallback(() => {
|
||||
return fetcherSubmit({}, { action: href('/git-credentials/github/sign-out'), method: 'POST' });
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
25
packages/insomnia/src/routes/git-credentials.github.tsx
Normal file
25
packages/insomnia/src/routes/git-credentials.github.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { gitCredentials } from '~/models';
|
||||
|
||||
import type { Route } from './+types/git-credentials.github';
|
||||
|
||||
export async function clientLoader(_args: Route.ClientActionArgs) {
|
||||
const credentials = await gitCredentials.getByProvider('github');
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
export function useGitHubCredentialsFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { load: fetcherLoad, ...fetcherRest } = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback(() => {
|
||||
return fetcherLoad(href('/git-credentials/github'));
|
||||
}, [fetcherLoad]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git-credentials.gitlab.complete-sign-in';
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const { code, state } = (await request.json()) as { code: string; state: string; path: string };
|
||||
await window.main.git.completeSignInToGitLab({
|
||||
code,
|
||||
state,
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useGitLabCompleteSignInFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: { code: string; state: string }) => {
|
||||
return fetcherSubmit(data, { action: href('/git-credentials/gitlab/complete-sign-in'), method: 'POST' });
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git-credentials.gitlab.init-sign-in';
|
||||
|
||||
export async function clientAction(_args: Route.ClientActionArgs) {
|
||||
await window.main.git.initSignInToGitLab();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useInitSignInToGitLabFetcher() {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>();
|
||||
|
||||
const submit = useCallback(() => {
|
||||
return fetcherSubmit({}, { action: href('/git-credentials/gitlab/init-sign-in'), method: 'POST' });
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git-credentials.gitlab.sign-out';
|
||||
|
||||
export async function clientAction(_args: Route.ClientActionArgs) {
|
||||
await window.main.git.signOutOfGitLab();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useGitLabSignOutFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
return fetcherSubmit({}, { action: href('/git-credentials/gitlab/sign-out'), method: 'POST' });
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
25
packages/insomnia/src/routes/git-credentials.gitlab.tsx
Normal file
25
packages/insomnia/src/routes/git-credentials.gitlab.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { gitCredentials } from '~/models';
|
||||
|
||||
import type { Route } from './+types/git-credentials.gitlab';
|
||||
|
||||
export async function clientLoader(_args: Route.ClientActionArgs) {
|
||||
const credentials = await gitCredentials.getByProvider('gitlab');
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
export function useGitLabCredentialsFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { load: fetcherLoad, ...fetcherRest } = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback(() => {
|
||||
return fetcherLoad(href('/git-credentials/gitlab'));
|
||||
}, [fetcherLoad]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
9
packages/insomnia/src/routes/git-credentials.tsx
Normal file
9
packages/insomnia/src/routes/git-credentials.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { LoaderFunctionArgs } from 'react-router';
|
||||
|
||||
import { gitCredentials } from '~/models';
|
||||
|
||||
export async function clientLoader(_args: LoaderFunctionArgs) {
|
||||
const credentials = await gitCredentials.all();
|
||||
|
||||
return credentials;
|
||||
}
|
||||
40
packages/insomnia/src/routes/git.branch.checkout.tsx
Normal file
40
packages/insomnia/src/routes/git.branch.checkout.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import type { Route } from './+types/git.branch.checkout';
|
||||
|
||||
interface CheckoutGitBranchData {
|
||||
branch: string;
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as CheckoutGitBranchData;
|
||||
|
||||
invariant(typeof data.branch === 'string', 'Branch is required');
|
||||
|
||||
return window.main.git.checkoutGitBranch(data);
|
||||
}
|
||||
|
||||
export function useGitProjectCheckoutBranchActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: CheckoutGitBranchData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/branch/checkout'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
39
packages/insomnia/src/routes/git.branch.delete.tsx
Normal file
39
packages/insomnia/src/routes/git.branch.delete.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { invariant } from '../utils/invariant';
|
||||
import type { Route } from './+types/git.branch.delete';
|
||||
|
||||
interface DeleteGitBranchData {
|
||||
branch: string;
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as DeleteGitBranchData;
|
||||
|
||||
invariant(typeof data.branch === 'string', 'Branch is required');
|
||||
|
||||
return window.main.git.deleteGitBranch(data);
|
||||
}
|
||||
|
||||
export function useGitProjectDeleteBranchActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: DeleteGitBranchData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/branch/delete'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
40
packages/insomnia/src/routes/git.branch.new.tsx
Normal file
40
packages/insomnia/src/routes/git.branch.new.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import type { Route } from './+types/git.branch.new';
|
||||
|
||||
interface NewGitBranchData {
|
||||
branch: string;
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as NewGitBranchData;
|
||||
|
||||
invariant(typeof data.branch === 'string', 'Branch is required');
|
||||
|
||||
return window.main.git.createNewGitBranch(data);
|
||||
}
|
||||
|
||||
export function useGitProjectNewBranchActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: NewGitBranchData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/branch/new'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
41
packages/insomnia/src/routes/git.branches.tsx
Normal file
41
packages/insomnia/src/routes/git.branches.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.branches';
|
||||
|
||||
export async function clientLoader({ request }: Route.ClientLoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
|
||||
const workspaceId = params.workspaceId;
|
||||
const projectId = params.projectId;
|
||||
return window.main.git.getGitBranches({
|
||||
projectId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGitProjectBranchesLoaderFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
load: fetcherLoad,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback(
|
||||
({ projectId, workspaceId }: { workspaceId?: string; projectId: string }) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (workspaceId) {
|
||||
searchParams.set('workspaceId', workspaceId);
|
||||
}
|
||||
searchParams.set('projectId', projectId);
|
||||
return fetcherLoad(`${href('/git/branches')}?${searchParams.toString()}`);
|
||||
},
|
||||
[fetcherLoad]
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
41
packages/insomnia/src/routes/git.changes.tsx
Normal file
41
packages/insomnia/src/routes/git.changes.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.changes';
|
||||
|
||||
export async function clientLoader({ request }: Route.ClientLoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
const workspaceId = params.workspaceId;
|
||||
const projectId = params.projectId;
|
||||
|
||||
return window.main.git.gitChangesLoader({
|
||||
projectId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGitProjectChangesFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
load: fetcherLoad,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback(
|
||||
({ projectId, workspaceId }: { projectId: string; workspaceId?: string }) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (workspaceId) {
|
||||
searchParams.set('workspaceId', workspaceId);
|
||||
}
|
||||
searchParams.set('projectId', projectId);
|
||||
|
||||
return fetcherLoad(`${href('/git/changes')}?${searchParams.toString()}`);
|
||||
},
|
||||
[fetcherLoad]
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
57
packages/insomnia/src/routes/git.clone.tsx
Normal file
57
packages/insomnia/src/routes/git.clone.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, redirect, useFetcher } from 'react-router';
|
||||
|
||||
import type { GitCredentials } from '~/models/git-repository';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import type { Route } from './+types/git.clone';
|
||||
|
||||
interface CloneGitRepoData {
|
||||
organizationId: string;
|
||||
projectId?: string;
|
||||
uri: string;
|
||||
author: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
credentials: GitCredentials;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as CloneGitRepoData;
|
||||
|
||||
const { errors, projectId } = await window.main.git.cloneGitRepo(data);
|
||||
|
||||
if (errors) {
|
||||
return { errors };
|
||||
}
|
||||
|
||||
invariant(projectId, 'Project ID is required');
|
||||
|
||||
return redirect(
|
||||
href(`/organization/:organizationId/project/:projectId`, {
|
||||
organizationId: data.organizationId,
|
||||
projectId,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function useGitCloneActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
submit: fetcherSubmit,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback((data: CloneGitRepoData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/clone'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
45
packages/insomnia/src/routes/git.commit.tsx
Normal file
45
packages/insomnia/src/routes/git.commit.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import type { Route } from './+types/git.commit';
|
||||
|
||||
interface CommitGitRepoData {
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
message: string;
|
||||
push?: boolean;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as CommitGitRepoData;
|
||||
|
||||
invariant(typeof data.message === 'string', 'Message is required');
|
||||
|
||||
if (data.push) {
|
||||
return window.main.git.commitAndPushToGitRepo(data);
|
||||
}
|
||||
|
||||
return window.main.git.commitToGitRepo(data);
|
||||
}
|
||||
|
||||
export function useGitProjectCommitActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
submit: fetcherSubmit,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback((data: CommitGitRepoData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
action: href('/git/commit'),
|
||||
method: 'POST',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
56
packages/insomnia/src/routes/git.diff.tsx
Normal file
56
packages/insomnia/src/routes/git.diff.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
import type { Route } from './+types/git.diff';
|
||||
|
||||
export async function clientLoader({ request }: Route.ClientLoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const filepath = url.searchParams.get('filepath');
|
||||
invariant(filepath, 'Filepath is required');
|
||||
|
||||
const staged = url.searchParams.get('staged') === 'true';
|
||||
const projectId = url.searchParams.get('projectId');
|
||||
invariant(projectId, 'Project ID is required');
|
||||
const workspaceId = url.searchParams.get('workspaceId') || undefined;
|
||||
|
||||
return window.main.git.diffFileLoader({ filepath, staged, projectId, workspaceId });
|
||||
}
|
||||
|
||||
export function useGitProjectDiffLoaderFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
load: fetcherLoad,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback((
|
||||
{
|
||||
workspaceId,
|
||||
projectId,
|
||||
filePath,
|
||||
staged,
|
||||
}: {
|
||||
workspaceId?: string;
|
||||
projectId: string;
|
||||
filePath: string;
|
||||
staged: boolean;
|
||||
}
|
||||
) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('filepath', filePath);
|
||||
params.set('staged', staged ? 'true' : 'false');
|
||||
if (workspaceId) {
|
||||
params.set('workspaceId', workspaceId);
|
||||
}
|
||||
params.set('projectId', projectId);
|
||||
|
||||
return fetcherLoad(`${href('/git/diff')}?${params.toString()}`);
|
||||
}, [fetcherLoad]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
36
packages/insomnia/src/routes/git.discard.tsx
Normal file
36
packages/insomnia/src/routes/git.discard.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.discard';
|
||||
|
||||
interface DiscardGitChangesData {
|
||||
paths: string[];
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as DiscardGitChangesData;
|
||||
|
||||
return window.main.git.discardChanges(data);
|
||||
}
|
||||
|
||||
export function useGitProjectDiscardActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
submit: fetcherSubmit,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback((data: DiscardGitChangesData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/discard'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
36
packages/insomnia/src/routes/git.fetch.tsx
Normal file
36
packages/insomnia/src/routes/git.fetch.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.fetch';
|
||||
|
||||
interface FetchGitData {
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
console.log('Client action for git fetch', request);
|
||||
const data = (await request.json()) as FetchGitData;
|
||||
return window.main.git.gitFetchAction(data);
|
||||
}
|
||||
|
||||
export function useGitProjectFetchActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
submit: fetcherSubmit,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback((data: FetchGitData) => {
|
||||
console.log('Submitting git fetch action', data);
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/fetch'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
50
packages/insomnia/src/routes/git.init-clone.tsx
Normal file
50
packages/insomnia/src/routes/git.init-clone.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { GitCredentials } from '~/models/git-repository';
|
||||
|
||||
import type { Route } from './+types/git.init-clone';
|
||||
|
||||
interface RepoInitCloneData {
|
||||
organizationId: string;
|
||||
projectId?: string;
|
||||
uri: string;
|
||||
authorName: string;
|
||||
authorEmail: string;
|
||||
credentials: Required<GitCredentials>;
|
||||
ref?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as RepoInitCloneData;
|
||||
|
||||
const initCloneResult = await window.main.git.initGitRepoClone(data);
|
||||
|
||||
if ('errors' in initCloneResult) {
|
||||
return { errors: initCloneResult.errors };
|
||||
}
|
||||
|
||||
return {
|
||||
files: initCloneResult.files,
|
||||
};
|
||||
}
|
||||
|
||||
export function useGitProjectInitCloneActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
submit: fetcherSubmit,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback((data: RepoInitCloneData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
action: href('/git/init-clone'),
|
||||
method: 'POST',
|
||||
encType: 'application/json',
|
||||
});
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
37
packages/insomnia/src/routes/git.log.tsx
Normal file
37
packages/insomnia/src/routes/git.log.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.log';
|
||||
|
||||
export async function clientLoader({ request }: Route.ClientLoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
const workspaceId = params.workspaceId;
|
||||
const projectId = params.projectId;
|
||||
|
||||
return window.main.git.gitLogLoader({ workspaceId, projectId });
|
||||
}
|
||||
|
||||
export function useGitProjectLogLoaderFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
load: fetcherLoad,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback(
|
||||
({ workspaceId, projectId }: { workspaceId?: string; projectId: string }) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (workspaceId) {
|
||||
searchParams.set('workspaceId', workspaceId);
|
||||
}
|
||||
searchParams.set('projectId', projectId);
|
||||
return fetcherLoad(`${href('/git/log')}?${searchParams.toString()}`);
|
||||
},
|
||||
[fetcherLoad]
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.migrate-legacy-insomnia-folder-to-file';
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const { projectId } = (await request.json()) as {
|
||||
projectId: string;
|
||||
};
|
||||
return window.main.git.migrateLegacyInsomniaFolderToFile({ projectId });
|
||||
}
|
||||
|
||||
export function useGitProjectMigrateLegacyInsomniaFolderActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
submit: fetcherSubmit,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(({ projectId }: { projectId: string }) => {
|
||||
return fetcherSubmit(
|
||||
{
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
action: href('/git/migrate-legacy-insomnia-folder-to-file'),
|
||||
},
|
||||
);
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
33
packages/insomnia/src/routes/git.push.tsx
Normal file
33
packages/insomnia/src/routes/git.push.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.push';
|
||||
|
||||
interface PushGitData {
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as PushGitData;
|
||||
|
||||
return window.main.git.pushToGitRemote(data);
|
||||
}
|
||||
|
||||
export function useGitProjectPushActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: PushGitData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/push'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return { ...fetcherRest, submit };
|
||||
}
|
||||
37
packages/insomnia/src/routes/git.remote-branches.tsx
Normal file
37
packages/insomnia/src/routes/git.remote-branches.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { GitCredentials } from '~/models/git-repository';
|
||||
|
||||
import type { Route } from './+types/git.remote-branches';
|
||||
|
||||
interface FetchRemoteBranchesData {
|
||||
uri: string;
|
||||
credentials: GitCredentials;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as FetchRemoteBranchesData;
|
||||
|
||||
return window.main.git.fetchGitRemoteBranches(data);
|
||||
}
|
||||
|
||||
export function useGitRemoteBranchesActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: FetchRemoteBranchesData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href(`/git/remote-branches`),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
35
packages/insomnia/src/routes/git.repo.tsx
Normal file
35
packages/insomnia/src/routes/git.repo.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.repo';
|
||||
|
||||
export async function clientLoader({ request }: Route.ClientLoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
const workspaceId = params.workspaceId;
|
||||
const projectId = params.projectId;
|
||||
|
||||
return window.main.git.loadGitRepository({ workspaceId, projectId });
|
||||
}
|
||||
|
||||
export function useGitProjectRepoFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { load: fetcherLoad, ...fetcherRest } = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback(
|
||||
({ workspaceId, projectId }: { workspaceId?: string; projectId: string }) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (workspaceId) {
|
||||
searchParams.set('workspaceId', workspaceId);
|
||||
}
|
||||
searchParams.set('projectId', projectId);
|
||||
|
||||
return fetcherLoad(`${href('/git/repo')}?${searchParams.toString()}`);
|
||||
},
|
||||
[fetcherLoad],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
32
packages/insomnia/src/routes/git.repository-tree.tsx
Normal file
32
packages/insomnia/src/routes/git.repository-tree.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.repository-tree';
|
||||
|
||||
export async function clientLoader({ request }: Route.ClientLoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
const params = Object.fromEntries(url.searchParams.entries());
|
||||
|
||||
const projectId = params.projectId;
|
||||
return window.main.git.getRepositoryDirectoryTree({ projectId });
|
||||
}
|
||||
|
||||
export function useGitProjectRepositoryTreeLoaderFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { load: fetcherLoad, ...fetcherRest } = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback(
|
||||
({ projectId }: { projectId: string }) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.set('projectId', projectId);
|
||||
|
||||
return fetcherLoad(`${href('/git/repository-tree')}?${searchParams.toString()}`);
|
||||
},
|
||||
[fetcherLoad],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
35
packages/insomnia/src/routes/git.reset.tsx
Normal file
35
packages/insomnia/src/routes/git.reset.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.reset';
|
||||
|
||||
interface ResetGitRepoParams {
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as ResetGitRepoParams;
|
||||
|
||||
return window.main.git.resetGitRepo(data);
|
||||
}
|
||||
|
||||
export function useGitProjectResetActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: ResetGitRepoParams) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/reset'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
35
packages/insomnia/src/routes/git.stage.tsx
Normal file
35
packages/insomnia/src/routes/git.stage.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.stage';
|
||||
|
||||
interface StageGitChangesData {
|
||||
paths: string[];
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as StageGitChangesData;
|
||||
return window.main.git.stageChanges(data);
|
||||
}
|
||||
|
||||
export function useGitProjectStageActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: StageGitChangesData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/stage'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
35
packages/insomnia/src/routes/git.status.tsx
Normal file
35
packages/insomnia/src/routes/git.status.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.status';
|
||||
|
||||
interface GitStatusData {
|
||||
workspaceId?: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as GitStatusData;
|
||||
|
||||
return window.main.git.gitStatus(data);
|
||||
}
|
||||
|
||||
export function useGitProjectStatusActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: GitStatusData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/status'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
36
packages/insomnia/src/routes/git.unstage.tsx
Normal file
36
packages/insomnia/src/routes/git.unstage.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { Route } from './+types/git.unstage';
|
||||
|
||||
interface UnstageGitChangesData {
|
||||
paths: string[];
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as UnstageGitChangesData;
|
||||
|
||||
return window.main.git.unstageChanges(data);
|
||||
}
|
||||
|
||||
export function useGitProjectUnstageActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: UnstageGitChangesData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href('/git/unstage'),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
43
packages/insomnia/src/routes/git.update.tsx
Normal file
43
packages/insomnia/src/routes/git.update.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import type { GitCredentials } from '~/models/git-repository';
|
||||
|
||||
import type { Route } from './+types/git.update';
|
||||
|
||||
interface UpdateGitRepoData {
|
||||
author: {
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
credentials: GitCredentials;
|
||||
uri: string;
|
||||
workspaceId?: string;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const data = (await request.json()) as UpdateGitRepoData;
|
||||
|
||||
return window.main.git.updateGitRepo(data);
|
||||
}
|
||||
|
||||
export function useGitProjectUpdateActionFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback(
|
||||
(data: UpdateGitRepoData) => {
|
||||
return fetcherSubmit(JSON.stringify(data), {
|
||||
method: 'POST',
|
||||
action: href(`/git/update`),
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,19 @@
|
||||
import type { ActionFunctionArgs } from 'react-router';
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { importResourcesToProject, importResourcesToWorkspace } from '../../common/import';
|
||||
import * as models from '../../models';
|
||||
import { isRemoteProject } from '../../models/project';
|
||||
import type { Workspace } from '../../models/workspace';
|
||||
import { importResourcesToProject, importResourcesToWorkspace } from '~/common/import';
|
||||
import * as models from '~/models';
|
||||
import { isRemoteProject } from '~/models/project';
|
||||
import type { Workspace } from '~/models/workspace';
|
||||
import {
|
||||
initializeLocalBackendProjectAndMarkForSync,
|
||||
pushSnapshotOnInitialize,
|
||||
} from '../../sync/vcs/initialize-backend-project';
|
||||
import { VCSInstance } from '../../sync/vcs/insomnia-sync';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { fetchAndCacheOrganizationStorageRule } from '../organization-utils';
|
||||
} from '~/sync/vcs/initialize-backend-project';
|
||||
import { VCSInstance } from '~/sync/vcs/insomnia-sync';
|
||||
import { fetchAndCacheOrganizationStorageRule } from '~/ui/organization-utils';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
export interface ImportResourcesActionResult {
|
||||
errors?: string[];
|
||||
done: boolean;
|
||||
}
|
||||
import type { Route } from './+types/import.resources';
|
||||
|
||||
export const importScannedResources = async ({
|
||||
organizationId,
|
||||
@@ -44,26 +42,53 @@ export const importScannedResources = async ({
|
||||
}
|
||||
};
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const data = (await request.json()) as {
|
||||
organizationId: string;
|
||||
projectId: string;
|
||||
workspaceId?: string;
|
||||
};
|
||||
|
||||
const organizationId = data.organizationId;
|
||||
const projectId = data.projectId;
|
||||
const workspaceId = data.workspaceId;
|
||||
|
||||
invariant(typeof organizationId === 'string', 'OrganizationId is required.');
|
||||
invariant(typeof projectId === 'string', 'ProjectId is required.');
|
||||
|
||||
await importScannedResources({
|
||||
organizationId: formData.get('organizationId') as string,
|
||||
projectId: formData.get('projectId') as string,
|
||||
workspaceId: formData.get('workspaceId') as string | undefined,
|
||||
organizationId,
|
||||
projectId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
// TODO: find more elegant way to wait for import to finish
|
||||
return { done: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to import resources:', error);
|
||||
return {
|
||||
errors: [error instanceof Error ? error.message : 'An unknown error occurred'],
|
||||
done: false,
|
||||
errors: ['Failed to import resources.'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function useImportResourcesFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
const submit = useCallback(
|
||||
(data: { organizationId: string; projectId: string; workspaceId?: string }) => {
|
||||
fetcherSubmit(JSON.stringify(data), {
|
||||
action: href('/import/resources'),
|
||||
method: 'POST',
|
||||
encType: 'application/json',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
|
||||
// The reason why we put this function here is because this function indirectly depends on some modules that can only run in a browser environment.
|
||||
// If we put this function in import.ts which is depended by Inso CLI, Inso CLI will fail to build because it doesn't have access to the browser environment.
|
||||
// So we put this function here and pass it to importResourcesToProject func to avoid the dependency issue.
|
||||
@@ -92,9 +117,10 @@ export async function syncNewWorkspaceIfNeeded(newWorkspace: Workspace) {
|
||||
workspace: newWorkspace,
|
||||
project,
|
||||
});
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
console.warn(
|
||||
`Failed to initialize sync to insomnia cloud for workspace ${newWorkspace._id}. This will be retried when the workspace is opened on the app. ${e.message}`,
|
||||
`Failed to initialize sync to insomnia cloud for workspace ${newWorkspace._id}. This will be retried when the workspace is opened on the app. ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type { ActionFunctionArgs } from 'react-router';
|
||||
import { useCallback } from 'react';
|
||||
import { type ActionFunctionArgs, href, useFetcher } from 'react-router';
|
||||
|
||||
import type { ScanResult } from '../../common/import';
|
||||
import { fetchImportContentFromURI, getFilesFromPostmanExportedDataDump, scanResources } from '../../common/import';
|
||||
import type { ImportEntry } from '../../utils/importers/entities';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { SegmentEvent } from '../analytics';
|
||||
import type { ScanResult } from '~/common/import';
|
||||
import { fetchImportContentFromURI, getFilesFromPostmanExportedDataDump, scanResources } from '~/common/import';
|
||||
import { SegmentEvent } from '~/ui/analytics';
|
||||
import type { ImportEntry } from '~/utils/importers/entities';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
type SourceType = 'file' | 'uri' | 'clipboard';
|
||||
|
||||
@@ -129,21 +130,43 @@ export const scanImportResources = async (data: {
|
||||
return result;
|
||||
};
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
interface ImportScanInputData {
|
||||
source: SourceType;
|
||||
uri?: string;
|
||||
filePaths?: string | string[];
|
||||
postmanArchiveFile?: string | null;
|
||||
}
|
||||
|
||||
export async function clientAction({ request }: ActionFunctionArgs) {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const data = Object.fromEntries(formData.entries()) as unknown as ImportScanInputData;
|
||||
|
||||
return await scanImportResources({
|
||||
source: formData.get('importFrom') as SourceType,
|
||||
uri: formData.get('uri') as string | undefined,
|
||||
filePaths: formData.get('filePaths') as string | string[] | undefined,
|
||||
postmanArchiveFile: formData.get('postmanArchiveFile') as string | null,
|
||||
});
|
||||
return await scanImportResources(data);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
||||
return [
|
||||
{
|
||||
errors: [err.message],
|
||||
errors: [errorMessage],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export function useScanResourcesFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { submit: fetcherSubmit, ...fetcherRest } = useFetcher<typeof clientAction>(args);
|
||||
const submit = useCallback(
|
||||
(data: FormData | HTMLFormElement) => {
|
||||
return fetcherSubmit(data, {
|
||||
action: href('/import/scan'),
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
[fetcherSubmit],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -1,26 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Button, Heading, Radio, RadioGroup } from 'react-aria-components';
|
||||
import { type ActionFunction, type LoaderFunction, redirect, useFetcher } from 'react-router';
|
||||
import { href, redirect, useFetcher } from 'react-router';
|
||||
|
||||
import { shouldMigrateProjectUnderOrganization } from '../../sync/vcs/migrate-projects-into-organization';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { Icon } from '../components/icon';
|
||||
import { InsomniaLogo } from '../components/insomnia-icon';
|
||||
import { TrailLinesContainer } from '../components/trail-lines-container';
|
||||
import { shouldMigrateProjectUnderOrganization } from '~/sync/vcs/migrate-projects-into-organization';
|
||||
import { Icon } from '~/ui/components/icon';
|
||||
import { InsomniaLogo } from '~/ui/components/insomnia-icon';
|
||||
import { TrailLinesContainer } from '~/ui/components/trail-lines-container';
|
||||
import { invariant } from '~/utils/invariant';
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
import type { Route } from './+types/onboarding.migrate';
|
||||
|
||||
export async function clientLoader(_args: Route.ClientLoaderArgs) {
|
||||
if (!(await shouldMigrateProjectUnderOrganization())) {
|
||||
return redirect('/organization');
|
||||
return redirect(href('/organization'));
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
interface MigrationActionData {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
export async function clientAction({ request }: Route.ClientActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const type = formData.get('type');
|
||||
invariant(type === 'local' || type === 'remote', 'Expected type to be either local or remote');
|
||||
@@ -28,10 +25,10 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
localStorage.setItem('prefers-project-type', type);
|
||||
|
||||
return redirect('/organization');
|
||||
};
|
||||
}
|
||||
|
||||
export const Migrate = () => {
|
||||
const { Form, state } = useFetcher<MigrationActionData>();
|
||||
const Component = () => {
|
||||
const { Form, state } = useFetcher<typeof clientAction>();
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full bg-[--color-bg] text-left text-base">
|
||||
@@ -103,3 +100,5 @@ export const Migrate = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Component;
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React from 'react';
|
||||
import { Link, Route, Routes, useLocation } from 'react-router';
|
||||
|
||||
import { InsomniaLogo } from '../components/insomnia-icon';
|
||||
import { TrailLinesContainer } from '../components/trail-lines-container';
|
||||
import git_projects from '../images/onboarding/git_projects.png';
|
||||
import multiple_tabs from '../images/onboarding/multiple_tabs.png';
|
||||
import secret_vaults from '../images/onboarding/secret_vaults.png';
|
||||
import { InsomniaLogo } from '~/ui/components/insomnia-icon';
|
||||
import { TrailLinesContainer } from '~/ui/components/trail-lines-container';
|
||||
import git_projects from '~/ui/images/onboarding/git_projects.png';
|
||||
import multiple_tabs from '~/ui/images/onboarding/multiple_tabs.png';
|
||||
import secret_vaults from '~/ui/images/onboarding/secret_vaults.png';
|
||||
|
||||
const features = [
|
||||
{
|
||||
@@ -123,7 +122,7 @@ const FeatureWizardView = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Onboarding = () => {
|
||||
const Component = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
@@ -165,4 +164,4 @@ const Onboarding = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Onboarding;
|
||||
export default Component;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { redirect } from 'react-router';
|
||||
|
||||
import { syncProjects } from '~/ui/organization-utils';
|
||||
import { getInitialRouteForOrganization } from '~/utils/router';
|
||||
|
||||
import type { Route } from './+types/organization.$organizationId._index';
|
||||
|
||||
export async function clientLoader({ params }: Route.ClientLoaderArgs) {
|
||||
const { organizationId } = params;
|
||||
|
||||
try {
|
||||
await syncProjects(organizationId);
|
||||
} catch {
|
||||
console.log('[project] Could not fetch remote projects.');
|
||||
}
|
||||
const initialOrganizationRoute = await getInitialRouteForOrganization({ organizationId });
|
||||
return redirect(initialOrganizationRoute);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import { userSession } from '~/models';
|
||||
import { insomniaFetch } from '~/ui/insomniaFetch';
|
||||
|
||||
import type { Route } from './+types/organization.$organizationId.collaborators-search';
|
||||
|
||||
type CollaboratorType = 'invite' | 'member' | 'group';
|
||||
|
||||
interface CollaboratorSearchResultItem {
|
||||
id: string;
|
||||
picture: string;
|
||||
type: CollaboratorType;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function clientLoader({ params, request }: Route.ClientLoaderArgs) {
|
||||
const { id: sessionId } = await userSession.get();
|
||||
|
||||
const { organizationId } = params;
|
||||
|
||||
try {
|
||||
const requestUrl = new URL(request.url);
|
||||
const searchParams = Object.fromEntries(requestUrl.searchParams.entries());
|
||||
|
||||
const collaboratorsSearchList = await insomniaFetch<CollaboratorSearchResultItem[]>({
|
||||
method: 'GET',
|
||||
path: `/v1/desktop/organizations/${organizationId}/collaborators/search/${searchParams.query}`,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
return collaboratorsSearchList;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function useCollaboratorsSearchLoaderFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const { load: fetcherLoad, ...fetcherRest } = useFetcher<typeof clientLoader>(args);
|
||||
|
||||
const load = useCallback(
|
||||
({ organizationId, query }: { organizationId: string; query?: string }) => {
|
||||
return fetcherLoad(
|
||||
`${href(`/organization/:organizationId/collaborators-search`, { organizationId })}?${encodeURIComponent(query || '')}`,
|
||||
);
|
||||
},
|
||||
[fetcherLoad],
|
||||
);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
load,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useCallback } from 'react';
|
||||
import { href, useFetcher } from 'react-router';
|
||||
|
||||
import * as models from '~/models';
|
||||
import { insomniaFetch } from '~/ui/insomniaFetch';
|
||||
|
||||
import type { Route } from './+types/organization.$organizationId.collaborators.invites.$invitationId.reinvite';
|
||||
|
||||
export async function clientAction({ params }: Route.ClientActionArgs) {
|
||||
const { organizationId, invitationId } = params;
|
||||
|
||||
try {
|
||||
const user = await models.userSession.getOrCreate();
|
||||
const sessionId = user.id;
|
||||
|
||||
const response = await insomniaFetch<{ enabled: boolean }>({
|
||||
method: 'POST',
|
||||
path: `/v1/organizations/${organizationId}/invites/${invitationId}/reinvite`,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch {
|
||||
throw new Error('Failed to reinvite member. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
export function useReinviteFetcher(args?: Parameters<typeof useFetcher>[0]) {
|
||||
const {
|
||||
submit: fetcherSubmit,
|
||||
...fetcherRest
|
||||
} = useFetcher<typeof clientAction>(args);
|
||||
|
||||
const submit = useCallback((
|
||||
{ organizationId, invitationId }: { organizationId: string; invitationId: string }
|
||||
) => {
|
||||
return fetcherSubmit(
|
||||
{},
|
||||
{
|
||||
action: href(`/organization/:organizationId/collaborators/invites/:invitationId/reinvite`, {
|
||||
organizationId,
|
||||
invitationId,
|
||||
}),
|
||||
method: 'POST',
|
||||
},
|
||||
);
|
||||
}, [fetcherSubmit]);
|
||||
|
||||
return {
|
||||
...fetcherRest,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user