diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 034a6ad426..c8ef91f610 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,8 +18,6 @@ concurrency: jobs: Test: runs-on: ubuntu-latest - strategy: - fail-fast: false steps: - name: Checkout branch uses: actions/checkout@v3 @@ -103,8 +101,10 @@ jobs: - name: Smoke test Inso CLI run: npm run test:smoke:cli + # This step should always run even when previous steps fail - name: Upload smoke test traces uses: actions/upload-artifact@v3 + if: failure() with: if-no-files-found: ignore name: ubuntu-smoke-test-traces-${{ github.run_number }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b990f267a..53e056b595 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,4 +9,7 @@ "editor.formatOnSave": true, "editor.formatOnSaveMode": "modifications", "editor.defaultFormatter": "vscode.typescript-language-features", + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, } diff --git a/package-lock.json b/package-lock.json index a32faf7496..a7506416bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13888,6 +13888,11 @@ "@grpc/grpc-js": "^1.0.0" } }, + "node_modules/gsap": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.2.tgz", + "integrity": "sha512-EkYnpG8qHgYBFAwsgsGEqvT1WUidX0tt/ijepx7z8EUJHElykg91RvW1XbkT59T0gZzzszOpjQv7SE41XuIXyQ==" + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -24562,6 +24567,7 @@ "electron-context-menu": "^3.6.1", "electron-log": "^4.4.8", "grpc-reflection-js": "0.3.0", + "gsap": "^3.11.3", "hawk": "9.0.1", "hkdf": "^0.0.2", "hosted-git-info": "5.2.1", diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ApiSpec.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ApiSpec.db new file mode 100644 index 0000000000..0413b07576 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ApiSpec.db @@ -0,0 +1,5 @@ +{"_id":"spc_2e8929148e664a46a2d442a9614a03fa","type":"ApiSpec","parentId":"wrk_d783eca2fa46457c9f02a221ec862458","modified":1694513890776,"created":1694513890776,"fileName":"New Document","contents":"","contentType":"yaml"} +{"_id":"spc_7a58096047614d12b6e26499e3ac4d46","type":"ApiSpec","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694447965881,"created":1694436053827,"fileName":"OpenAPI","contents":"openapi: 3.0.0\ninfo:\n description: \"This is a sample server Petstore server. You can find out more about\n Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net,\n #swagger](http://swagger.io/irc/). For this sample, you can use the api key\n `special-key` to test the authorization filters.\"\n version: 1.0.2\n title: Swagger Petstore\n termsOfService: http://swagger.io/terms/\n contact:\n email: apiteam@swagger.io\n license:\n name: Apache 2.0\n url: http://www.apache.org/licenses/LICENSE-2.0.html\ntags:\n - name: pet\n description: Everything about your Pets\n externalDocs:\n description: Find out more\n url: http://swagger.io\n - name: store\n description: Access to Petstore orders\n - name: user\n description: Operations about user\n externalDocs:\n description: Find out more about our store\n url: http://swagger.io\npaths:\n /pet:\n post:\n tags:\n - pet\n summary: Add a new pet to the store\n description: \"\"\n operationId: addPet\n requestBody:\n $ref: \"#/components/requestBodies/Pet\"\n responses:\n \"405\":\n description: Invalid input\n put:\n tags:\n - pet\n summary: Update an existing pet\n description: \"\"\n operationId: updatePet\n requestBody:\n $ref: \"#/components/requestBodies/Pet\"\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n \"405\":\n description: Validation exception\n /pet/findByStatus:\n get:\n tags:\n - pet\n summary: Finds Pets by status\n description: Multiple status values can be provided with comma separated strings\n operationId: findPetsByStatus\n parameters:\n - name: status\n in: query\n description: Status values that need to be considered for filter\n required: true\n explode: true\n schema:\n type: array\n items:\n type: string\n enum:\n - available\n - pending\n - sold\n default: available\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid status value\n /pet/findByTags:\n get:\n tags:\n - pet\n summary: Finds Pets by tags\n description: Multiple tags can be provided with comma separated strings. Use tag1,\n tag2, tag3 for testing.\n operationId: findPetsByTags\n parameters:\n - name: tags\n in: query\n description: Tags to filter by\n required: true\n explode: true\n schema:\n type: array\n items:\n type: string\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid tag value\n deprecated: true\n \"/pet/{petId}\":\n get:\n tags:\n - pet\n summary: Find pet by ID\n description: Returns a single pet\n operationId: getPetById\n parameters:\n - name: petId\n in: path\n description: ID of pet to return\n required: true\n schema:\n type: integer\n format: int64\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n post:\n tags:\n - pet\n summary: Updates a pet in the store with form data\n description: \"\"\n operationId: updatePetWithForm\n parameters:\n - name: petId\n in: path\n description: ID of pet that needs to be updated\n required: true\n schema:\n type: integer\n format: int64\n requestBody:\n content:\n application/x-www-form-urlencoded:\n schema:\n type: object\n properties:\n name:\n description: Updated name of the pet\n type: string\n status:\n description: Updated status of the pet\n type: string\n responses:\n \"405\":\n description: Invalid input\n delete:\n tags:\n - pet\n summary: Deletes a pet\n description: \"\"\n operationId: deletePet\n parameters:\n - name: api_key\n in: header\n required: false\n schema:\n type: string\n - name: petId\n in: path\n description: Pet id to delete\n required: true\n schema:\n type: integer\n format: int64\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n /store/inventory:\n get:\n tags:\n - store\n summary: Returns pet inventories by status\n description: Returns a map of status codes to quantities\n operationId: getInventory\n responses:\n \"200\":\n description: successful operation\n content:\n application/json:\n schema:\n type: object\n additionalProperties:\n type: integer\n format: int32\n /store/order:\n post:\n tags:\n - store\n summary: Place an order for a pet\n description: \"\"\n operationId: placeOrder\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n description: order placed for purchasing the pet\n required: true\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Order\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n \"400\":\n description: Invalid Order\n \"/store/order/{orderId}\":\n get:\n tags:\n - store\n summary: Find purchase order by ID\n description: For valid response try integer IDs with value >= 1 and <= 10. Other\n values will generated exceptions\n operationId: getOrderById\n parameters:\n - name: orderId\n in: path\n description: ID of pet that needs to be fetched\n required: true\n schema:\n type: integer\n format: int64\n minimum: 1\n maximum: 10\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Order\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Order not found\n delete:\n tags:\n - store\n summary: Delete purchase order by ID\n description: For valid response try integer IDs with positive integer value. Negative\n or non-integer values will generate API errors\n operationId: deleteOrder\n parameters:\n - name: orderId\n in: path\n description: ID of the order that needs to be deleted\n required: true\n schema:\n type: integer\n format: int64\n minimum: 1\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Order not found\n /user:\n post:\n tags:\n - user\n summary: Create user\n description: This can only be done by the logged in user.\n operationId: createUser\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n description: Created user object\n required: true\n responses:\n default:\n description: successful operation\n /user/createWithArray:\n post:\n tags:\n - user\n summary: Creates list of users with given input array\n description: \"\"\n operationId: createUsersWithArrayInput\n requestBody:\n $ref: \"#/components/requestBodies/UserArray\"\n responses:\n default:\n description: successful operation\n /user/createWithList:\n post:\n tags:\n - user\n summary: Creates list of users with given input array\n description: \"\"\n operationId: createUsersWithListInput\n requestBody:\n $ref: \"#/components/requestBodies/UserArray\"\n responses:\n default:\n description: successful operation\n /user/login:\n get:\n tags:\n - user\n summary: Logs user into the system\n description: \"\"\n operationId: loginUser\n parameters:\n - name: username\n in: query\n description: The user name for login\n required: true\n schema:\n type: string\n - name: password\n in: query\n description: The password for login in clear text\n required: true\n schema:\n type: string\n responses:\n \"200\":\n description: successful operation\n headers:\n X-Rate-Limit:\n description: calls per hour allowed by the user\n schema:\n type: integer\n format: int32\n X-Expires-After:\n description: date in UTC when token expires\n schema:\n type: string\n format: date-time\n content:\n application/xml:\n schema:\n type: string\n application/json:\n schema:\n type: string\n \"400\":\n description: Invalid username/password supplied\n /user/logout:\n get:\n tags:\n - user\n summary: Logs out current logged in user session\n description: \"\"\n operationId: logoutUser\n responses:\n default:\n description: successful operation\n \"/user/{username}\":\n get:\n tags:\n - user\n summary: Get user by user name\n description: \"\"\n operationId: getUserByName\n parameters:\n - name: username\n in: path\n description: \"The name that needs to be fetched. Use user1 for testing. \"\n required: true\n schema:\n type: string\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/User\"\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n \"400\":\n description: Invalid username supplied\n \"404\":\n description: User not found\n put:\n tags:\n - user\n summary: Updated user\n description: This can only be done by the logged in user.\n operationId: updateUser\n parameters:\n - name: username\n in: path\n description: name that need to be updated\n required: true\n schema:\n type: string\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n description: Updated user object\n required: true\n responses:\n \"400\":\n description: Invalid user supplied\n \"404\":\n description: User not found\n delete:\n tags:\n - user\n summary: Delete user\n description: This can only be done by the logged in user.\n operationId: deleteUser\n parameters:\n - name: username\n in: path\n description: The name that needs to be deleted\n required: true\n schema:\n type: string\n responses:\n \"400\":\n description: Invalid username supplied\n \"404\":\n description: User not found\nexternalDocs:\n description: Find out more about Swagger\n url: http://swagger.io\nservers:\n - url: https://petstore.swagger.io/v2\ncomponents:\n requestBodies:\n UserArray:\n content:\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/User\"\n description: List of user object\n required: true\n Pet:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/Pet\"\n application/xml:\n schema:\n $ref: \"#/components/schemas/Pet\"\n description: Pet object that needs to be added to the store\n required: true\n schemas:\n Order:\n type: object\n properties:\n id:\n type: integer\n format: int64\n petId:\n type: integer\n format: int64\n quantity:\n type: integer\n format: int32\n shipDate:\n type: string\n format: date-time\n status:\n type: string\n description: Order Status\n enum:\n - placed\n - approved\n - delivered\n complete:\n type: boolean\n default: false\n xml:\n name: Order\n User:\n type: object\n properties:\n id:\n type: integer\n format: int64\n username:\n type: string\n firstName:\n type: string\n lastName:\n type: string\n email:\n type: string\n password:\n type: string\n phone:\n type: string\n userStatus:\n type: integer\n format: int32\n description: User Status\n xml:\n name: User\n Category:\n type: object\n properties:\n id:\n type: integer\n format: int64\n name:\n type: string\n xml:\n name: Category\n Tag:\n type: object\n properties:\n id:\n type: integer\n format: int64\n name:\n type: string\n xml:\n name: Tag\n Pet:\n type: object\n required:\n - name\n - photoUrls\n properties:\n id:\n type: integer\n format: int64\n category:\n $ref: \"#/components/schemas/Category\"\n name:\n type: string\n example: doggie\n photoUrls:\n type: array\n xml:\n name: photoUrl\n wrapped: true\n items:\n type: string\n tags:\n type: array\n xml:\n name: tag\n wrapped: true\n items:\n $ref: \"#/components/schemas/Tag\"\n status:\n type: string\n description: pet status in the store\n enum:\n - available\n - pending\n - sold\n xml:\n name: Pet\n ApiResponse:\n type: object\n properties:\n code:\n type: integer\n format: int32\n type:\n type: string\n message:\n type: string","contentType":"yaml"} +{"_id":"spc_8f743ed2ca184229a62909dd7cf79433","type":"ApiSpec","parentId":"wrk_55a8240d025044d3a40eb40956126bec","modified":1694513791911,"created":1694513471867,"fileName":"(GIT) Remote Design Document","contents":"openapi: 3.0.0\ninfo:\n description: \"This is a sample server Petstore server. You can find out more about\n Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net,\n #swagger](http://swagger.io/irc/). For this sample, you can use the api key\n `special-key` to test the authorization filters.\"\n version: 1.0.2\n title: Swagger Petstore\n termsOfService: http://swagger.io/terms/\n contact:\n email: apiteam@swagger.io\n license:\n name: Apache 2.0\n url: http://www.apache.org/licenses/LICENSE-2.0.html\ntags:\n - name: pet\n description: Everything about your Pets\n externalDocs:\n description: Find out more\n url: http://swagger.io\n - name: store\n description: Access to Petstore orders\n - name: user\n description: Operations about user\n externalDocs:\n description: Find out more about our store\n url: http://swagger.io\npaths:\n /pet:\n post:\n tags:\n - pet\n summary: Add a new pet to the store\n description: \"\"\n operationId: addPet\n requestBody:\n $ref: \"#/components/requestBodies/Pet\"\n responses:\n \"405\":\n description: Invalid input\n put:\n tags:\n - pet\n summary: Update an existing pet\n description: \"\"\n operationId: updatePet\n requestBody:\n $ref: \"#/components/requestBodies/Pet\"\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n \"405\":\n description: Validation exception\n /pet/findByStatus:\n get:\n tags:\n - pet\n summary: Finds Pets by status\n description: Multiple status values can be provided with comma separated strings\n operationId: findPetsByStatus\n parameters:\n - name: status\n in: query\n description: Status values that need to be considered for filter\n required: true\n explode: true\n schema:\n type: array\n items:\n type: string\n enum:\n - available\n - pending\n - sold\n default: available\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid status value\n /pet/findByTags:\n get:\n tags:\n - pet\n summary: Finds Pets by tags\n description: Multiple tags can be provided with comma separated strings. Use tag1,\n tag2, tag3 for testing.\n operationId: findPetsByTags\n parameters:\n - name: tags\n in: query\n description: Tags to filter by\n required: true\n explode: true\n schema:\n type: array\n items:\n type: string\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid tag value\n deprecated: true\n \"/pet/{petId}\":\n get:\n tags:\n - pet\n summary: Find pet by ID\n description: Returns a single pet\n operationId: getPetById\n parameters:\n - name: petId\n in: path\n description: ID of pet to return\n required: true\n schema:\n type: integer\n format: int64\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n post:\n tags:\n - pet\n summary: Updates a pet in the store with form data\n description: \"\"\n operationId: updatePetWithForm\n parameters:\n - name: petId\n in: path\n description: ID of pet that needs to be updated\n required: true\n schema:\n type: integer\n format: int64\n requestBody:\n content:\n application/x-www-form-urlencoded:\n schema:\n type: object\n properties:\n name:\n description: Updated name of the pet\n type: string\n status:\n description: Updated status of the pet\n type: string\n responses:\n \"405\":\n description: Invalid input\n delete:\n tags:\n - pet\n summary: Deletes a pet\n description: \"\"\n operationId: deletePet\n parameters:\n - name: api_key\n in: header\n required: false\n schema:\n type: string\n - name: petId\n in: path\n description: Pet id to delete\n required: true\n schema:\n type: integer\n format: int64\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n /store/inventory:\n get:\n tags:\n - store\n summary: Returns pet inventories by status\n description: Returns a map of status codes to quantities\n operationId: getInventory\n responses:\n \"200\":\n description: successful operation\n content:\n application/json:\n schema:\n type: object\n additionalProperties:\n type: integer\n format: int32\n /store/order:\n post:\n tags:\n - store\n summary: Place an order for a pet\n description: \"\"\n operationId: placeOrder\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n description: order placed for purchasing the pet\n required: true\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Order\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n \"400\":\n description: Invalid Order\n \"/store/order/{orderId}\":\n get:\n tags:\n - store\n summary: Find purchase order by ID\n description: For valid response try integer IDs with value >= 1 and <= 10. Other\n values will generated exceptions\n operationId: getOrderById\n parameters:\n - name: orderId\n in: path\n description: ID of pet that needs to be fetched\n required: true\n schema:\n type: integer\n format: int64\n minimum: 1\n maximum: 10\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Order\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Order not found\n delete:\n tags:\n - store\n summary: Delete purchase order by ID\n description: For valid response try integer IDs with positive integer value. Negative\n or non-integer values will generate API errors\n operationId: deleteOrder\n parameters:\n - name: orderId\n in: path\n description: ID of the order that needs to be deleted\n required: true\n schema:\n type: integer\n format: int64\n minimum: 1\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Order not found\n /user:\n post:\n tags:\n - user\n summary: Create user\n description: This can only be done by the logged in user.\n operationId: createUser\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n description: Created user object\n required: true\n responses:\n default:\n description: successful operation\n /user/createWithArray:\n post:\n tags:\n - user\n summary: Creates list of users with given input array\n description: \"\"\n operationId: createUsersWithArrayInput\n requestBody:\n $ref: \"#/components/requestBodies/UserArray\"\n responses:\n default:\n description: successful operation\n /user/createWithList:\n post:\n tags:\n - user\n summary: Creates list of users with given input array\n description: \"\"\n operationId: createUsersWithListInput\n requestBody:\n $ref: \"#/components/requestBodies/UserArray\"\n responses:\n default:\n description: successful operation\n /user/login:\n get:\n tags:\n - user\n summary: Logs user into the system\n description: \"\"\n operationId: loginUser\n parameters:\n - name: username\n in: query\n description: The user name for login\n required: true\n schema:\n type: string\n - name: password\n in: query\n description: The password for login in clear text\n required: true\n schema:\n type: string\n responses:\n \"200\":\n description: successful operation\n headers:\n X-Rate-Limit:\n description: calls per hour allowed by the user\n schema:\n type: integer\n format: int32\n X-Expires-After:\n description: date in UTC when token expires\n schema:\n type: string\n format: date-time\n content:\n application/xml:\n schema:\n type: string\n application/json:\n schema:\n type: string\n \"400\":\n description: Invalid username/password supplied\n /user/logout:\n get:\n tags:\n - user\n summary: Logs out current logged in user session\n description: \"\"\n operationId: logoutUser\n responses:\n default:\n description: successful operation\n \"/user/{username}\":\n get:\n tags:\n - user\n summary: Get user by user name\n description: \"\"\n operationId: getUserByName\n parameters:\n - name: username\n in: path\n description: \"The name that needs to be fetched. Use user1 for testing. \"\n required: true\n schema:\n type: string\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/User\"\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n \"400\":\n description: Invalid username supplied\n \"404\":\n description: User not found\n put:\n tags:\n - user\n summary: Updated user\n description: This can only be done by the logged in user.\n operationId: updateUser\n parameters:\n - name: username\n in: path\n description: name that need to be updated\n required: true\n schema:\n type: string\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n description: Updated user object\n required: true\n responses:\n \"400\":\n description: Invalid user supplied\n \"404\":\n description: User not found\n delete:\n tags:\n - user\n summary: Delete user\n description: This can only be done by the logged in user.\n operationId: deleteUser\n parameters:\n - name: username\n in: path\n description: The name that needs to be deleted\n required: true\n schema:\n type: string\n responses:\n \"400\":\n description: Invalid username supplied\n \"404\":\n description: User not found\nexternalDocs:\n description: Find out more about Swagger\n url: http://swagger.io\nservers:\n - url: https://petstore.swagger.io/v2\ncomponents:\n requestBodies:\n UserArray:\n content:\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/User\"\n description: List of user object\n required: true\n Pet:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/Pet\"\n application/xml:\n schema:\n $ref: \"#/components/schemas/Pet\"\n description: Pet object that needs to be added to the store\n required: true\n schemas:\n Order:\n type: object\n properties:\n id:\n type: integer\n format: int64\n petId:\n type: integer\n format: int64\n quantity:\n type: integer\n format: int32\n shipDate:\n type: string\n format: date-time\n status:\n type: string\n description: Order Status\n enum:\n - placed\n - approved\n - delivered\n complete:\n type: boolean\n default: false\n xml:\n name: Order\n User:\n type: object\n properties:\n id:\n type: integer\n format: int64\n username:\n type: string\n firstName:\n type: string\n lastName:\n type: string\n email:\n type: string\n password:\n type: string\n phone:\n type: string\n userStatus:\n type: integer\n format: int32\n description: User Status\n xml:\n name: User\n Category:\n type: object\n properties:\n id:\n type: integer\n format: int64\n name:\n type: string\n xml:\n name: Category\n Tag:\n type: object\n properties:\n id:\n type: integer\n format: int64\n name:\n type: string\n xml:\n name: Tag\n Pet:\n type: object\n required:\n - name\n - photoUrls\n properties:\n id:\n type: integer\n format: int64\n category:\n $ref: \"#/components/schemas/Category\"\n name:\n type: string\n example: doggie\n photoUrls:\n type: array\n xml:\n name: photoUrl\n wrapped: true\n items:\n type: string\n tags:\n type: array\n xml:\n name: tag\n wrapped: true\n items:\n $ref: \"#/components/schemas/Tag\"\n status:\n type: string\n description: pet status in the store\n enum:\n - available\n - pending\n - sold\n xml:\n name: Pet\n ApiResponse:\n type: object\n properties:\n code:\n type: integer\n format: int32\n type:\n type: string\n message:\n type: string","contentType":"yaml"} +{"_id":"spc_92e496ca75c54aee9a5a05d49053d1e7","type":"ApiSpec","parentId":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","modified":1694513770535,"created":1694513741499,"fileName":"Remote Design Document","contents":"openapi: 3.0.0\ninfo:\n description: \"This is a sample server Petstore server. You can find out more about\n Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net,\n #swagger](http://swagger.io/irc/). For this sample, you can use the api key\n `special-key` to test the authorization filters.\"\n version: 1.0.2\n title: Swagger Petstore\n termsOfService: http://swagger.io/terms/\n contact:\n email: apiteam@swagger.io\n license:\n name: Apache 2.0\n url: http://www.apache.org/licenses/LICENSE-2.0.html\ntags:\n - name: pet\n description: Everything about your Pets\n externalDocs:\n description: Find out more\n url: http://swagger.io\n - name: store\n description: Access to Petstore orders\n - name: user\n description: Operations about user\n externalDocs:\n description: Find out more about our store\n url: http://swagger.io\npaths:\n /pet:\n post:\n tags:\n - pet\n summary: Add a new pet to the store\n description: \"\"\n operationId: addPet\n requestBody:\n $ref: \"#/components/requestBodies/Pet\"\n responses:\n \"405\":\n description: Invalid input\n put:\n tags:\n - pet\n summary: Update an existing pet\n description: \"\"\n operationId: updatePet\n requestBody:\n $ref: \"#/components/requestBodies/Pet\"\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n \"405\":\n description: Validation exception\n /pet/findByStatus:\n get:\n tags:\n - pet\n summary: Finds Pets by status\n description: Multiple status values can be provided with comma separated strings\n operationId: findPetsByStatus\n parameters:\n - name: status\n in: query\n description: Status values that need to be considered for filter\n required: true\n explode: true\n schema:\n type: array\n items:\n type: string\n enum:\n - available\n - pending\n - sold\n default: available\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid status value\n /pet/findByTags:\n get:\n tags:\n - pet\n summary: Finds Pets by tags\n description: Multiple tags can be provided with comma separated strings. Use tag1,\n tag2, tag3 for testing.\n operationId: findPetsByTags\n parameters:\n - name: tags\n in: query\n description: Tags to filter by\n required: true\n explode: true\n schema:\n type: array\n items:\n type: string\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid tag value\n deprecated: true\n \"/pet/{petId}\":\n get:\n tags:\n - pet\n summary: Find pet by ID\n description: Returns a single pet\n operationId: getPetById\n parameters:\n - name: petId\n in: path\n description: ID of pet to return\n required: true\n schema:\n type: integer\n format: int64\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Pet\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Pet\"\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n post:\n tags:\n - pet\n summary: Updates a pet in the store with form data\n description: \"\"\n operationId: updatePetWithForm\n parameters:\n - name: petId\n in: path\n description: ID of pet that needs to be updated\n required: true\n schema:\n type: integer\n format: int64\n requestBody:\n content:\n application/x-www-form-urlencoded:\n schema:\n type: object\n properties:\n name:\n description: Updated name of the pet\n type: string\n status:\n description: Updated status of the pet\n type: string\n responses:\n \"405\":\n description: Invalid input\n delete:\n tags:\n - pet\n summary: Deletes a pet\n description: \"\"\n operationId: deletePet\n parameters:\n - name: api_key\n in: header\n required: false\n schema:\n type: string\n - name: petId\n in: path\n description: Pet id to delete\n required: true\n schema:\n type: integer\n format: int64\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Pet not found\n /store/inventory:\n get:\n tags:\n - store\n summary: Returns pet inventories by status\n description: Returns a map of status codes to quantities\n operationId: getInventory\n responses:\n \"200\":\n description: successful operation\n content:\n application/json:\n schema:\n type: object\n additionalProperties:\n type: integer\n format: int32\n /store/order:\n post:\n tags:\n - store\n summary: Place an order for a pet\n description: \"\"\n operationId: placeOrder\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n description: order placed for purchasing the pet\n required: true\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Order\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n \"400\":\n description: Invalid Order\n \"/store/order/{orderId}\":\n get:\n tags:\n - store\n summary: Find purchase order by ID\n description: For valid response try integer IDs with value >= 1 and <= 10. Other\n values will generated exceptions\n operationId: getOrderById\n parameters:\n - name: orderId\n in: path\n description: ID of pet that needs to be fetched\n required: true\n schema:\n type: integer\n format: int64\n minimum: 1\n maximum: 10\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/Order\"\n application/json:\n schema:\n $ref: \"#/components/schemas/Order\"\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Order not found\n delete:\n tags:\n - store\n summary: Delete purchase order by ID\n description: For valid response try integer IDs with positive integer value. Negative\n or non-integer values will generate API errors\n operationId: deleteOrder\n parameters:\n - name: orderId\n in: path\n description: ID of the order that needs to be deleted\n required: true\n schema:\n type: integer\n format: int64\n minimum: 1\n responses:\n \"400\":\n description: Invalid ID supplied\n \"404\":\n description: Order not found\n /user:\n post:\n tags:\n - user\n summary: Create user\n description: This can only be done by the logged in user.\n operationId: createUser\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n description: Created user object\n required: true\n responses:\n default:\n description: successful operation\n /user/createWithArray:\n post:\n tags:\n - user\n summary: Creates list of users with given input array\n description: \"\"\n operationId: createUsersWithArrayInput\n requestBody:\n $ref: \"#/components/requestBodies/UserArray\"\n responses:\n default:\n description: successful operation\n /user/createWithList:\n post:\n tags:\n - user\n summary: Creates list of users with given input array\n description: \"\"\n operationId: createUsersWithListInput\n requestBody:\n $ref: \"#/components/requestBodies/UserArray\"\n responses:\n default:\n description: successful operation\n /user/login:\n get:\n tags:\n - user\n summary: Logs user into the system\n description: \"\"\n operationId: loginUser\n parameters:\n - name: username\n in: query\n description: The user name for login\n required: true\n schema:\n type: string\n - name: password\n in: query\n description: The password for login in clear text\n required: true\n schema:\n type: string\n responses:\n \"200\":\n description: successful operation\n headers:\n X-Rate-Limit:\n description: calls per hour allowed by the user\n schema:\n type: integer\n format: int32\n X-Expires-After:\n description: date in UTC when token expires\n schema:\n type: string\n format: date-time\n content:\n application/xml:\n schema:\n type: string\n application/json:\n schema:\n type: string\n \"400\":\n description: Invalid username/password supplied\n /user/logout:\n get:\n tags:\n - user\n summary: Logs out current logged in user session\n description: \"\"\n operationId: logoutUser\n responses:\n default:\n description: successful operation\n \"/user/{username}\":\n get:\n tags:\n - user\n summary: Get user by user name\n description: \"\"\n operationId: getUserByName\n parameters:\n - name: username\n in: path\n description: \"The name that needs to be fetched. Use user1 for testing. \"\n required: true\n schema:\n type: string\n responses:\n \"200\":\n description: successful operation\n content:\n application/xml:\n schema:\n $ref: \"#/components/schemas/User\"\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n \"400\":\n description: Invalid username supplied\n \"404\":\n description: User not found\n put:\n tags:\n - user\n summary: Updated user\n description: This can only be done by the logged in user.\n operationId: updateUser\n parameters:\n - name: username\n in: path\n description: name that need to be updated\n required: true\n schema:\n type: string\n requestBody:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/User\"\n description: Updated user object\n required: true\n responses:\n \"400\":\n description: Invalid user supplied\n \"404\":\n description: User not found\n delete:\n tags:\n - user\n summary: Delete user\n description: This can only be done by the logged in user.\n operationId: deleteUser\n parameters:\n - name: username\n in: path\n description: The name that needs to be deleted\n required: true\n schema:\n type: string\n responses:\n \"400\":\n description: Invalid username supplied\n \"404\":\n description: User not found\nexternalDocs:\n description: Find out more about Swagger\n url: http://swagger.io\nservers:\n - url: https://petstore.swagger.io/v2\ncomponents:\n requestBodies:\n UserArray:\n content:\n application/json:\n schema:\n type: array\n items:\n $ref: \"#/components/schemas/User\"\n description: List of user object\n required: true\n Pet:\n content:\n application/json:\n schema:\n $ref: \"#/components/schemas/Pet\"\n application/xml:\n schema:\n $ref: \"#/components/schemas/Pet\"\n description: Pet object that needs to be added to the store\n required: true\n schemas:\n Order:\n type: object\n properties:\n id:\n type: integer\n format: int64\n petId:\n type: integer\n format: int64\n quantity:\n type: integer\n format: int32\n shipDate:\n type: string\n format: date-time\n status:\n type: string\n description: Order Status\n enum:\n - placed\n - approved\n - delivered\n complete:\n type: boolean\n default: false\n xml:\n name: Order\n User:\n type: object\n properties:\n id:\n type: integer\n format: int64\n username:\n type: string\n firstName:\n type: string\n lastName:\n type: string\n email:\n type: string\n password:\n type: string\n phone:\n type: string\n userStatus:\n type: integer\n format: int32\n description: User Status\n xml:\n name: User\n Category:\n type: object\n properties:\n id:\n type: integer\n format: int64\n name:\n type: string\n xml:\n name: Category\n Tag:\n type: object\n properties:\n id:\n type: integer\n format: int64\n name:\n type: string\n xml:\n name: Tag\n Pet:\n type: object\n required:\n - name\n - photoUrls\n properties:\n id:\n type: integer\n format: int64\n category:\n $ref: \"#/components/schemas/Category\"\n name:\n type: string\n example: doggie\n photoUrls:\n type: array\n xml:\n name: photoUrl\n wrapped: true\n items:\n type: string\n tags:\n type: array\n xml:\n name: tag\n wrapped: true\n items:\n $ref: \"#/components/schemas/Tag\"\n status:\n type: string\n description: pet status in the store\n enum:\n - available\n - pending\n - sold\n xml:\n name: Pet\n ApiResponse:\n type: object\n properties:\n code:\n type: integer\n format: int32\n type:\n type: string\n message:\n type: string","contentType":"yaml"} +{"_id":"spc_2e8929148e664a46a2d442a9614a03fa","type":"ApiSpec","parentId":"wrk_d783eca2fa46457c9f02a221ec862458","modified":1694513952923,"created":1694513890776,"fileName":"Local Design Document","contents":"","contentType":"yaml"} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.CaCertificate.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.CaCertificate.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ClientCertificate.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ClientCertificate.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.CookieJar.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.CookieJar.db new file mode 100644 index 0000000000..5d7339d6eb --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.CookieJar.db @@ -0,0 +1,8 @@ +{"_id":"jar_477c547643674a53b75ec94371a59119","type":"CookieJar","parentId":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","modified":1694513741482,"created":1694513741482,"name":"Default Jar","cookies":[]} +{"_id":"jar_5715ba490d323a1c17eddd5670b87527f86c23df","type":"CookieJar","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436050045,"created":1694436050045,"name":"Default Jar","cookies":[]} +{"_id":"jar_727e68a0650a9198b46e09fe922ebdf2c4b14ee9","type":"CookieJar","parentId":"wrk_f3246c005c9d48ed9e35724415c962d6","modified":1694435944034,"created":1694435944034,"name":"Default Jar","cookies":[]} +{"_id":"jar_7b77a9be2508777ca03ff78df0ae609fc5f582bd","type":"CookieJar","parentId":"wrk_3f71ff5dcf5343169b9a67236f9deafe","modified":1694513691440,"created":1694513691440,"name":"Default Jar","cookies":[]} +{"_id":"jar_ab007691f15cfc166d22b053624849abab40c5f7","type":"CookieJar","parentId":"wrk_ef815fde95ce489aa225fc4408a04bf7","modified":1694436238621,"created":1694436238621,"name":"Default Jar","cookies":[]} +{"_id":"jar_b289a3371fe1228038a8a1df06cc2d0934f6b4f3","type":"CookieJar","parentId":"wrk_55a8240d025044d3a40eb40956126bec","modified":1694513468821,"created":1694513468821,"name":"Default Jar","cookies":[]} +{"_id":"jar_ce5c6ddfabc8dac33df7b4d14509356c9d280eb8","type":"CookieJar","parentId":"wrk_7c9f917b97cb4a43bb402ace6df1b107","modified":1694513835062,"created":1694513835062,"name":"Default Jar","cookies":[]} +{"_id":"jar_dfc984a037a5edccac6000064716590ad1628ad9","type":"CookieJar","parentId":"wrk_d783eca2fa46457c9f02a221ec862458","modified":1694513890792,"created":1694513890792,"name":"Default Jar","cookies":[]} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Environment.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Environment.db new file mode 100644 index 0000000000..f67a6810c9 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Environment.db @@ -0,0 +1,12 @@ +{"_id":"env_05e568304a74414b8b19bec3bae0e38d","type":"Environment","parentId":"env_5715ba490d323a1c17eddd5670b87527f86c23df","modified":1694436082762,"created":1694436082762,"name":"OpenAPI env petstore.swagger.io","data":{"scheme":"https","base_path":"/v2","host":"petstore.swagger.io"},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694436082762} +{"_id":"env_284ec449cf894ffd958b9b54ad598c02","type":"Environment","parentId":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","modified":1694513741462,"created":1694513741462,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694513468817} +{"_id":"env_5715ba490d323a1c17eddd5670b87527f86c23df","type":"Environment","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436050043,"created":1694436050043,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694436050043} +{"_id":"env_727e68a0650a9198b46e09fe922ebdf2c4b14ee9","type":"Environment","parentId":"wrk_f3246c005c9d48ed9e35724415c962d6","modified":1694436015076,"created":1694435944033,"name":"Base Environment","data":{"planet":"earth"},"dataPropertyOrder":{"&":["planet"]},"color":null,"isPrivate":false,"metaSortKey":1694435944033} +{"_id":"env_7b77a9be2508777ca03ff78df0ae609fc5f582bd","type":"Environment","parentId":"wrk_3f71ff5dcf5343169b9a67236f9deafe","modified":1694513691435,"created":1694513691435,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694513691435} +{"_id":"env_84795a9d18884f0fa11582cc88c0f206","type":"Environment","parentId":"env_b289a3371fe1228038a8a1df06cc2d0934f6b4f3","modified":1694513478190,"created":1694513478190,"name":"OpenAPI env petstore.swagger.io","data":{"scheme":"https","base_path":"/v2","host":"petstore.swagger.io"},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694513478190} +{"_id":"env_ab007691f15cfc166d22b053624849abab40c5f7","type":"Environment","parentId":"wrk_ef815fde95ce489aa225fc4408a04bf7","modified":1694436238604,"created":1628147455088,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1628147455088} +{"_id":"env_ae8fb881d7f14abbb60c27c82dcf6072","type":"Environment","parentId":"env_727e68a0650a9198b46e09fe922ebdf2c4b14ee9","modified":1694436005646,"created":1694435987858,"name":"Mars","data":{"planet":"mars"},"dataPropertyOrder":{"&":["planet"]},"color":null,"isPrivate":false,"metaSortKey":1694435987858} +{"_id":"env_b289a3371fe1228038a8a1df06cc2d0934f6b4f3","type":"Environment","parentId":"wrk_55a8240d025044d3a40eb40956126bec","modified":1694513468817,"created":1694513468817,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694513468817} +{"_id":"env_ce5c6ddfabc8dac33df7b4d14509356c9d280eb8","type":"Environment","parentId":"wrk_7c9f917b97cb4a43bb402ace6df1b107","modified":1694513835049,"created":1694513835049,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694513835050} +{"_id":"env_dfc984a037a5edccac6000064716590ad1628ad9","type":"Environment","parentId":"wrk_d783eca2fa46457c9f02a221ec862458","modified":1694513890780,"created":1694513890780,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694513890780} +{"_id":"env_e123dd1aa0b14e27b25f162787fa90d6","type":"Environment","parentId":"env_284ec449cf894ffd958b9b54ad598c02","modified":1694513741464,"created":1694513741464,"name":"OpenAPI env petstore.swagger.io","data":{"scheme":"https","base_path":"/v2","host":"petstore.swagger.io"},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1694513478190} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.GitRepository.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.GitRepository.db new file mode 100644 index 0000000000..fad0b10d6d --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.GitRepository.db @@ -0,0 +1,4 @@ +{"_id":"git_1fded700bfb74aa483560e116866d25e","type":"GitRepository","parentId":null,"modified":1694436298203,"created":1694436297415,"needsFullClone":false,"uri":"https://github.com/gatzjames/insomnia-git-example.git","credentials":{"username":"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","token":"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","oauth2format":"github"},"author":{"name":"gatzjames","email":"jamesgatzos@gmail.com"},"uriNeedsMigration":false} +{"_id":"git_82bf4ce708514ba4a61e5a23a644e0d7","type":"GitRepository","parentId":null,"modified":1694513652868,"created":1694513651705,"needsFullClone":false,"uri":"https://github.com/gatzjames/insomnia-git-example.git","credentials":{"username":"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","token":"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","oauth2format":"github"},"author":{"name":"gatzjames","email":"jamesgatzos@gmail.com"},"uriNeedsMigration":false} +{"_id":"git_a02b558275414a518e6107c3e85e405a","type":"GitRepository","parentId":null,"modified":1694513701781,"created":1694513700687,"needsFullClone":false,"uri":"https://github.com/gatzjames/insomnia-git-example.git","credentials":{"username":"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","token":"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","oauth2format":"github"},"author":{"name":"gatzjames","email":"jamesgatzos@gmail.com"},"uriNeedsMigration":false} +{"_id":"git_da0217efdd80460faa2f153c1e871083","type":"GitRepository","parentId":null,"modified":1694513852485,"created":1694513851549,"needsFullClone":false,"uri":"https://github.com/gatzjames/insomnia-git-example.git","credentials":{"username":"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","token":"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","oauth2format":"github"},"author":{"name":"gatzjames","email":"jamesgatzos@gmail.com"},"uriNeedsMigration":false} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.GrpcRequest.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.GrpcRequest.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.GrpcRequestMeta.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.GrpcRequestMeta.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.OAuth2Token.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.OAuth2Token.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.PluginData.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.PluginData.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Project.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Project.db new file mode 100644 index 0000000000..95acd76119 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Project.db @@ -0,0 +1,5 @@ +{"_id":"proj_default-project","type":"Project","parentId":null,"modified":1694435924693,"created":1694435924693,"name":"Insomnia","remoteId":null} +{"_id":"proj_f5359773e8b544c2bfeec86c95b978dc","type":"Project","parentId":null,"modified":1694513815752,"created":1694436032201,"name":"Local Project (GIT)","remoteId":null} +{"_id":"proj_777cc16ca5f3419fbc262df283d27bdf","type":"Project","parentId":null,"modified":1694513815752,"created":1694436032201,"name":"Local Project","remoteId":null} +{"_id":"proj_scratchpad","type":"Project","parentId":null,"modified":1694513815752,"created":1694436032201,"name":"Scratchpad","remoteId":null} +{"_id":"proj_team_195a6ce0edb1427eb2e8ba7b986072e4","type":"Project","parentId":null,"modified":1694513929456,"created":1694513929456,"name":"🪄 Legacy Remote Project","remoteId":"team_195a6ce0edb1427eb2e8ba7b986072e4"} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ProtoDirectory.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ProtoDirectory.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ProtoFile.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.ProtoFile.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Request.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Request.db new file mode 100644 index 0000000000..1c5793e96b --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Request.db @@ -0,0 +1,59 @@ +{"_id":"req_074eb9af1eb04f7da98424aff4f80b50","type":"Request","parentId":"fld_f5d684c7cb994f0abddd2af7b2574369","modified":1694513741665,"created":1694513741665,"url":"{{ _.base_url }}/user/login","name":"Logs user into the system","description":"","method":"GET","body":{},"parameters":[{"name":"username","disabled":false,"value":"string"},{"name":"password","disabled":false,"value":"string"}],"headers":[],"authentication":{},"metaSortKey":-1694513478225,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_0c44ae04191b4b26af4a1c4231f3ca26","type":"Request","parentId":"fld_41731ba5147d4132b0969fe7e1a1aff3","modified":1694436082789,"created":1694436082789,"url":"{{ _.base_url }}/user/createWithArray","name":"Creates list of users with given input array","description":"","method":"POST","body":{"mimeType":"application/json","text":"[\n {\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n }\n]"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694436082789,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_0d40d2c37e0c4c31af63098803aa6da9","type":"Request","parentId":"fld_cf6d9eae350248a1afac20cce84c80a4","modified":1694513741540,"created":1694513741540,"url":"{{ _.base_url }}/pet/findByTags","name":"Finds Pets by tags","description":"","method":"GET","body":{},"parameters":[{"name":"tags","disabled":false,"value":"string"}],"headers":[],"authentication":{},"metaSortKey":-1694513478202,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_13512e54128346f3ab1d67ad0494fa56","type":"Request","parentId":"fld_fd2217f91f9a48b6b60ba1ecdf21ddef","modified":1694513478197,"created":1694513478197,"url":"{{ _.base_url }}/pet","name":"Add a new pet to the store","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"string\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478198,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_145f3154cdcf4a77a8c79a848467704a","type":"Request","parentId":"fld_f5d684c7cb994f0abddd2af7b2574369","modified":1694513741679,"created":1694513741679,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Get user by user name","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478232,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_163e848a55f94c8096d53a70d5ca6ef9","type":"Request","parentId":"fld_0dc862678d1b4b62b519d4ebabccf25f","modified":1694436154213,"created":1694436082780,"url":"{{ _.base_url }}/pet/findByStatus","name":"Finds Pets by status","description":"","method":"GET","body":{},"parameters":[{"name":"status","disabled":false,"value":"available"}],"headers":[],"authentication":{},"metaSortKey":-1694436082780,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_1727645245ea4a8a9a485d3af1f8d2f3","type":"Request","parentId":"fld_fd2217f91f9a48b6b60ba1ecdf21ddef","modified":1694513478207,"created":1694513478207,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Deletes a pet","description":"","method":"DELETE","body":{},"parameters":[],"headers":[{"name":"api_key","disabled":true,"value":"string"}],"authentication":{},"metaSortKey":-1694513478207,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_192c75d1b70a47c0bbdb38a2ec592e5d","type":"Request","parentId":"fld_15d587ccb28a4438a0c4c1e42b3aebfe","modified":1694513741600,"created":1694513741600,"url":"{{ _.base_url }}/store/order","name":"Place an order for a pet","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"petId\": 0,\n \"quantity\": 0,\n \"shipDate\": \"2023-09-12T10:11:18.185Z\",\n \"status\": \"string\",\n \"complete\": true\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478211,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_1ca5010386a74c19ad9606d4258998fa","type":"Request","parentId":"fld_cf6d9eae350248a1afac20cce84c80a4","modified":1694513741524,"created":1694513741524,"url":"{{ _.base_url }}/pet","name":"Update an existing pet","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"string\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478200,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_21d2291e0e944359bc57db4d4fb900bc","type":"Request","parentId":"fld_f5d684c7cb994f0abddd2af7b2574369","modified":1694513741673,"created":1694513741673,"url":"{{ _.base_url }}/user/logout","name":"Logs out current logged in user session","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478229,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_2b30b68997304e1ab09fdbe2d3cc3767","type":"Request","parentId":"fld_0dc862678d1b4b62b519d4ebabccf25f","modified":1694436082781,"created":1694436082781,"url":"{{ _.base_url }}/pet/findByTags","name":"Finds Pets by tags","description":"","method":"GET","body":{},"parameters":[{"name":"tags","disabled":false,"value":"string"}],"headers":[],"authentication":{},"metaSortKey":-1694436082781,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_2b8a18c434654b2cb7b7d1a7d5000fdf","type":"Request","parentId":"fld_15d587ccb28a4438a0c4c1e42b3aebfe","modified":1694513741593,"created":1694513741593,"url":"{{ _.base_url }}/store/inventory","name":"Returns pet inventories by status","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478208,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_2c5dc56e4f2c438a89dbb8f7f74ed345","type":"Request","parentId":"fld_cf6d9eae350248a1afac20cce84c80a4","modified":1694513741565,"created":1694513741565,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Deletes a pet","description":"","method":"DELETE","body":{},"parameters":[],"headers":[{"name":"api_key","disabled":true,"value":"string"}],"authentication":{},"metaSortKey":-1694513478207,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_3049c3a7c24245c986b29e80ad5a44c6","type":"Request","parentId":"wrk_f3246c005c9d48ed9e35724415c962d6","modified":1694435970569,"created":1694435946705,"url":"https://mockbin.org/status/200","name":"Get list of rockets","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnia/2023.5.8"}],"authentication":{},"metaSortKey":-1694435946705,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_32194eff4bfb4fa097d9a390eff6a775","type":"Request","parentId":"fld_f5d684c7cb994f0abddd2af7b2574369","modified":1694513741684,"created":1694513741684,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Updated user","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478234,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_35eb88c4741948cbae6e9067d19b2f32","type":"Request","parentId":"fld_41731ba5147d4132b0969fe7e1a1aff3","modified":1694436082790,"created":1694436082790,"url":"{{ _.base_url }}/user/createWithList","name":"Creates list of users with given input array","description":"","method":"POST","body":{"mimeType":"application/json","text":"[\n {\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n }\n]"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694436082790,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_3b4ca2e394bb486ab0242ddf03035d38","type":"Request","parentId":"fld_41731ba5147d4132b0969fe7e1a1aff3","modified":1694436082788,"created":1694436082788,"url":"{{ _.base_url }}/user","name":"Create user","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694436082788,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_40504764445b424b9931d242d568a067","type":"Request","parentId":"fld_0dc862678d1b4b62b519d4ebabccf25f","modified":1694436082784,"created":1694436082784,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Deletes a pet","description":"","method":"DELETE","body":{},"parameters":[],"headers":[{"name":"api_key","disabled":true,"value":"string"}],"authentication":{},"metaSortKey":-1694436082784,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_42f11c00991b4d5890af3f8b751f6842","type":"Request","parentId":"fld_e665ff77102645bf888faaa3af3287a9","modified":1694513478215,"created":1694513478215,"url":"{{ _.base_url }}/store/order/{{ _.orderId }}","name":"Find purchase order by ID","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478215,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_45a81258cef8479cbf112d14c44b1da3","type":"Request","parentId":"fld_b6964b1458c749e490815c94816c4ed9","modified":1694436082786,"created":1694436082786,"url":"{{ _.base_url }}/store/order/{{ _.orderId }}","name":"Find purchase order by ID","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694436082786,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_48b9de3ca2f941798e5da7ffadbdd6bd","type":"Request","parentId":"fld_17cc4f5762c5423592d1a384461adf18","modified":1694513478235,"created":1694513478235,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Delete user","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478235,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_4fc95d07f7704bed9dc78bb845ef76e1","type":"Request","parentId":"fld_15d587ccb28a4438a0c4c1e42b3aebfe","modified":1694513741616,"created":1694513741616,"url":"{{ _.base_url }}/store/order/{{ _.orderId }}","name":"Find purchase order by ID","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478215,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_4ffad86de65a4388bdba565cabacee1a","type":"Request","parentId":"fld_0dc862678d1b4b62b519d4ebabccf25f","modified":1694436082782,"created":1694436082782,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Find pet by ID","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694436082782,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_529632695f3e479bbe9229ae2f63c32c","type":"Request","parentId":"fld_15d587ccb28a4438a0c4c1e42b3aebfe","modified":1694513741625,"created":1694513741625,"url":"{{ _.base_url }}/store/order/{{ _.orderId }}","name":"Delete purchase order by ID","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478217,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_5d0f087267a342e1800541654b4ee895","type":"Request","parentId":"fld_f5d684c7cb994f0abddd2af7b2574369","modified":1694513741636,"created":1694513741636,"url":"{{ _.base_url }}/user","name":"Create user","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478219,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_6391c7cb69f84ca6bcd88920875cad4c","type":"Request","parentId":"fld_41731ba5147d4132b0969fe7e1a1aff3","modified":1694436082792,"created":1694436082792,"url":"{{ _.base_url }}/user/logout","name":"Logs out current logged in user session","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694436082792,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_6724a9231d6e40278a7f5d270c40617a","type":"Request","parentId":"fld_e665ff77102645bf888faaa3af3287a9","modified":1694513478208,"created":1694513478208,"url":"{{ _.base_url }}/store/inventory","name":"Returns pet inventories by status","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478208,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_68172cea6e4942a3b914e5ebf1834bcd","type":"Request","parentId":"fld_fd2217f91f9a48b6b60ba1ecdf21ddef","modified":1694513478205,"created":1694513478205,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Updates a pet in the store with form data","description":"","method":"POST","body":{"mimeType":"application/x-www-form-urlencoded"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/x-www-form-urlencoded"}],"authentication":{},"metaSortKey":-1694513478205,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_6945f2841b8b4daf9f6141d7a06c6d62","type":"Request","parentId":"fld_41731ba5147d4132b0969fe7e1a1aff3","modified":1694436082794,"created":1694436082794,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Delete user","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694436082794,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_74bbe9b9290c4bb991e31277d0ca06ce","type":"Request","parentId":"fld_0dc862678d1b4b62b519d4ebabccf25f","modified":1694436082778,"created":1694436082778,"url":"{{ _.base_url }}/pet","name":"Add a new pet to the store","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"string\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694436082778,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_7885c94ca5444575bf245fe35d99bb2b","type":"Request","parentId":"fld_17cc4f5762c5423592d1a384461adf18","modified":1694513478223,"created":1694513478223,"url":"{{ _.base_url }}/user/createWithList","name":"Creates list of users with given input array","description":"","method":"POST","body":{"mimeType":"application/json","text":"[\n {\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n }\n]"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478223,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_7d18e1be691a4b6eb4c048bf2024f42c","type":"Request","parentId":"fld_b6964b1458c749e490815c94816c4ed9","modified":1694436082785,"created":1694436082785,"url":"{{ _.base_url }}/store/order","name":"Place an order for a pet","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"petId\": 0,\n \"quantity\": 0,\n \"shipDate\": \"2023-09-11T12:41:22.746Z\",\n \"status\": \"string\",\n \"complete\": true\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694436082785,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_8a19defaeb904531896c36af722b7489","type":"Request","parentId":"fld_41731ba5147d4132b0969fe7e1a1aff3","modified":1694436082793,"created":1694436082793,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Updated user","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694436082793,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_8c5f57330ff04979b508bdd88d893ebd","type":"Request","parentId":"fld_cf6d9eae350248a1afac20cce84c80a4","modified":1694513741531,"created":1694513741531,"url":"{{ _.base_url }}/pet/findByStatus","name":"Finds Pets by status","description":"","method":"GET","body":{},"parameters":[{"name":"status","disabled":false,"value":"available"}],"headers":[],"authentication":{},"metaSortKey":-1694513478201,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_8f33c838b6d046979d9ceb9b4bece221","type":"Request","parentId":"fld_0dc862678d1b4b62b519d4ebabccf25f","modified":1694436082779,"created":1694436082779,"url":"{{ _.base_url }}/pet","name":"Update an existing pet","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"string\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694436082779,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_916d759fb4d242b6820d934757004499","type":"Request","parentId":"fld_41731ba5147d4132b0969fe7e1a1aff3","modified":1694436082791,"created":1694436082791,"url":"{{ _.base_url }}/user/login","name":"Logs user into the system","description":"","method":"GET","body":{},"parameters":[{"name":"username","disabled":false,"value":"string"},{"name":"password","disabled":false,"value":"string"}],"headers":[],"authentication":{},"metaSortKey":-1694436082791,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_91ff9bd78c0145e1aaddc0c35e8272d7","type":"Request","parentId":"fld_fd2217f91f9a48b6b60ba1ecdf21ddef","modified":1694513478202,"created":1694513478202,"url":"{{ _.base_url }}/pet/findByTags","name":"Finds Pets by tags","description":"","method":"GET","body":{},"parameters":[{"name":"tags","disabled":false,"value":"string"}],"headers":[],"authentication":{},"metaSortKey":-1694513478202,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_941bb4c1300444a8b8f5764bf544a720","type":"Request","parentId":"fld_e665ff77102645bf888faaa3af3287a9","modified":1694513478211,"created":1694513478211,"url":"{{ _.base_url }}/store/order","name":"Place an order for a pet","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"petId\": 0,\n \"quantity\": 0,\n \"shipDate\": \"2023-09-12T10:11:18.185Z\",\n \"status\": \"string\",\n \"complete\": true\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478211,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_99a7c1e90e4845988eabd18e45de05e2","type":"Request","parentId":"fld_17cc4f5762c5423592d1a384461adf18","modified":1694513478232,"created":1694513478232,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Get user by user name","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478232,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_9b14424c0b764e26958fae1f9fd6a908","type":"Request","parentId":"fld_f5d684c7cb994f0abddd2af7b2574369","modified":1694513741693,"created":1694513741693,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Delete user","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478235,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_a6619ad15c1c46209e5d3777d2015536","type":"Request","parentId":"fld_b6964b1458c749e490815c94816c4ed9","modified":1694436082785,"created":1694436082785,"url":"{{ _.base_url }}/store/inventory","name":"Returns pet inventories by status","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694436082785,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_ae7eb298851f48379de12ff46bd9d1ad","type":"Request","parentId":"fld_e665ff77102645bf888faaa3af3287a9","modified":1694513478217,"created":1694513478217,"url":"{{ _.base_url }}/store/order/{{ _.orderId }}","name":"Delete purchase order by ID","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478217,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_b223a210e2b64c42b6ac98afec1e9552","type":"Request","parentId":"fld_fd2217f91f9a48b6b60ba1ecdf21ddef","modified":1694513478200,"created":1694513478200,"url":"{{ _.base_url }}/pet","name":"Update an existing pet","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"string\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478200,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_bc90a18e061a41938f0661ab4fff9a3b","type":"Request","parentId":"fld_b6964b1458c749e490815c94816c4ed9","modified":1694436082787,"created":1694436082787,"url":"{{ _.base_url }}/store/order/{{ _.orderId }}","name":"Delete purchase order by ID","description":"","method":"DELETE","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694436082787,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_c81095cc13414914a581b82f24bedf91","type":"Request","parentId":"fld_f5d684c7cb994f0abddd2af7b2574369","modified":1694513741658,"created":1694513741658,"url":"{{ _.base_url }}/user/createWithList","name":"Creates list of users with given input array","description":"","method":"POST","body":{"mimeType":"application/json","text":"[\n {\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n }\n]"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478223,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_cbe176232cac4dfc9ac2be7302623f03","type":"Request","parentId":"fld_fd2217f91f9a48b6b60ba1ecdf21ddef","modified":1694513478204,"created":1694513478204,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Find pet by ID","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478204,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_d108b051765444faa2ab920f166c8e99","type":"Request","parentId":"fld_f5d684c7cb994f0abddd2af7b2574369","modified":1694513741646,"created":1694513741646,"url":"{{ _.base_url }}/user/createWithArray","name":"Creates list of users with given input array","description":"","method":"POST","body":{"mimeType":"application/json","text":"[\n {\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n }\n]"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478221,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_d8314112ea8b494a9d2954949deef6c2","type":"Request","parentId":"fld_17cc4f5762c5423592d1a384461adf18","modified":1694513478229,"created":1694513478229,"url":"{{ _.base_url }}/user/logout","name":"Logs out current logged in user session","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478229,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_daa311e962424e52a98e51d5ddb4fa5e","type":"Request","parentId":"fld_17cc4f5762c5423592d1a384461adf18","modified":1694513478234,"created":1694513478234,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Updated user","description":"","method":"PUT","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478234,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_dbd2fe72a91946359532fbff416f25d7","type":"Request","parentId":"wrk_ef815fde95ce489aa225fc4408a04bf7","modified":1694436250026,"created":1694436242029,"url":"https://mockbin.org","name":"New Request","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnia/2023.5.8"}],"authentication":{},"metaSortKey":-1694436242029,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_e66296d2c3b6452fb7a425975e045dcf","type":"Request","parentId":"fld_0dc862678d1b4b62b519d4ebabccf25f","modified":1694436082783,"created":1694436082783,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Updates a pet in the store with form data","description":"","method":"POST","body":{"mimeType":"application/x-www-form-urlencoded"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/x-www-form-urlencoded"}],"authentication":{},"metaSortKey":-1694436082783,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_e6c3e9c0bdb847b6a7049e2bd1b43a23","type":"Request","parentId":"fld_41731ba5147d4132b0969fe7e1a1aff3","modified":1694436082792,"created":1694436082792,"url":"{{ _.base_url }}/user/{{ _.username }}","name":"Get user by user name","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694436082792,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_e70fd54fa9284182a776a067d82c2476","type":"Request","parentId":"fld_17cc4f5762c5423592d1a384461adf18","modified":1694513478225,"created":1694513478225,"url":"{{ _.base_url }}/user/login","name":"Logs user into the system","description":"","method":"GET","body":{},"parameters":[{"name":"username","disabled":false,"value":"string"},{"name":"password","disabled":false,"value":"string"}],"headers":[],"authentication":{},"metaSortKey":-1694513478225,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_e75682413e444776a85d228211e38285","type":"Request","parentId":"fld_cf6d9eae350248a1afac20cce84c80a4","modified":1694513741515,"created":1694513741515,"url":"{{ _.base_url }}/pet","name":"Add a new pet to the store","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"category\": {\n \"id\": 0,\n \"name\": \"string\"\n },\n \"name\": \"doggie\",\n \"photoUrls\": [\n \"string\"\n ],\n \"tags\": [\n {\n \"id\": 0,\n \"name\": \"string\"\n }\n ],\n \"status\": \"string\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478198,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_e87d6b3019e44dc797296b6bef80371e","type":"Request","parentId":"fld_17cc4f5762c5423592d1a384461adf18","modified":1694513478221,"created":1694513478221,"url":"{{ _.base_url }}/user/createWithArray","name":"Creates list of users with given input array","description":"","method":"POST","body":{"mimeType":"application/json","text":"[\n {\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n }\n]"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478221,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_e972c7bc07bc42f7862b4d0888bbb7b5","type":"Request","parentId":"fld_cf6d9eae350248a1afac20cce84c80a4","modified":1694513741546,"created":1694513741546,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Find pet by ID","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1694513478204,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_f2a1313381bf471cb3e4e4d95bd67c9f","type":"Request","parentId":"fld_17cc4f5762c5423592d1a384461adf18","modified":1694513478219,"created":1694513478219,"url":"{{ _.base_url }}/user","name":"Create user","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n \"id\": 0,\n \"username\": \"string\",\n \"firstName\": \"string\",\n \"lastName\": \"string\",\n \"email\": \"string\",\n \"password\": \"string\",\n \"phone\": \"string\",\n \"userStatus\": 0\n}"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/json"}],"authentication":{},"metaSortKey":-1694513478219,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_f9f57fb269a04e9d8b810b988ce715d6","type":"Request","parentId":"fld_cf6d9eae350248a1afac20cce84c80a4","modified":1694513741557,"created":1694513741557,"url":"{{ _.base_url }}/pet/{{ _.petId }}","name":"Updates a pet in the store with form data","description":"","method":"POST","body":{"mimeType":"application/x-www-form-urlencoded"},"parameters":[],"headers":[{"name":"Content-Type","disabled":false,"value":"application/x-www-form-urlencoded"}],"authentication":{},"metaSortKey":-1694513478205,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} +{"_id":"req_fd68a98e5f2a4a61b7d1552aa083e12a","type":"Request","parentId":"fld_fd2217f91f9a48b6b60ba1ecdf21ddef","modified":1694513478201,"created":1694513478201,"url":"{{ _.base_url }}/pet/findByStatus","name":"Finds Pets by status","description":"","method":"GET","body":{},"parameters":[{"name":"status","disabled":false,"value":"available"}],"headers":[],"authentication":{},"metaSortKey":-1694513478201,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global"} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestGroup.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestGroup.db new file mode 100644 index 0000000000..f44fb8b32a --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestGroup.db @@ -0,0 +1,9 @@ +{"_id":"fld_0dc862678d1b4b62b519d4ebabccf25f","type":"RequestGroup","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436082773,"created":1694436082773,"name":"pet","description":"Everything about your Pets","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694436082773} +{"_id":"fld_15d587ccb28a4438a0c4c1e42b3aebfe","type":"RequestGroup","parentId":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","modified":1694513741584,"created":1694513741584,"name":"store","description":"Access to Petstore orders","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694513478194} +{"_id":"fld_17cc4f5762c5423592d1a384461adf18","type":"RequestGroup","parentId":"wrk_55a8240d025044d3a40eb40956126bec","modified":1694513478196,"created":1694513478196,"name":"user","description":"Operations about user","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694513478196} +{"_id":"fld_41731ba5147d4132b0969fe7e1a1aff3","type":"RequestGroup","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436082777,"created":1694436082777,"name":"user","description":"Operations about user","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694436082777} +{"_id":"fld_b6964b1458c749e490815c94816c4ed9","type":"RequestGroup","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436082775,"created":1694436082775,"name":"store","description":"Access to Petstore orders","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694436082775} +{"_id":"fld_cf6d9eae350248a1afac20cce84c80a4","type":"RequestGroup","parentId":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","modified":1694513741509,"created":1694513741509,"name":"pet","description":"Everything about your Pets","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694513478192} +{"_id":"fld_e665ff77102645bf888faaa3af3287a9","type":"RequestGroup","parentId":"wrk_55a8240d025044d3a40eb40956126bec","modified":1694513478194,"created":1694513478194,"name":"store","description":"Access to Petstore orders","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694513478194} +{"_id":"fld_f5d684c7cb994f0abddd2af7b2574369","type":"RequestGroup","parentId":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","modified":1694513741632,"created":1694513741632,"name":"user","description":"Operations about user","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694513478196} +{"_id":"fld_fd2217f91f9a48b6b60ba1ecdf21ddef","type":"RequestGroup","parentId":"wrk_55a8240d025044d3a40eb40956126bec","modified":1694513478192,"created":1694513478192,"name":"pet","description":"Everything about your Pets","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1694513478192} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestGroupMeta.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestGroupMeta.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestMeta.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestMeta.db new file mode 100644 index 0000000000..e7c1fa4676 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestMeta.db @@ -0,0 +1,3 @@ +{"_id":"reqm_3d151304a57144e7aec52dfe22227fcb","type":"RequestMeta","parentId":"req_3049c3a7c24245c986b29e80ad5a44c6","modified":1694513621353,"created":1694435946729,"previewMode":"friendly","responseFilter":"","responseFilterHistory":[],"activeResponseId":"res_12166e333b4245d58f5faa232c348b44","savedRequestBody":{},"pinned":false,"lastActive":1694513621011,"downloadPath":null,"expandedAccordionKeys":{}} +{"_id":"reqm_4cad9f930583499cab22d70531d557b6","type":"RequestMeta","parentId":"req_163e848a55f94c8096d53a70d5ca6ef9","modified":1694513822929,"created":1694436132724,"previewMode":"friendly","responseFilter":"","responseFilterHistory":[],"activeResponseId":null,"savedRequestBody":{},"pinned":false,"lastActive":1694513822927,"downloadPath":null,"expandedAccordionKeys":{}} +{"_id":"reqm_f7d5118cb91d42d1a50f2a78c781f6bc","type":"RequestMeta","parentId":"req_dbd2fe72a91946359532fbff416f25d7","modified":1694513669923,"created":1694436242051,"previewMode":"friendly","responseFilter":"","responseFilterHistory":[],"activeResponseId":"res_bdef9b7f52ba4c91adfd0bd863c70156","savedRequestBody":{},"pinned":false,"lastActive":1694513669922,"downloadPath":null,"expandedAccordionKeys":{}} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestVersion.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestVersion.db new file mode 100644 index 0000000000..fb7d9bb239 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.RequestVersion.db @@ -0,0 +1,2 @@ +{"_id":"rvr_7ff1fa7e4fe64dabbf1d9ba5ba56c79b","type":"RequestVersion","parentId":"req_dbd2fe72a91946359532fbff416f25d7","modified":1694436253725,"created":1694436253725,"compressedRequest":"H4sIAAAAAAAAE21QyU7DMBD9FeRzaFPXCU1u7EJICBU4IVRN4nFj1bWLPaFCVf+dCYtYxHHmzdtmJxZWi1pEfF7oRkuDRxKqSaXKaVEVU2kaY9SkNLLQRyIT9LpBvp7jc4+JeLGBiJ6uBoltXC3QzCaF0VgVLapZBSBlYVql8hnkqjGDxDpoaywyY1JWSk1LWeS5LDPRRgT6uVcyl1Um+uhYvSPapHo8Xod21Vg/CnHJYh7WQ54b3B58Z9KY2mg3ZINnbLBE6sIQ8fL8nscm6FdR7/bv6VmAMCZRPz5lokPQH8PuS/ohYTw8XnJJZr6A64el9SmsvYUxJ5yOitFM7JkNPXV8Z1v4sB4cWB3uQqRrZMvDP81suo32hUuL2oBLmImERNYv7yhEPA1hZZHDUOx/QOj1/8iZTdA4nPMBxs9vnLxX/S1+7tug8WH46i/+HJveOn0L1P1BLoJzYTtHbSO2xL5i6UIDTuzfAN32OFE/AgAA"} +{"_id":"rvr_b9e82de8e4df44a8bfc98cce22e4937d","type":"RequestVersion","parentId":"req_3049c3a7c24245c986b29e80ad5a44c6","modified":1694435971248,"created":1694435971248,"compressedRequest":"H4sIAAAAAAAAE21Ry07rMBD9lcrr0IbESZvsuPdyEWKDCqwQqib2tBnVtYs9AaGq/86EhwSIpec85pzxQa3IqlZFfFyVuW5MCXNT6EJXplnUXdHgIgdbgdamVpnilz0Ke4mPAyaWwR4ier4cLZ7jdrUuC12bPBe11Qu0DZbVvND6VAZ1YUeLXbC0JhTFad1oXVbNPK/qJlMmIvDXua4FydQQnbj3zPvUzma7YLYd+WmIm1li4CHNijwXXw+7MdoF8sRR4klYT6JwkZOAFpOJtGcKXjhjCuQ+jKkvzm/l2QX7otrD8a2QGDHGpNr7h0z1CPb9cfhccZcwnpxtpLcon8AN45B8CjtPIGmKclpNF+ooahi4Fx4ZeF89bhB3uAmRr1BWnvwoS+k60pPcQbVrcAkzlZCZ/OaGQ8S/IWwJJQzH4QuE3v6O/KMEncOlEDB+fNqft6rfzc+9CRbvxkN/0y+xG8jZa+D+B/I/OBeel2gpopETt2rjQgdOHV8B1feGK1ICAAA="} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Response.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Response.db new file mode 100644 index 0000000000..535888252a --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Response.db @@ -0,0 +1,2 @@ +{"_id":"res_12166e333b4245d58f5faa232c348b44","type":"Response","parentId":"req_3049c3a7c24245c986b29e80ad5a44c6","modified":1694435971253,"created":1694435971253,"statusCode":200,"statusMessage":"","httpVersion":"HTTP/2","contentType":"application/json; charset=utf-8","url":"https://mockbin.org/status/200","bytesRead":38,"bytesContent":38,"elapsedTime":381.818,"headers":[{"name":"date","value":"Mon, 11 Sep 2023 12:39:33 GMT"},{"name":"content-type","value":"application/json; charset=utf-8"},{"name":"content-length","value":"38"},{"name":"access-control-allow-origin","value":"*"},{"name":"access-control-allow-methods","value":"GET"},{"name":"access-control-allow-headers","value":"host,connection,accept-encoding,x-forwarded-for,cf-ray,x-forwarded-proto,cf-visitor,user-agent,accept,cdn-loop,cf-connecting-ip,x-request-id,x-forwarded-port,via,connect-time,x-request-start,total-route-time"},{"name":"access-control-allow-credentials","value":"true"},{"name":"x-powered-by","value":"mockbin"},{"name":"vary","value":"Accept, Accept-Encoding"},{"name":"etag","value":"W/\"26-tc4xT7f7mVsEKKLOEWnIg/voxtc\""},{"name":"via","value":"1.1 vegur"},{"name":"cf-cache-status","value":"DYNAMIC"},{"name":"report-to","value":"{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=miPfsyiWpJm9tu2MW7LXgMXvGoMBjgoWrTYaiyni%2FNe%2FhLrnMUMggweKHreYvw3Wdb97m3q1EO1aIq64p0BJVdhiJEEqRvTvYIQADjKPO3wF92BZNnWrP1hZNgDOeQ%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"},{"name":"nel","value":"{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"},{"name":"server","value":"cloudflare"},{"name":"cf-ray","value":"804ff960ffcf0a69-AMS"},{"name":"alt-svc","value":"h3=\":443\"; ma=86400"}],"timelinePath":"/Users/jamesgatz/Library/Application Support/Insomnia/responses/caa4e99f-3aec-418c-a6b8-e235cb9cf100.timeline","bodyPath":"/Users/jamesgatz/Library/Application Support/Insomnia/responses/bb1d4190-8057-4812-b9db-e3073031242d.response","bodyCompression":null,"error":"","requestVersionId":"rvr_b9e82de8e4df44a8bfc98cce22e4937d","settingStoreCookies":true,"settingSendCookies":true,"environmentId":null} +{"_id":"res_bdef9b7f52ba4c91adfd0bd863c70156","type":"Response","parentId":"req_dbd2fe72a91946359532fbff416f25d7","modified":1694436253730,"created":1694436253730,"statusCode":200,"statusMessage":"","httpVersion":"HTTP/2","contentType":"text/html; charset=utf-8","url":"https://mockbin.org/","bytesRead":10696,"bytesContent":10696,"elapsedTime":326.637,"headers":[{"name":"date","value":"Mon, 11 Sep 2023 12:44:15 GMT"},{"name":"content-type","value":"text/html; charset=utf-8"},{"name":"vary","value":"Accept-Encoding"},{"name":"via","value":"1.1 vegur"},{"name":"cf-cache-status","value":"DYNAMIC"},{"name":"report-to","value":"{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=%2FQ93Z%2FUYZDusNDucRzBELssFpK1QckLl728ZkJ%2Fo%2BYuPeBIsT1nhFp6gRkKglylrjA3YgknADybNMr9Cf2moPv%2B8hw%2F80QO0vxVQTTKJlUS9DZSCcK5aUGO9b1X4gQ%3D%3D\"}],\"group\":\"cf-nel\",\"max_age\":604800}"},{"name":"nel","value":"{\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}"},{"name":"server","value":"cloudflare"},{"name":"cf-ray","value":"805000467d910a6c-AMS"},{"name":"alt-svc","value":"h3=\":443\"; ma=86400"}],"timelinePath":"/Users/jamesgatz/Library/Application Support/Insomnia/responses/4551ecf6-4d74-4aae-958a-a9b1b4e5cde0.timeline","bodyPath":"/Users/jamesgatz/Library/Application Support/Insomnia/responses/f34e1b3d-cba5-43de-be7a-3a699ffbde1c.response","bodyCompression":null,"error":"","requestVersionId":"rvr_7ff1fa7e4fe64dabbf1d9ba5ba56c79b","settingStoreCookies":true,"settingSendCookies":true,"environmentId":null} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Settings.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Settings.db new file mode 100644 index 0000000000..bba1507f54 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Settings.db @@ -0,0 +1 @@ +{"_id":"set_67b83b5de9214a388619fa8ef1a199d9","type":"Settings","parentId":null,"modified":1694448071561,"created":1694435923637,"autoDetectColorScheme":false,"autoHideMenuBar":false,"autocompleteDelay":1200,"clearOAuth2SessionOnRestart":true,"darkTheme":"default","deviceId":"722a9204-c1dc-4f08-b0ba-2140e74451df","disableHtmlPreviewJs":false,"disableResponsePreviewLinks":false,"disableUpdateNotification":false,"editorFontSize":11,"editorIndentSize":2,"editorIndentWithTabs":true,"editorKeyMap":"default","editorLineWrapping":true,"enableAnalytics":true,"showVariableSourceAndValue":false,"filterResponsesByEnv":false,"followRedirects":true,"fontInterface":null,"fontMonospace":null,"fontSize":13,"fontVariantLigatures":false,"forceVerticalLayout":false,"hotKeyRegistry":{"workspace_showSettings":{"macKeys":[{"shift":true,"meta":true,"keyCode":188}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":188}]},"request_showSettings":{"macKeys":[{"alt":true,"shift":true,"meta":true,"keyCode":188}],"winLinuxKeys":[{"ctrl":true,"alt":true,"shift":true,"keyCode":188}]},"preferences_showKeyboardShortcuts":{"macKeys":[{"ctrl":true,"shift":true,"meta":true,"keyCode":191}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":191}]},"preferences_showGeneral":{"macKeys":[{"meta":true,"keyCode":188}],"winLinuxKeys":[{"ctrl":true,"keyCode":188}]},"request_quickSwitch":{"macKeys":[{"meta":true,"keyCode":80}],"winLinuxKeys":[{"ctrl":true,"keyCode":80}]},"request_showRecent":{"macKeys":[{"ctrl":true,"keyCode":9}],"winLinuxKeys":[{"ctrl":true,"keyCode":9}]},"request_showRecentPrevious":{"macKeys":[{"ctrl":true,"shift":true,"keyCode":9}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":9}]},"plugin_reload":{"macKeys":[{"shift":true,"meta":true,"keyCode":82}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":82}]},"showAutocomplete":{"macKeys":[{"ctrl":true,"keyCode":32}],"winLinuxKeys":[{"ctrl":true,"keyCode":32}]},"request_send":{"macKeys":[{"meta":true,"keyCode":13},{"meta":true,"keyCode":82},{"keyCode":116}],"winLinuxKeys":[{"ctrl":true,"keyCode":13},{"ctrl":true,"keyCode":82},{"keyCode":116}]},"request_showOptions":{"macKeys":[{"shift":true,"meta":true,"keyCode":13}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":13}]},"environment_showEditor":{"macKeys":[{"meta":true,"keyCode":69}],"winLinuxKeys":[{"ctrl":true,"keyCode":69}]},"environment_showSwitchMenu":{"macKeys":[{"shift":true,"meta":true,"keyCode":69}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":69}]},"request_toggleHttpMethodMenu":{"macKeys":[{"shift":true,"meta":true,"keyCode":76}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":76}]},"request_toggleHistory":{"macKeys":[{"shift":true,"meta":true,"keyCode":72}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":72}]},"request_focusUrl":{"macKeys":[{"meta":true,"keyCode":76}],"winLinuxKeys":[{"ctrl":true,"keyCode":76}]},"request_showGenerateCodeEditor":{"macKeys":[{"shift":true,"meta":true,"keyCode":71}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":71}]},"sidebar_focusFilter":{"macKeys":[{"shift":true,"meta":true,"keyCode":70}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":70}]},"sidebar_toggle":{"macKeys":[{"meta":true,"keyCode":220}],"winLinuxKeys":[{"ctrl":true,"keyCode":220}]},"response_focus":{"macKeys":[{"meta":true,"keyCode":222}],"winLinuxKeys":[{"ctrl":true,"keyCode":222}]},"showCookiesEditor":{"macKeys":[{"meta":true,"keyCode":75}],"winLinuxKeys":[{"ctrl":true,"keyCode":75}]},"request_createHTTP":{"macKeys":[{"meta":true,"keyCode":78},{"alt":true,"meta":true,"keyCode":78}],"winLinuxKeys":[{"ctrl":true,"keyCode":78},{"ctrl":true,"alt":true,"keyCode":78}]},"request_showDelete":{"macKeys":[{"shift":true,"meta":true,"keyCode":46}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":46}]},"request_showCreateFolder":{"macKeys":[{"shift":true,"meta":true,"keyCode":78}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":78}]},"request_showDuplicate":{"macKeys":[{"meta":true,"keyCode":68}],"winLinuxKeys":[{"ctrl":true,"keyCode":68}]},"request_togglePin":{"macKeys":[{"shift":true,"meta":true,"keyCode":80}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":80}]},"environment_showVariableSourceAndValue":{"macKeys":[{"alt":true,"shift":true,"keyCode":85}],"winLinuxKeys":[{"alt":true,"shift":true,"keyCode":85}]},"beautifyRequestBody":{"macKeys":[{"shift":true,"meta":true,"keyCode":70}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":70}]},"graphql_explorer_focus_filter":{"macKeys":[{"shift":true,"meta":true,"keyCode":73}],"winLinuxKeys":[{"ctrl":true,"shift":true,"keyCode":73}]}},"httpProxy":"","httpsProxy":"","lightTheme":"studio-light","maxHistoryResponses":20,"maxRedirects":10,"maxTimelineDataSizeKB":10,"noProxy":"","nunjucksPowerUserMode":false,"pluginConfig":{},"pluginPath":"","preferredHttpVersion":"default","proxyEnabled":false,"showPasswords":false,"theme":"default","timeout":30000,"updateAutomatically":true,"updateChannel":"stable","useBulkHeaderEditor":false,"useBulkParametersEditor":false,"validateAuthSSL":true,"validateSSL":true} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Stats.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Stats.db new file mode 100644 index 0000000000..795ee0ca44 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Stats.db @@ -0,0 +1,2 @@ +{"_id":"sta_75eb607a69ce4fdba7c4056c9e56201d","type":"Stats","parentId":null,"modified":1694513922516,"created":1694435923608,"currentLaunch":1694513410381,"lastLaunch":1694503948477,"currentVersion":"2023.5.8","lastVersion":"2023.5.8","launches":5,"createdRequests":21,"deletedRequests":68,"executedRequests":6} +{"_id":"sta_75eb607a69ce4fdba7c4056c9e56201d","type":"Stats","parentId":null,"modified":1694513953000,"created":1694435923608,"currentLaunch":1694513952999,"lastLaunch":1694513410381,"currentVersion":"2023.5.8","lastVersion":"2023.5.8","launches":6,"createdRequests":21,"deletedRequests":68,"executedRequests":6} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTest.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTest.db new file mode 100644 index 0000000000..f44f6b66a6 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTest.db @@ -0,0 +1,3 @@ +{"_id":"ut_3778208cf3054e5fb9ddd5ca639a6271","type":"UnitTest","parentId":"uts_aeb27e8dade847ab89f53cab2c0eafbc","modified":1694436094618,"created":1694436076224,"requestId":"req_163e848a55f94c8096d53a70d5ca6ef9","name":"Returns 200","code":"const response1 = await insomnia.send();\nexpect(response1.status).to.equal(200);"} +{"_id":"ut_622a9d9239524358bad3418a35f4e4f7","type":"UnitTest","parentId":"uts_c547332ddcda43ca8e47b489519b170f","modified":1694513487822,"created":1694513487822,"requestId":null,"name":"Returns 200","code":"const response1 = await insomnia.send();\nexpect(response1.status).to.equal(200);"} +{"_id":"ut_d7e95a6a18fb4409bce4406ac9ea247f","type":"UnitTest","parentId":"uts_6d26df3191ab46a08fe2e5859754e1cf","modified":1694513741709,"created":1694513741709,"requestId":null,"name":"Returns 200","code":"const response1 = await insomnia.send();\nexpect(response1.status).to.equal(200);"} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTestResult.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTestResult.db new file mode 100644 index 0000000000..a63919fafb --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTestResult.db @@ -0,0 +1,4 @@ +{"_id":"utr_151e4ecb47784bacbea430c71dc6a90b","type":"UnitTestResult","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436090299,"created":1694436090299,"results":{"stats":{"suites":1,"tests":1,"passes":0,"pending":0,"failures":1,"start":{"$$date":1694436090277},"end":{"$$date":1694436090299},"duration":22},"tests":[{"title":"Returns 200","fullTitle":"My Suite Returns 200","file":"/var/folders/_7/6ms57pwx6899ftqnx4cxcsf80000gn/T/insomnia-testing/0.37939980943090235-test.ts","duration":18,"currentRetry":0,"err":{"stack":"Error: attempted to output null or undefined value\n at file:///Applications/Insomnia.app/Contents/Resources/app.asar/index-f89f6e08.js:90:20151\n at /Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:2307:5\n at RawTask.call (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4583:19)\n at MutationObserver.flush (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4660:29)","message":"attempted to output null or undefined value","path":"url","location":{"line":1,"column":1},"type":"render","reason":"undefined","multiple":[{"path":"url","location":"[object Object]","type":"render","reason":"undefined","multiple":"Error: attempted to output null or undefined value"}]}}],"pending":[],"failures":[{"title":"Returns 200","fullTitle":"My Suite Returns 200","file":"/var/folders/_7/6ms57pwx6899ftqnx4cxcsf80000gn/T/insomnia-testing/0.37939980943090235-test.ts","duration":18,"currentRetry":0,"err":{"stack":"Error: attempted to output null or undefined value\n at file:///Applications/Insomnia.app/Contents/Resources/app.asar/index-f89f6e08.js:90:20151\n at /Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:2307:5\n at RawTask.call (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4583:19)\n at MutationObserver.flush (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4660:29)","message":"attempted to output null or undefined value","path":"url","location":{"line":1,"column":1},"type":"render","reason":"undefined","multiple":[{"path":"url","location":"[object Object]","type":"render","reason":"undefined","multiple":"Error: attempted to output null or undefined value"}]}}],"passes":[]}} +{"_id":"utr_257e8a95b57d4ce790d12a47fccb18d9","type":"UnitTestResult","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436095101,"created":1694436095101,"results":{"stats":{"suites":1,"tests":1,"passes":0,"pending":0,"failures":1,"start":{"$$date":1694436095069},"end":{"$$date":1694436095101},"duration":32},"tests":[{"title":"Returns 200","fullTitle":"My Suite Returns 200","file":"/var/folders/_7/6ms57pwx6899ftqnx4cxcsf80000gn/T/insomnia-testing/0.46268860035276904-test.ts","duration":31,"currentRetry":0,"err":{"stack":"Error: attempted to output null or undefined value\n at file:///Applications/Insomnia.app/Contents/Resources/app.asar/index-f89f6e08.js:90:20151\n at /Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:2307:5\n at RawTask.call (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4583:19)\n at MutationObserver.flush (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4660:29)","message":"attempted to output null or undefined value","path":"url","location":{"line":1,"column":1},"type":"render","reason":"undefined","multiple":[{"path":"url","location":"[object Object]","type":"render","reason":"undefined","multiple":"Error: attempted to output null or undefined value"}]}}],"pending":[],"failures":[{"title":"Returns 200","fullTitle":"My Suite Returns 200","file":"/var/folders/_7/6ms57pwx6899ftqnx4cxcsf80000gn/T/insomnia-testing/0.46268860035276904-test.ts","duration":31,"currentRetry":0,"err":{"stack":"Error: attempted to output null or undefined value\n at file:///Applications/Insomnia.app/Contents/Resources/app.asar/index-f89f6e08.js:90:20151\n at /Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:2307:5\n at RawTask.call (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4583:19)\n at MutationObserver.flush (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4660:29)","message":"attempted to output null or undefined value","path":"url","location":{"line":1,"column":1},"type":"render","reason":"undefined","multiple":[{"path":"url","location":"[object Object]","type":"render","reason":"undefined","multiple":"Error: attempted to output null or undefined value"}]}}],"passes":[]}} +{"_id":"utr_89249ee8bad14df883d26fc097d77eaf","type":"UnitTestResult","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436101914,"created":1694436101914,"results":{"stats":{"suites":1,"tests":1,"passes":0,"pending":0,"failures":1,"start":{"$$date":1694436101899},"end":{"$$date":1694436101914},"duration":15},"tests":[{"title":"Returns 200","fullTitle":"My Suite Returns 200","file":"/var/folders/_7/6ms57pwx6899ftqnx4cxcsf80000gn/T/insomnia-testing/0.5942253207975523-test.ts","duration":14,"currentRetry":0,"err":{"stack":"Error: attempted to output null or undefined value\n at file:///Applications/Insomnia.app/Contents/Resources/app.asar/index-f89f6e08.js:90:20151\n at /Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:2307:5\n at RawTask.call (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4583:19)\n at MutationObserver.flush (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4660:29)","message":"attempted to output null or undefined value","path":"url","location":{"line":1,"column":1},"type":"render","reason":"undefined","multiple":[{"path":"url","location":"[object Object]","type":"render","reason":"undefined","multiple":"Error: attempted to output null or undefined value"}]}}],"pending":[],"failures":[{"title":"Returns 200","fullTitle":"My Suite Returns 200","file":"/var/folders/_7/6ms57pwx6899ftqnx4cxcsf80000gn/T/insomnia-testing/0.5942253207975523-test.ts","duration":14,"currentRetry":0,"err":{"stack":"Error: attempted to output null or undefined value\n at file:///Applications/Insomnia.app/Contents/Resources/app.asar/index-f89f6e08.js:90:20151\n at /Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:2307:5\n at RawTask.call (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4583:19)\n at MutationObserver.flush (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4660:29)","message":"attempted to output null or undefined value","path":"url","location":{"line":1,"column":1},"type":"render","reason":"undefined","multiple":[{"path":"url","location":"[object Object]","type":"render","reason":"undefined","multiple":"Error: attempted to output null or undefined value"}]}}],"passes":[]}} +{"_id":"utr_96caab34bbdd46e8940ab260c53441cd","type":"UnitTestResult","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436103834,"created":1694436103834,"results":{"stats":{"suites":1,"tests":1,"passes":0,"pending":0,"failures":1,"start":{"$$date":1694436103818},"end":{"$$date":1694436103834},"duration":16},"tests":[{"title":"Returns 200","fullTitle":"My Suite Returns 200","file":"/var/folders/_7/6ms57pwx6899ftqnx4cxcsf80000gn/T/insomnia-testing/0.55166761668619-test.ts","duration":14,"currentRetry":0,"err":{"stack":"Error: attempted to output null or undefined value\n at file:///Applications/Insomnia.app/Contents/Resources/app.asar/index-f89f6e08.js:90:20151\n at /Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:2307:5\n at RawTask.call (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4583:19)\n at MutationObserver.flush (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4660:29)","message":"attempted to output null or undefined value","path":"url","location":{"line":1,"column":1},"type":"render","reason":"undefined","multiple":[{"path":"url","location":"[object Object]","type":"render","reason":"undefined","multiple":"Error: attempted to output null or undefined value"}]}}],"pending":[],"failures":[{"title":"Returns 200","fullTitle":"My Suite Returns 200","file":"/var/folders/_7/6ms57pwx6899ftqnx4cxcsf80000gn/T/insomnia-testing/0.55166761668619-test.ts","duration":14,"currentRetry":0,"err":{"stack":"Error: attempted to output null or undefined value\n at file:///Applications/Insomnia.app/Contents/Resources/app.asar/index-f89f6e08.js:90:20151\n at /Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:2307:5\n at RawTask.call (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4583:19)\n at MutationObserver.flush (/Applications/Insomnia.app/Contents/Resources/app.asar/node_modules/nunjucks/browser/nunjucks.js:4660:29)","message":"attempted to output null or undefined value","path":"url","location":{"line":1,"column":1},"type":"render","reason":"undefined","multiple":[{"path":"url","location":"[object Object]","type":"render","reason":"undefined","multiple":"Error: attempted to output null or undefined value"}]}}],"passes":[]}} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTestSuite.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTestSuite.db new file mode 100644 index 0000000000..f7303f2a73 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.UnitTestSuite.db @@ -0,0 +1,3 @@ +{"_id":"uts_6d26df3191ab46a08fe2e5859754e1cf","type":"UnitTestSuite","parentId":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","modified":1694513741703,"created":1694513741703,"name":"New Suite"} +{"_id":"uts_aeb27e8dade847ab89f53cab2c0eafbc","type":"UnitTestSuite","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694436073148,"created":1694436073148,"name":"Test Open API"} +{"_id":"uts_c547332ddcda43ca8e47b489519b170f","type":"UnitTestSuite","parentId":"wrk_55a8240d025044d3a40eb40956126bec","modified":1694513485323,"created":1694513485323,"name":"New Suite"} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WebSocketPayload.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WebSocketPayload.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WebSocketRequest.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WebSocketRequest.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WebSocketResponse.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WebSocketResponse.db new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Workspace.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Workspace.db new file mode 100644 index 0000000000..01f9728acb --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.Workspace.db @@ -0,0 +1,8 @@ +{"_id":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","type":"Workspace","parentId":"proj_team_195a6ce0edb1427eb2e8ba7b986072e4","modified":1694513770541,"created":1694513741451,"name":"Remote Design Document","description":"","scope":"design"} +{"_id":"wrk_3f71ff5dcf5343169b9a67236f9deafe","type":"Workspace","parentId":"proj_team_195a6ce0edb1427eb2e8ba7b986072e4","modified":1694513800004,"created":1694513691424,"name":"(GIT) Remote Collection","description":"","scope":"collection"} +{"_id":"wrk_55a8240d025044d3a40eb40956126bec","type":"Workspace","parentId":"proj_team_195a6ce0edb1427eb2e8ba7b986072e4","modified":1694513791914,"created":1694513468782,"name":"(GIT) Remote Design Document","description":"","scope":"design"} +{"_id":"wrk_7c9f917b97cb4a43bb402ace6df1b107","type":"Workspace","parentId":"proj_f5359773e8b544c2bfeec86c95b978dc","modified":1694513877548,"created":1694513835041,"name":"Local Collection","description":"","scope":"collection"} +{"_id":"wrk_a528e60334a54420a7b384bae215ad1b","type":"Workspace","parentId":"proj_f5359773e8b544c2bfeec86c95b978dc","modified":1694436050037,"created":1694436050037,"name":"OpenAPI","description":"","scope":"design"} +{"_id":"wrk_d783eca2fa46457c9f02a221ec862458","type":"Workspace","parentId":"proj_default-project","modified":1694513890770,"created":1694513890770,"name":"Local Design Document","description":"","scope":"design"} +{"_id":"wrk_ef815fde95ce489aa225fc4408a04bf7","type":"Workspace","parentId":"proj_team_195a6ce0edb1427eb2e8ba7b986072e4","modified":1694513499173,"created":1628147455076,"name":"Remote Collection","description":"","scope":"collection"} +{"_id":"wrk_f3246c005c9d48ed9e35724415c962d6","type":"Workspace","parentId":"proj_default-project","modified":1694513902250,"created":1694435944027,"name":"Local Collection","description":"","scope":"collection"} diff --git a/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WorkspaceMeta.db b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WorkspaceMeta.db new file mode 100644 index 0000000000..fe524cb032 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/insomnia-legacy-db/insomnia.WorkspaceMeta.db @@ -0,0 +1,8 @@ +{"_id":"wrkm_058529e2e6f541d4b58f4beebc0c8dbe","type":"WorkspaceMeta","parentId":"wrk_3f71ff5dcf5343169b9a67236f9deafe","modified":1694513700703,"created":1694513691444,"activeActivity":null,"activeEnvironmentId":null,"activeRequestId":null,"activeUnitTestSuiteId":null,"cachedGitLastAuthor":null,"cachedGitLastCommitTime":null,"cachedGitRepositoryBranch":null,"gitRepositoryId":"git_a02b558275414a518e6107c3e85e405a","hasSeen":true,"paneHeight":0.5,"paneWidth":0.5,"sidebarFilter":"","sidebarHidden":false,"sidebarWidth":19,"pushSnapshotOnInitialize":false} +{"_id":"wrkm_13722a5db6a34919b78af1b167674ef0","type":"WorkspaceMeta","parentId":"wrk_1b49626a0a9442b7b9474a1f3493bb5d","modified":1694513743823,"created":1694513741737,"activeActivity":null,"activeEnvironmentId":null,"activeRequestId":null,"activeUnitTestSuiteId":null,"cachedGitLastAuthor":null,"cachedGitLastCommitTime":null,"cachedGitRepositoryBranch":null,"gitRepositoryId":null,"hasSeen":true,"paneHeight":0.5,"paneWidth":0.5,"sidebarFilter":"","sidebarHidden":false,"sidebarWidth":19,"pushSnapshotOnInitialize":false} +{"_id":"wrkm_3a87a51a288d428786650a91deeb507e","type":"WorkspaceMeta","parentId":"wrk_f3246c005c9d48ed9e35724415c962d6","modified":1694513620993,"created":1694435944035,"activeActivity":null,"activeEnvironmentId":"env_ae8fb881d7f14abbb60c27c82dcf6072","activeRequestId":"req_3049c3a7c24245c986b29e80ad5a44c6","activeUnitTestSuiteId":null,"cachedGitLastAuthor":null,"cachedGitLastCommitTime":null,"cachedGitRepositoryBranch":null,"gitRepositoryId":null,"hasSeen":true,"paneHeight":0.5,"paneWidth":0.5,"sidebarFilter":"","sidebarHidden":false,"sidebarWidth":19,"pushSnapshotOnInitialize":false} +{"_id":"wrkm_6d6bed9b0a464bc2a1fcdf0c1a137538","type":"WorkspaceMeta","parentId":"wrk_55a8240d025044d3a40eb40956126bec","modified":1694513651760,"created":1694513468824,"activeActivity":null,"activeEnvironmentId":null,"activeRequestId":null,"activeUnitTestSuiteId":"uts_c547332ddcda43ca8e47b489519b170f","cachedGitLastAuthor":null,"cachedGitLastCommitTime":null,"cachedGitRepositoryBranch":null,"gitRepositoryId":"git_82bf4ce708514ba4a61e5a23a644e0d7","hasSeen":true,"paneHeight":0.5,"paneWidth":0.5,"sidebarFilter":"","sidebarHidden":false,"sidebarWidth":19,"pushSnapshotOnInitialize":false} +{"_id":"wrkm_9675fa52fcb544bd9b9f383ae2bfcc5d","type":"WorkspaceMeta","parentId":"wrk_ef815fde95ce489aa225fc4408a04bf7","modified":1694513669920,"created":1694436238621,"activeActivity":null,"activeEnvironmentId":null,"activeRequestId":"req_dbd2fe72a91946359532fbff416f25d7","activeUnitTestSuiteId":null,"cachedGitLastAuthor":null,"cachedGitLastCommitTime":null,"cachedGitRepositoryBranch":null,"gitRepositoryId":null,"hasSeen":true,"paneHeight":0.5,"paneWidth":0.5,"sidebarFilter":"","sidebarHidden":false,"sidebarWidth":19,"pushSnapshotOnInitialize":false} +{"_id":"wrkm_b7225af0fbe347568acadd18904389ee","type":"WorkspaceMeta","parentId":"wrk_7c9f917b97cb4a43bb402ace6df1b107","modified":1694513851556,"created":1694513835065,"activeActivity":null,"activeEnvironmentId":null,"activeRequestId":null,"activeUnitTestSuiteId":null,"cachedGitLastAuthor":null,"cachedGitLastCommitTime":null,"cachedGitRepositoryBranch":null,"gitRepositoryId":"git_da0217efdd80460faa2f153c1e871083","hasSeen":true,"paneHeight":0.5,"paneWidth":0.5,"sidebarFilter":"","sidebarHidden":false,"sidebarWidth":19,"pushSnapshotOnInitialize":false} +{"_id":"wrkm_ede333e3b385412fbdce7e4b5e2d1695","type":"WorkspaceMeta","parentId":"wrk_a528e60334a54420a7b384bae215ad1b","modified":1694513822919,"created":1694436050046,"activeActivity":null,"activeEnvironmentId":"env_05e568304a74414b8b19bec3bae0e38d","activeRequestId":"req_163e848a55f94c8096d53a70d5ca6ef9","activeUnitTestSuiteId":"uts_aeb27e8dade847ab89f53cab2c0eafbc","cachedGitLastAuthor":null,"cachedGitLastCommitTime":null,"cachedGitRepositoryBranch":null,"gitRepositoryId":"git_1fded700bfb74aa483560e116866d25e","hasSeen":true,"paneHeight":0.5,"paneWidth":0.5,"sidebarFilter":"","sidebarHidden":false,"sidebarWidth":19,"pushSnapshotOnInitialize":false} +{"_id":"wrkm_f305f30c0eea4bfbb81ecbf3f00887e8","type":"WorkspaceMeta","parentId":"wrk_d783eca2fa46457c9f02a221ec862458","modified":1694513890794,"created":1694513890794,"activeActivity":null,"activeEnvironmentId":null,"activeRequestId":null,"activeUnitTestSuiteId":null,"cachedGitLastAuthor":null,"cachedGitLastCommitTime":null,"cachedGitRepositoryBranch":null,"gitRepositoryId":null,"hasSeen":true,"paneHeight":0.5,"paneWidth":0.5,"sidebarFilter":"","sidebarHidden":false,"sidebarWidth":19,"pushSnapshotOnInitialize":false} diff --git a/packages/insomnia-smoke-test/playwright.config.ts b/packages/insomnia-smoke-test/playwright.config.ts index b71d81cf73..99b36cbbb5 100644 --- a/packages/insomnia-smoke-test/playwright.config.ts +++ b/packages/insomnia-smoke-test/playwright.config.ts @@ -20,6 +20,12 @@ const config: PlaywrightTestConfig = { testMatch: /critical\/.*.test.ts/, retries: 0, }, + { + // Single critical path test, runs on release recurring + name: 'Migration', + testMatch: /migration\/.*.test.ts/, + retries: 0, + }, ], webServer: { command: 'npm run serve', diff --git a/packages/insomnia-smoke-test/playwright/paths.ts b/packages/insomnia-smoke-test/playwright/paths.ts index c47974fffb..6fe141154f 100644 --- a/packages/insomnia-smoke-test/playwright/paths.ts +++ b/packages/insomnia-smoke-test/playwright/paths.ts @@ -12,6 +12,16 @@ export const loadFixture = async (fixturePath: string) => { return buffer.toString('utf-8'); }; +export const copyFixtureDatabase = async (fixturePath: string, dataPath: string) => { + const fixtureDir = path.join(__dirname, '..', 'fixtures', fixturePath); + + if (!fs.existsSync(fixtureDir)) { + throw new Error(`Cannot find fixture directory ${fixtureDir}`); + } + + await fs.promises.cp(fixtureDir, dataPath, { recursive: true }); +}; + export const randomDataPath = () => path.join(os.tmpdir(), 'insomnia-smoke-test', `${uuidv4()}`); export const INSOMNIA_DATA_PATH = randomDataPath(); diff --git a/packages/insomnia-smoke-test/playwright/test.ts b/packages/insomnia-smoke-test/playwright/test.ts index 5b75507eed..7bf1870f5e 100644 --- a/packages/insomnia-smoke-test/playwright/test.ts +++ b/packages/insomnia-smoke-test/playwright/test.ts @@ -14,24 +14,53 @@ interface EnvOptions { INSOMNIA_DATA_PATH: string; INSOMNIA_API_URL: string; INSOMNIA_APP_WEBSITE_URL: string; + INSOMNIA_AI_URL: string; INSOMNIA_GITHUB_API_URL: string; INSOMNIA_GITLAB_API_URL: string; INSOMNIA_UPDATES_URL: string; + INSOMNIA_SKIP_ONBOARDING: string; + INSOMNIA_PUBLIC_KEY: string; + INSOMNIA_SECRET_KEY: string; + INSOMNIA_SESSION?: string; } export const test = baseTest.extend<{ app: ElectronApplication; + dataPath: string; + fixturesPath: string; + userConfig: { + skipOnboarding: boolean; + publicKey: string; + secretKey: string; + code: string; + session?: { + accountId: string; + id: string; + sessionExpiry: Date; + email: string; + firstName: string; + lastName: string; + symmetricKey: string; + publicKey: string; + encPrivateKey: string; + }; + }; }>({ - app: async ({ playwright, trace }, use, testInfo) => { + app: async ({ playwright, trace, dataPath, userConfig }, use, testInfo) => { const webServerUrl = testInfo.config.webServer?.url; const options: EnvOptions = { - INSOMNIA_DATA_PATH: randomDataPath(), + INSOMNIA_DATA_PATH: dataPath, INSOMNIA_API_URL: webServerUrl + '/api', INSOMNIA_APP_WEBSITE_URL: webServerUrl + '/website', + INSOMNIA_AI_URL: webServerUrl + '/ai', INSOMNIA_GITHUB_API_URL: webServerUrl + '/github-api/graphql', INSOMNIA_GITLAB_API_URL: webServerUrl + '/gitlab-api', INSOMNIA_UPDATES_URL: webServerUrl || 'https://updates.insomnia.rest', + INSOMNIA_SKIP_ONBOARDING: String(userConfig.skipOnboarding), + INSOMNIA_PUBLIC_KEY: userConfig.publicKey, + INSOMNIA_SECRET_KEY: userConfig.secretKey, + ...userConfig.session ? { INSOMNIA_SESSION: JSON.stringify(userConfig.session) } : {}, }; const electronApp = await playwright._electron.launch({ @@ -46,6 +75,7 @@ export const test = baseTest.extend<{ }); const appContext = electronApp.context(); + const traceMode: TraceMode = typeof trace === 'string' ? trace as TraceMode : trace.mode; const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true }; @@ -73,4 +103,29 @@ export const test = baseTest.extend<{ await use(page); }, + dataPath: async ({ }, use) => { + const insomniaDataPath = randomDataPath(); + + await use(insomniaDataPath); + }, + userConfig: async ({ }, use) => { + await use({ + skipOnboarding: true, + publicKey: 'txb/w8DASTpPQqeHE/hpI3ABKzit+pv5n2We5dbtYRo=', + secretKey: 'Tb1QKsI3wVZxhS8TuQESHB2x7f68PzeTzTMmLpnnFVU=', + code: 'BTxpIfgXY1VgUpoPpqA25RkCPGQ2MAkZsaY6IZ0bamd0WsYQlJM6iy8PV9hEHS1Gk96SBC6%2BM%2FGhv8IaVl1N6V5wdghHwU2sGKGkW%2Fevx1HiqAUsAqIry8aWRqAkc0n3KmW%2B%2F8lyeHCpy5jhsXqMMqXMbZh8dN1q%2ByRe2C6MJS1A706KbPUhI7PRi%2FsmK0TcNT7lgBKKHRVzPTvjpLcjgzSJFL4K%2BEzgY9Ue4gh0gPw89sM9dV%2F2sAlpw0LA7rF06NyoPhA%3D', + session: { + 'id': 'sess_64a477e6b59d43a5a607f84b4f73e3ce', + // Expire in 2077 + 'sessionExpiry': new Date(2147483647000), + 'publicKey': "{ 'alg': 'RSA-OAEP-256', 'e': 'AQAB', 'ext': true, 'key_ops': ['encrypt'], 'kty': 'RSA', 'n': 'pTQVaUaiqggIldSKm6ib6eFRLLoGj9W-2O4gTbiorR-2b8-ZmKUwQ0F-jgYX71AjYaFn5VjOHOHSP6byNAjN7WzJ6A_Z3tytNraLoZfwK8KdfflOCZiZzQeD3nO8BNgh_zEgCHStU61b6N6bSpCKjbyPkmZcOkJfsz0LJMAxrXvFB-I42WYA2vJKReTJKXeYx4d6L_XGNIoYtmGZit8FldT4AucfQUXgdlKvr4_OZmt6hgjwt_Pjcu-_jO7m589mMWMebfUhjte3Lp1jps0MqTOvgRb0FQf5eoBHnL01OZjvFPDKeqlvoz7II9wFNHIKzSvgAKnyemh6DiyPuIukyQ' }", + 'encPrivateKey': "{ 'iv': '4a09fba2c412d7ea205b6168', 't': 'a4fa1a524e89e668444de654f1bd1ba8', 'd': 'b695b6ec7d41327b723e395b3788cc08c89dcf7b4db0811c3e91af4aa9ed1c9cb8132b591fe24498ffcc48c0055c1c0985a48d9e96962961c049cea508b6c38e83dfc831a4b1a82ad3e79a26d6ed3c1c9b73043a0e266cfe6eac661a75f4b9862afe2a81362d640bb2fdb6d0015204d04c322f1cb7f33faa593b538b58bda75b0c5e56583d5a55eea89e74d96ce29d862614414ee298f56105ea3dbc479aea9330618ba3e94efe874b33cd99954b12f27d7ff9e7f981310381fa0b3f1a05fbac71862ecc67ddbf7062f718d1d8bbf03f35afc3a8b1b36b177f278ab8dd12c14c862bca52a2c63bca05c7fc9bd8f1000ddc328ad1b5a72b96f110c3443294129db416bd385d19c73a1b342b4887feffa17639cc96b7b7154903c2de183f73d3116d98c8d32ea8b9627d0a5200da98d28c89c34008d4e6fa4cfbd7e1b7e1f36cfec6d0020b306c7def8d24c6091252764bde2eef74f35bdaa605dc27fcd302d179fcf65d6c7d18d3dd36cf6380bd40a29198a0398a6cb1ab79a71be00f7783b6559a146f1f825d25a990162e923e2dbfdd1ee868c8b63844f9394415ebab9f397c8d78608de00369728744b8344724e5c2f1e4ba95522406bd7042fc271793af32bfa1c2724defc8a88185cc5c2825c0453fcb39dc5ac9147d3ba60006fccca855678521da06b426dfd04333511b7d8184a8960dadefb1ef89f5fa648304adcd79734fea763f7dbd0c2788c64ea302f8e33602ab041aec619661167b2f167a4b2cadc6b7cf0c22867fcdd73528fb5585b9d13f6d90ab36c5ef231bd4f2464650e541e6dead1753487b45a8ade3fe46e2327fd0e32d8adafd4e1d2ffcb2df1ed50718d0d29829f8bf4bc33da524640388b0cb9f640b70f9ef0a1d073bc80abdf975fe77b35f07aee5135e2924661d26e5d299a432c563a3bbc5ff21d1fde07ff2336b32b17067c01adc6697568aabf3ff4882530763f77d96cef2fcc7336e3f8e4a2b8de5df42aaeaeccd9e3681604e677fad555148ced6057d99cab03389bb566b27cc4ea3946a640f05593e5944c74adf5d0649941b8032dc6959bdef917fd7d2da3139b1d3770d411b52752c9299f789ce5de64e802740a8210e1c70e0ffb8aa45a3647b837e2c5d1a7dd676b238268ac7c060bcc771285f21a283c2f0eefb54254086770a4f09140f7b6a118a7df1e06445379080c2cb6a8840b9d70411521107b47751634e3ff6437974ac2e5897e7e15ac8bdbb5325bb1dd09c20f8ac37fa1eaf2765671fd1434460cbf3cc97bf67b19f88b2bfbde99836c27c338b7f19fc20aad90b91e268c87b81aa17026ac5ac74f47c3525fd2d0d584d3b1d75dd360f105f78b2831481802b6a40e88938660d1598947ffb4e7cf75cd67308d6f910be2fb9beafb68d5ea7c3d2f79c3b66223d610a70a6f9ce3c3447bc0acf74e84687c2da5a137b6c14631971e19133a61bfa94c247ae99d771d8efc11d983e2ea904dab4ca479fa00be3b0372e100225311ffb5b95faed4c32b5794cc618ded1027e6f2a54328a5cd322da6c6ef91151cacfa456680547adf18cde5323869bc03b2c7edd731f5e5c9b9eaa7b2f57d4246d324047482a48d7472650b8d0614a0e133e849a09d37bc9f7921d05397b98e398cd3411dde80d9bd6be4384289a2d1a0416cd914e7520cae962493d31b652a520a9fca7d7e6e9e5df53d719e518125b73ea49af30b720a6d0a71089dcaf04da9ee05c4ede6d0ba376c86282293248a4b36a2ac07a3297d569ad4e958c918317a4d526dae47b07f8c6615a77f5831d146c063b88246e2b3ce7b8f4b75291c339c317e3fbc84563694cd749f021b1e0c076521d685f87497831f19cba89601a344c5d1d08f12b2de9d5d068daf760bf87993e89a2912ab29e3fc79af39db18e982bfe0ee0374c84487cabc1f59bc216d71c38654805cddbc338fc8c14413849ea3ef79444ac6078fb2403ecd84de5b678538be0b2580ecb1926e03643ff29464f943cd729ad386daf1f81ea385e79260cdf78d579281adb72946692d54e94a2fd8530dfb8e5923ce642ec92ccee28f6b21efdff24ec820b4eff26f493278acb485af055881e077cdf3017bc6104b2394fee39343b7c71c20cbf7a3ea96b0c7b603a2ff998d27b16833b028bf30ab668041c8d82225d58bd95f9246de742815067046adbf55eddfddfbc30fea3b8c6659d2756c702f2012395245035bf5338427051b4eb392225aa10179d8b042ba77bc1a37cd66a655ec03aa3aa3c75e05aad4ad38240ee0e6c5af85bc7813f2c0de6eccf5cfba2f6295daf448c042c50c6eba6127f170848cc2034bd61698747a3bee155146d2ab73ba79d969cffcba737bd85b20123a5e3080edba483d831c38c9a4aee9a2fdeb0819665cf28aab91f2317f77d22b29f49d6dbd4ae5b82f9529fb208824f22fdd48666dfc0abe4a9d00dd7d552f4bf6fade29b63ad080614bfe04d9fdca7cff96378e201f6e71cc665e6ae8abd64d125a8c222b03f9153824251db960b4eae41280b681a9fa6c1ec76e94bcd9656aa3df3b57f2da9', 'ad': '' }", + 'symmetricKey': "{ 'iv': '0146bfcc7b89a3aa055bedae', 't': '7559a4ef7ca495c9605516cc846007a3', 'd': '4de3ab37e73dd6fc294257fe4cb1af26229434297ecdd309e93a941bd1f9913b5916dfe55d62f0d4015c46f66d8fdf4c84ab88c22e4d24428d5fe6c1affce1e14b33760a382b67b4a37262f9ca5a44cb9760b151bbd0748fc18f8f438545df356d99a66c74ff22b623b2b67d9765f80ac6f18af01684e6e3efbce947832ac0bea010c1cde00390e1f3d2187286ff00a43aef8ddc13a98d8f4f771bba1694712623b115f0b2c0e891eccd074ade0b551737b915d0f1242ffcbc1c65555ff7', 'ad': '' }", + 'email': 'insomnia-user@konghq.com', + 'accountId': 'acct_64a477e6b59d43a5a607f84b4f73e3ce', + 'firstName': 'Rick', + 'lastName': 'Morty', + }, + }); + }, }); diff --git a/packages/insomnia-smoke-test/server/index.ts b/packages/insomnia-smoke-test/server/index.ts index c52f49996a..e2ea91effa 100644 --- a/packages/insomnia-smoke-test/server/index.ts +++ b/packages/insomnia-smoke-test/server/index.ts @@ -11,6 +11,7 @@ import githubApi from './github-api'; import gitlabApi from './gitlab-api'; import { schema } from './graphql'; import { startGRPCServer } from './grpc'; +import insomniaApi from './insomnia-api'; import { oauthRoutes } from './oauth'; import { startWebSocketServer } from './websocket'; @@ -47,6 +48,7 @@ app.use('/auth/basic', basicAuthRouter); githubApi(app); gitlabApi(app); +insomniaApi(app); app.get('/delay/seconds/:duration', (req, res) => { const delaySec = Number.parseInt(req.params.duration || '2'); diff --git a/packages/insomnia-smoke-test/server/insomnia-api.ts b/packages/insomnia-smoke-test/server/insomnia-api.ts new file mode 100644 index 0000000000..c9c25e1da2 --- /dev/null +++ b/packages/insomnia-smoke-test/server/insomnia-api.ts @@ -0,0 +1,208 @@ +import { randomUUID } from 'crypto'; +import type { Application } from 'express'; +import { json } from 'express'; + +const currentPlan = { + isActive: true, + period: 'year', + planId: 'team', + price: 100, + quantity: 10, + type: 'team', +}; + +const projectsByOrgId = new Map(Object.entries({ + 'org_7ef19d06-5a24-47ca-bc81-3dea011edec2': [ + { + id: 'proj_org_7ef19d06-5a24-47ca-bc81-3dea011edec2', + name: 'Personal Workspace', + }, + ], + 'team_195a6ce0edb1427eb2e8ba7b986072e4': [ + { + id: 'proj_team_195a6ce0edb1427eb2e8ba7b986072e4', + name: 'Personal Workspace', + }, + ], +})); + +const organizations = [ + // Personal organization + { + 'id': 'org_7ef19d06-5a24-47ca-bc81-3dea011edec2', + 'name': 'feb56ab4b19347c4b648c99bfa7db363', + 'display_name': 'Personal workspace', + 'branding': { + 'logo_url': '', + }, + 'metadata': { + 'organizationType': 'personal', + 'ownerAccountId': 'acct_64a477e6b59d43a5a607f84b4f73e3ce', + }, + }, + // Team Organization: Was a team before the migration now looks like this: + // Teams migrated to Organizations have an id that starts with team_ and the team id is the same as the organization id + { + 'id': 'team_195a6ce0edb1427eb2e8ba7b986072e4', + 'name': '07df6d95b60e4593af0424c74d96637a-team', + 'display_name': '🦄 Magic', + 'branding': { + 'logo_url': '', + }, + 'metadata': { + 'organizationType': 'team', + 'ownerAccountId': 'acct_64a477e6b59d43a5a607f84b4f73e3ce', + }, + }, +]; + +const organizationFeatures = { + features: { + gitSync: { + enabled: true, + }, + }, +}; + +const user = { + 'id': 'email|64f0dd619ab0786da330d83a', + 'email': 'insomnia-user@konghq.com', + 'name': 'Rick Morty', + 'picture': '', + 'bio': 'My BIO', + 'github': '', + 'linkedin': '', + 'twitter': '', + 'identities': null, + 'given_name': '', + 'family_name': '', +}; + +const whoami = { + 'sessionExpiry': 4838400, + 'publicKey': '{"alg":"RSA-OAEP-256","e":"AQAB","ext":true,"key_ops":["encrypt"],"kty":"RSA","n":"pTQVaUaiqggIldSKm6ib6eFRLLoGj9W-2O4gTbiorR-2b8-ZmKUwQ0F-jgYX71AjYaFn5VjOHOHSP6byNAjN7WzJ6A_Z3tytNraLoZfwK8KdfflOCZiZzQeD3nO8BNgh_zEgCHStU61b6N6bSpCKjbyPkmZcOkJfsz0LJMAxrXvFB-I42WYA2vJKReTJKXeYx4d6L_XGNIoYtmGZit8FldT4AucfQUXgdlKvr4_OZmt6hgjwt_Pjcu-_jO7m589mMWMebfUhjte3Lp1jps0MqTOvgRb0FQf5eoBHnL01OZjvFPDKeqlvoz7II9wFNHIKzSvgAKnyemh6DiyPuIukyQ"}', + 'encPrivateKey': '{"iv":"4a09fba2c412d7ea205b6168","t":"a4fa1a524e89e668444de654f1bd1ba8","d":"b695b6ec7d41327b723e395b3788cc08c89dcf7b4db0811c3e91af4aa9ed1c9cb8132b591fe24498ffcc48c0055c1c0985a48d9e96962961c049cea508b6c38e83dfc831a4b1a82ad3e79a26d6ed3c1c9b73043a0e266cfe6eac661a75f4b9862afe2a81362d640bb2fdb6d0015204d04c322f1cb7f33faa593b538b58bda75b0c5e56583d5a55eea89e74d96ce29d862614414ee298f56105ea3dbc479aea9330618ba3e94efe874b33cd99954b12f27d7ff9e7f981310381fa0b3f1a05fbac71862ecc67ddbf7062f718d1d8bbf03f35afc3a8b1b36b177f278ab8dd12c14c862bca52a2c63bca05c7fc9bd8f1000ddc328ad1b5a72b96f110c3443294129db416bd385d19c73a1b342b4887feffa17639cc96b7b7154903c2de183f73d3116d98c8d32ea8b9627d0a5200da98d28c89c34008d4e6fa4cfbd7e1b7e1f36cfec6d0020b306c7def8d24c6091252764bde2eef74f35bdaa605dc27fcd302d179fcf65d6c7d18d3dd36cf6380bd40a29198a0398a6cb1ab79a71be00f7783b6559a146f1f825d25a990162e923e2dbfdd1ee868c8b63844f9394415ebab9f397c8d78608de00369728744b8344724e5c2f1e4ba95522406bd7042fc271793af32bfa1c2724defc8a88185cc5c2825c0453fcb39dc5ac9147d3ba60006fccca855678521da06b426dfd04333511b7d8184a8960dadefb1ef89f5fa648304adcd79734fea763f7dbd0c2788c64ea302f8e33602ab041aec619661167b2f167a4b2cadc6b7cf0c22867fcdd73528fb5585b9d13f6d90ab36c5ef231bd4f2464650e541e6dead1753487b45a8ade3fe46e2327fd0e32d8adafd4e1d2ffcb2df1ed50718d0d29829f8bf4bc33da524640388b0cb9f640b70f9ef0a1d073bc80abdf975fe77b35f07aee5135e2924661d26e5d299a432c563a3bbc5ff21d1fde07ff2336b32b17067c01adc6697568aabf3ff4882530763f77d96cef2fcc7336e3f8e4a2b8de5df42aaeaeccd9e3681604e677fad555148ced6057d99cab03389bb566b27cc4ea3946a640f05593e5944c74adf5d0649941b8032dc6959bdef917fd7d2da3139b1d3770d411b52752c9299f789ce5de64e802740a8210e1c70e0ffb8aa45a3647b837e2c5d1a7dd676b238268ac7c060bcc771285f21a283c2f0eefb54254086770a4f09140f7b6a118a7df1e06445379080c2cb6a8840b9d70411521107b47751634e3ff6437974ac2e5897e7e15ac8bdbb5325bb1dd09c20f8ac37fa1eaf2765671fd1434460cbf3cc97bf67b19f88b2bfbde99836c27c338b7f19fc20aad90b91e268c87b81aa17026ac5ac74f47c3525fd2d0d584d3b1d75dd360f105f78b2831481802b6a40e88938660d1598947ffb4e7cf75cd67308d6f910be2fb9beafb68d5ea7c3d2f79c3b66223d610a70a6f9ce3c3447bc0acf74e84687c2da5a137b6c14631971e19133a61bfa94c247ae99d771d8efc11d983e2ea904dab4ca479fa00be3b0372e100225311ffb5b95faed4c32b5794cc618ded1027e6f2a54328a5cd322da6c6ef91151cacfa456680547adf18cde5323869bc03b2c7edd731f5e5c9b9eaa7b2f57d4246d324047482a48d7472650b8d0614a0e133e849a09d37bc9f7921d05397b98e398cd3411dde80d9bd6be4384289a2d1a0416cd914e7520cae962493d31b652a520a9fca7d7e6e9e5df53d719e518125b73ea49af30b720a6d0a71089dcaf04da9ee05c4ede6d0ba376c86282293248a4b36a2ac07a3297d569ad4e958c918317a4d526dae47b07f8c6615a77f5831d146c063b88246e2b3ce7b8f4b75291c339c317e3fbc84563694cd749f021b1e0c076521d685f87497831f19cba89601a344c5d1d08f12b2de9d5d068daf760bf87993e89a2912ab29e3fc79af39db18e982bfe0ee0374c84487cabc1f59bc216d71c38654805cddbc338fc8c14413849ea3ef79444ac6078fb2403ecd84de5b678538be0b2580ecb1926e03643ff29464f943cd729ad386daf1f81ea385e79260cdf78d579281adb72946692d54e94a2fd8530dfb8e5923ce642ec92ccee28f6b21efdff24ec820b4eff26f493278acb485af055881e077cdf3017bc6104b2394fee39343b7c71c20cbf7a3ea96b0c7b603a2ff998d27b16833b028bf30ab668041c8d82225d58bd95f9246de742815067046adbf55eddfddfbc30fea3b8c6659d2756c702f2012395245035bf5338427051b4eb392225aa10179d8b042ba77bc1a37cd66a655ec03aa3aa3c75e05aad4ad38240ee0e6c5af85bc7813f2c0de6eccf5cfba2f6295daf448c042c50c6eba6127f170848cc2034bd61698747a3bee155146d2ab73ba79d969cffcba737bd85b20123a5e3080edba483d831c38c9a4aee9a2fdeb0819665cf28aab91f2317f77d22b29f49d6dbd4ae5b82f9529fb208824f22fdd48666dfc0abe4a9d00dd7d552f4bf6fade29b63ad080614bfe04d9fdca7cff96378e201f6e71cc665e6ae8abd64d125a8c222b03f9153824251db960b4eae41280b681a9fa6c1ec76e94bcd9656aa3df3b57f2da9","ad":""}', + 'encSymmetricKey': '{"iv":"0146bfcc7b89a3aa055bedae","t":"7559a4ef7ca495c9605516cc846007a3","d":"4de3ab37e73dd6fc294257fe4cb1af26229434297ecdd309e93a941bd1f9913b5916dfe55d62f0d4015c46f66d8fdf4c84ab88c22e4d24428d5fe6c1affce1e14b33760a382b67b4a37262f9ca5a44cb9760b151bbd0748fc18f8f438545df356d99a66c74ff22b623b2b67d9765f80ac6f18af01684e6e3efbce947832ac0bea010c1cde00390e1f3d2187286ff00a43aef8ddc13a98d8f4f771bba1694712623b115f0b2c0e891eccd074ade0b551737b915d0f1242ffcbc1c65555ff7","ad":""}', + 'email': 'insomnia-user@konghq.com', + 'accountId': 'acct_64a477e6b59d43a5a607f84b4f73e3ce', + 'firstName': 'Rick', + 'lastName': 'Morty', +}; + +export default (app: Application) => { + app.post('/api/graphql', json(), (_req, res) => { + res.status(200).send({ + data: { + viewer: { + name: 'InsomniaUser', + email: 'sleepyhead@email.com', + }, + }, + }); + }); + + // User + app.get('/api/v1/user/profile', (_req, res) => { + console.log('GET *'); + res.status(200).send(user); + }); + + app.get('/api/auth/whoami', (_req, res) => { + res.status(200).send(whoami); + }); + + // Billing + app.get('/api/v1/billing/current-plan', json(), (_req, res) => { + res.status(200).send(currentPlan); + }); + + // Organizations + app.get('/api/v1/organizations', (_req, res) => { + res.status(200).send({ + organizations: organizations, + }); + }); + + app.get('/api/v1/organizations/:orgId/features', (_req, res) => { + res.status(200).send(organizationFeatures); + }); + + // Projects + app.get('/api/v1/organizations/:orgId/team-projects', (_req, res) => { + res.status(200).send({ + data: projectsByOrgId.get(_req.params.orgId), + }); + }); + + app.delete('/api/v1/organizations/:orgId/team-projects/:projectId', json(), (_req, res) => { + const projects = projectsByOrgId.get(_req.params.orgId)?.filter(project => project.id !== _req.params.projectId); + if (!projects) { + res.status(500).send(); + return; + } + projectsByOrgId.set(_req.params.orgId, projects); + res.status(200).send(); + }); + + app.patch('/api/v1/organizations/:orgId/team-projects/:projectId', json(), (_req, res) => { + const updatedProjects = projectsByOrgId.get(_req.params.orgId)?.map(project => { + if (project.id === _req.params.projectId) { + return { + ...project, + name: _req.body.name, + }; + } + return project; + }); + + updatedProjects && projectsByOrgId.set(_req.params.orgId, updatedProjects); + res.status(200).send(); + }); + + app.post('/api/v1/organizations/:organizationId/team-projects', json(), (_req, res) => { + const { organizationId } = _req.params; + + if (organizationId === 'personal') { + const personalOrg = organizations.find(org => org.metadata.organizationType === 'personal'); + + if (!personalOrg) { + res.status(500).send(); + return; + } + + const newProject = { + id: `proj_${randomUUID()}`, + name: _req.body.name, + }; + + const projects = [ + ...(projectsByOrgId.get(personalOrg.id) || []), + newProject, + ]; + + projectsByOrgId.set(personalOrg.id, projects); + res.status(200).send({ ...newProject, organizationId: personalOrg.id }); + return; + } + + const organization = organizations.find(org => org.id === organizationId); + + if (!organization) { + res.status(500).send(); + return; + } + + const newProject = { + id: `proj_${randomUUID()}`, + name: _req.body.name, + }; + + const projects = [ + ...(projectsByOrgId.get(organization.id) || []), + newProject, + ]; + + projectsByOrgId.set(organization.id, projects); + res.status(200).send({ ...newProject, organizationId: organization.id }); + }); +}; diff --git a/packages/insomnia-smoke-test/tests/critical/app-start.test.ts b/packages/insomnia-smoke-test/tests/critical/app-start.test.ts index f4698d0bfe..e0346b3ee2 100644 --- a/packages/insomnia-smoke-test/tests/critical/app-start.test.ts +++ b/packages/insomnia-smoke-test/tests/critical/app-start.test.ts @@ -13,7 +13,7 @@ test('can send requests', async ({ app, page }) => { await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionSmoke testsjust now').click(); diff --git a/packages/insomnia-smoke-test/tests/migration/local-to-cloud-projects.test.ts b/packages/insomnia-smoke-test/tests/migration/local-to-cloud-projects.test.ts new file mode 100644 index 0000000000..b263cfe1e1 --- /dev/null +++ b/packages/insomnia-smoke-test/tests/migration/local-to-cloud-projects.test.ts @@ -0,0 +1,75 @@ +import { copyFixtureDatabase } from '../../playwright/paths'; +import { test } from '../../playwright/test'; + +const testWithLegacyDatabase = test.extend({ + dataPath: async ({ dataPath }, use) => { + await copyFixtureDatabase('insomnia-legacy-db', dataPath); + + await use(dataPath); + }, + userConfig: async ({ userConfig }, use) => { + await use({ + ...userConfig, + session: undefined, + }); + }, +}); + +// tests new cloud project create but not the vcs part as its too complex to stub the gql endpoint +testWithLegacyDatabase('Run data migration to version 8', async ({ page, userConfig }) => { + // Migration takes a while, adding this to avoid test timeout before it ends + test.slow(); + + await page.getByLabel('Continue with Google').click(), + + await page.locator('input[name="code"]').click(); + await page.locator('input[name="code"]').fill(userConfig.code); + + await page.getByRole('button', { name: 'Log in' }).click(); + + // Open migrated Project (Local before migration) + await page.getByLabel('Insomnia').click(); + + // Open migrated local migrated collection that should now have Insomnia Sync + await page.getByLabel('Local Collection').click(); + await page.getByLabel('Mars', { exact: true }).click(); + await page.getByRole('option', { name: 'Mars' }).click(); + await page.getByLabel('Insomnia Sync').isVisible(); + await page.getByText('Get list of rockets').click(); + await page.getByTestId('project').click(); + + // Open migrated local migrated collection that should have Git Sync + await page.getByLabel('Local Project (GIT)').click(); + await page.getByLabel('OpenAPI').click(); + await page.getByText('Delete user').click(); + await page.getByLabel('Git Sync').isVisible(); + await page.getByTestId('project').click(); + + await page.getByLabel('Personal Workspace', { exact: true }).click(); + + // Open Team that is migrated to Organization + await page.getByRole('link', { name: '🦄' }).click(); + + // Open remote migrated collection that should have Insomnia Sync + await page.getByLabel('Personal Workspace', { exact: true }).click(); + await page.getByLabel('Remote Design Document', { exact: true }).click(); + await page.getByText('Updated user').click(); + await page.getByLabel('Insomnia Sync').isVisible(); + await page.getByTestId('project').click(); + + // Open remote migrated collection that should have GIT Sync + await page.getByLabel('(GIT) Remote Design Document').click(); + await page.getByText('Updated user').click(); + await page.getByLabel('Git Sync').isVisible(); + await page.getByTestId('project').click(); + + // Open remote migrated collection that should have Insomnia Sync + await page.getByLabel('Remote Collection', { exact: true }).click(); + await page.getByText('New Request').click(); + await page.getByLabel('Insomnia Sync').isVisible(); + await page.getByTestId('project').click(); + + // Open remote migrated collection that should have Insomnia Sync + await page.getByLabel('Remote Collection', { exact: true }).click(); + await page.getByLabel('Insomnia Sync').isVisible(); +}); diff --git a/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts index f5c5d5d5a8..cfb4e16089 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/design-interactions.test.ts @@ -12,7 +12,7 @@ test.describe('Design interactions', async () => { const text = await loadFixture('unit-test.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('unit-test.yaml').click(); @@ -20,37 +20,27 @@ test.describe('Design interactions', async () => { await page.click('a:has-text("Test")'); // Run tests and check results - await page.click('#wrapper >> text=Run Tests'); + await page.getByLabel('Run all tests').click(); await expect(page.locator('.app')).toContainText('Request A is found'); await expect(page.locator('.app')).toContainText('Request B is not found'); - await expect(page.locator('.app')).toContainText('Tests Passed 2/2'); + await expect(page.locator('.app')).toContainText('Tests passed'); // Create a new test suite - await page.click('text=New Test Suite'); - await page.click('text=Create Suite'); - await page.click('button:has-text("New Suite")'); + await page.click('text=New test suite'); + + // Rename test suite + await page.getByRole('button', { name: 'New Suite' }).click(); + await page.getByRole('textbox').fill('New Suite 2'); + await page.getByRole('textbox').press('Enter'); // Add a new test - await page.locator('text=New Test').nth(1).click(); - await page.click('div[role="dialog"] button:has-text("New Test")'); - const label = await page.locator('option', { hasText: 'Request A' }).textContent() || ''; - await page.locator('select[name="request"]').selectOption({ - label, - }); - - await page.click('#wrapper >> text=Run Tests'); - await expect(page.locator('.app')).toContainText('Tests Passed 1/1'); - - // Rename a test - // TODO(filipe) - add this in another PR - - // Rename a test suite - await page.click('button:has-text("Existing Test Suite")'); - await page.click('span:has-text("Existing Test Suite")'); - await page.locator('text=New TestRun Tests >> input[type="text"]').fill('Renamed'); - await page.locator('text=New TestRun Tests >> input[type="text"]').press('Enter'); - await page.click('button:has-text("Renamed")'); + await page.getByLabel('New test').click(); + // Rename test + await page.getByRole('button', { name: 'Returns 200' }).click(); + await page.getByRole('textbox').fill('Returns 200 and works'); + await page.getByRole('textbox').press('Enter'); + await page.getByRole('heading', { name: 'Returns 200 and works' }).click(); // Use autocomplete inside the test code // TODO(filipe) - add this in another PR }); diff --git a/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts index 7562a66961..c30a4ced8f 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/grpc-interactions.test.ts @@ -17,7 +17,7 @@ test.describe('gRPC interactions', () => { await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionPreRelease gRPCjust now').click(); diff --git a/packages/insomnia-smoke-test/tests/prerelease/plugins-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/plugins-interactions.test.ts index b05c0a4a6c..a9f8a0f0d6 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/plugins-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/plugins-interactions.test.ts @@ -14,7 +14,7 @@ test.describe('Plugins', async () => { await page.locator('text=Generate').first().click(); // check if the plugin shows up on the plugin list - await expect(page.locator('.app')).toContainText('insomnia-plugin-demo-example'); + await page.getByRole('cell', { name: 'insomnia-plugin-demo-example' }).click(); }); test('Check Declarative Config and Kong Kubernetes config', async ({ page }) => { @@ -26,8 +26,8 @@ test.describe('Plugins', async () => { await expect(page.locator('.app')).toContainText('This is a sample server Petstore server'); // Open declarative config - await page.getByTestId('workspace-dropdown').locator('button').click(); - await page.getByRole('menuitem', { name: 'Declarative Config (Legacy)' }).click(); + await page.getByLabel('Workspace actions').click(); + await page.getByRole('menuitemradio', { name: 'Declarative Config (Legacy)' }).click(); // Check for declarative config contents await page.getByText('_format_version').click(); diff --git a/packages/insomnia-smoke-test/tests/prerelease/preferences-interactions.test.ts b/packages/insomnia-smoke-test/tests/prerelease/preferences-interactions.test.ts index 22e821a0a7..46236bf371 100644 --- a/packages/insomnia-smoke-test/tests/prerelease/preferences-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/prerelease/preferences-interactions.test.ts @@ -21,7 +21,7 @@ test('Check filter responses by environment preference', async ({ app, page }) = const text = await loadFixture('simple.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('Collectionsimplejust now').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/app.test.ts b/packages/insomnia-smoke-test/tests/smoke/app.test.ts index 7600b25ef8..be5220e0dd 100644 --- a/packages/insomnia-smoke-test/tests/smoke/app.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/app.test.ts @@ -15,7 +15,7 @@ test('can send requests', async ({ app, page }) => { await page.getByRole('button', { name: 'Create in project' }).click(); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); @@ -93,7 +93,7 @@ test('can cancel requests', async ({ app, page }) => { await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionSmoke testsjust now').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts index 17ce9e3e78..f29d756c8b 100644 --- a/packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/cookie-editor-interactions.test.ts @@ -8,7 +8,7 @@ test.describe('Cookie editor', async () => { const text = await loadFixture('simple.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('Collectionsimplejust now').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts index 17d27b3838..eecd6a71c2 100644 --- a/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts @@ -13,7 +13,7 @@ test.describe('Dashboard', async () => { // Create new project await page.getByRole('button', { name: 'Create new Project' }).click(); - await page.locator('text=Create').nth(1).click(); + await page.getByRole('button', { name: 'Create', exact: true }).click(); // Check empty project await expect(page.locator('.app')).toContainText('This is an empty project, to get started create your first resource:'); @@ -23,6 +23,7 @@ test.describe('Dashboard', async () => { await page.getByRole('menuitemradio', { name: 'Settings' }).click(); await page.getByPlaceholder('My Project').click(); await page.getByPlaceholder('My Project').fill('My Project123'); + await page.getByRole('button', { name: 'Update' }).click(); // Check that the project name is updated on modal await expect(page.locator('.app')).toContainText('My Project123'); @@ -34,12 +35,12 @@ test.describe('Dashboard', async () => { // Delete project await page.getByRole('row', { name: 'My Project' }).getByRole('button', { name: 'Project Actions' }).click(); await page.getByRole('menuitemradio', { name: 'Settings' }).click(); - // Click text=NameActions Delete >> button - await page.click('text=NameActions Delete >> button'); + + await page.getByRole('button', { name: 'Delete' }).click(); await page.getByRole('button', { name: 'Click to confirm' }).click(); // After deleting project, return to default Insomnia Dashboard - await expect(page.locator('.app')).toContainText('Insomnia'); + await expect(page.locator('.app')).toContainText('Personal Workspace'); await expect(page.locator('.app')).not.toContainText('My Project123'); await expect(page.locator('.app')).toContainText('New Document'); await page.getByLabel('All Files (0)').click(); @@ -57,7 +58,7 @@ test.describe('Dashboard', async () => { const text = await loadFixture('multiple-workspaces.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionSmoke testsjust now').click(); @@ -94,7 +95,7 @@ test.describe('Dashboard', async () => { // Create new document await page.getByRole('button', { name: 'Create in project' }).click(); await page.getByRole('menuitemradio', { name: 'Design Document' }).click(); - await page.locator('text=Create').nth(1).click(); + await page.getByRole('button', { name: 'Create', exact: true }).click(); await page.getByTestId('project').click(); @@ -131,7 +132,7 @@ test.describe('Dashboard', async () => { // Create new collection await page.getByRole('button', { name: 'Create in project' }).click(); await page.getByRole('menuitemradio', { name: 'Request Collection' }).click(); - await page.locator('text=Create').nth(1).click(); + await page.getByRole('button', { name: 'Create', exact: true }).click(); await page.getByTestId('project').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/debug-sidebar-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/debug-sidebar-interactions.test.ts index 6fcb59efcd..95cc401242 100644 --- a/packages/insomnia-smoke-test/tests/smoke/debug-sidebar-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/debug-sidebar-interactions.test.ts @@ -10,7 +10,7 @@ test.describe('Debug-Sidebar', async () => { const text = await loadFixture('simple.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('Collectionsimplejust now').click(); @@ -58,8 +58,8 @@ test.describe('Debug-Sidebar', async () => { }); test('Open properties of the collection', async ({ page }) => { - await page.getByTestId('workspace-dropdown').locator('button').click(); - await page.getByRole('menuitem', { name: 'Settings' }).click(); + await page.getByLabel('Workspace actions', { exact: true }).click(); + await page.getByRole('menuitemradio', { name: 'Settings' }).click(); await page.getByText('Collection Settings').click(); }); diff --git a/packages/insomnia-smoke-test/tests/smoke/environment-editor-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/environment-editor-interactions.test.ts index 9948f3b4d1..5602aa166b 100644 --- a/packages/insomnia-smoke-test/tests/smoke/environment-editor-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/environment-editor-interactions.test.ts @@ -8,7 +8,7 @@ test.describe('Environment Editor', async () => { const text = await loadFixture('environments.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('Collectionenvironments').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts index 582ce29a70..f8e99d8d8e 100644 --- a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts @@ -1,6 +1,6 @@ import { test } from '../../playwright/test'; test('Clone from github', async ({ page }) => { - await page.getByRole('button', { name: 'Git Clone' }).click(); + await page.getByLabel('Clone git repository').click(); await page.getByRole('tab', { name: ' Git' }).click(); await page.getByPlaceholder('https://github.com/org/repo.git').fill('https://github.com/gatzjames/insomnia-git-example.git'); await page.getByPlaceholder('Name').fill('J'); @@ -13,8 +13,8 @@ test('Clone from github', async ({ page }) => { test('Sign in with GitHub', async ({ app, page }) => { await page.getByRole('button', { name: 'New Document' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); - await page.getByRole('button', { name: 'Git Sync' }).click(); - await page.getByRole('button', { name: 'Setup Git Sync' }).click(); + await page.getByLabel('Insomnia Sync').click(); + await page.getByLabel('Setup Git Sync').click(); await page.getByRole('tab', { name: 'Github' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts b/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts index 28bd0c4a3a..c6519defd0 100644 --- a/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/graphql.test.ts @@ -14,7 +14,7 @@ test('can render schema and send GraphQL requests', async ({ app, page }) => { // Import from clipboard await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionSmoke GraphQLjust now').click(); @@ -51,7 +51,7 @@ test('can send GraphQL requests after editing and prettifying query', async ({ a const text = await loadFixture('graphql.yaml'); await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionSmoke GraphQLjust now').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts b/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts index 1cf5af27e1..240fe457cd 100644 --- a/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/grpc.test.ts @@ -16,7 +16,7 @@ test('can send gRPC requests with reflection', async ({ app, page }) => { await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionPreRelease gRPCjust now').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts b/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts index b78fe6d1f5..f3361b3ec3 100644 --- a/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/oauth-gitlab.test.ts @@ -3,8 +3,8 @@ import { test } from '../../playwright/test'; test('Sign in with Gitlab', async ({ app, page }) => { await page.getByRole('button', { name: 'New Document' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); - await page.getByRole('button', { name: 'Git Sync' }).click(); - await page.getByRole('button', { name: 'Setup Git Sync' }).click(); + await page.getByLabel('Insomnia Sync').click(); + await page.getByLabel('Setup Git Sync').click(); await page.getByRole('tab', { name: 'GitLab' }).click(); const fakeGitLabOAuthWebFlow = app.evaluate(electron => { diff --git a/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts b/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts index 3335312982..844292bdeb 100644 --- a/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/oauth.test.ts @@ -23,7 +23,7 @@ test('can make oauth2 requests', async ({ app, page }) => { await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionOAuth Testingjust now').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/websocket.test.ts b/packages/insomnia-smoke-test/tests/smoke/websocket.test.ts index 0c979e611d..1ddf5691dc 100644 --- a/packages/insomnia-smoke-test/tests/smoke/websocket.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/websocket.test.ts @@ -16,7 +16,7 @@ test('can make websocket connection', async ({ app, page }) => { await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.getByText('Clipboard').click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); await page.getByRole('button', { name: 'Scan' }).click(); await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByText('CollectionWebSocketsjust now').click(); diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 103fa1cfb4..747e29c3b7 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -58,6 +58,7 @@ "electron-context-menu": "^3.6.1", "electron-log": "^4.4.8", "grpc-reflection-js": "0.3.0", + "gsap": "^3.11.3", "hawk": "9.0.1", "hkdf": "^0.0.2", "hosted-git-info": "5.2.1", diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts index c620e51a78..03b197f814 100644 --- a/packages/insomnia/src/account/session.ts +++ b/packages/insomnia/src/account/session.ts @@ -6,6 +6,7 @@ type LoginCallback = (isLoggedIn: boolean) => void; export interface WhoamiResponse { sessionAge: number; + sessionExpiry: number; accountId: string; email: string; firstName: string; @@ -29,6 +30,7 @@ export interface WhoamiResponse { export interface SessionData { accountId: string; id: string; + sessionExpiry: Date; email: string; firstName: string; lastName: string; @@ -46,6 +48,7 @@ export function onLoginLogout(loginCallback: LoginCallback) { export async function absorbKey(sessionId: string, key: string) { // Get and store some extra info (salts and keys) const { + sessionExpiry, publicKey, encPrivateKey, encSymmetricKey, @@ -55,9 +58,13 @@ export async function absorbKey(sessionId: string, key: string) { lastName, } = await _whoami(sessionId); const symmetricKeyStr = crypt.decryptAES(key, JSON.parse(encSymmetricKey)); + + const sessionExpiryDate = new Date(Date.now() + (sessionExpiry * 1000)); + // Store the information for later setSessionData( sessionId, + sessionExpiryDate, accountId, firstName, lastName, @@ -98,7 +105,7 @@ export async function changePasswordWithToken(rawNewPassphrase: string, confirma const symmetricKey = JSON.stringify(_getSymmetricKey()); const newEncSymmetricKeyJSON = crypt.encryptAES(newSecret, symmetricKey); const newEncSymmetricKey = JSON.stringify(newEncSymmetricKeyJSON); - return window.main.insomniaFetch({ + await window.main.insomniaFetch({ method: 'POST', path: '/auth/change-password', data: { @@ -113,7 +120,7 @@ export async function changePasswordWithToken(rawNewPassphrase: string, confirma } export function sendPasswordChangeCode() { - return window.main.insomniaFetch({ + window.main.insomniaFetch({ method: 'POST', path: '/auth/send-password-code', sessionId: getCurrentSessionId(), @@ -143,7 +150,19 @@ export function getPrivateKey() { export function getCurrentSessionId() { if (window) { - return window.localStorage.getItem('currentSessionId'); + const sessionId = window.localStorage.getItem('currentSessionId'); + try { + const { sessionExpiry } = JSON.parse(window.localStorage.getItem(_getSessionKey(sessionId)) || '{}'); + const isExpired = sessionExpiry && new Date(sessionExpiry).getTime() < Date.now(); + if (isExpired) { + console.log('Session has expired', sessionExpiry); + return ''; + } + return sessionId; + } catch (e) { + console.log('Error in expiry logic', e); + return ''; + } } else { return ''; } @@ -195,6 +214,7 @@ export async function logout() { /** Set data for the new session and store it encrypted with the sessionId */ export function setSessionData( id: string, + sessionExpiry: Date, accountId: string, firstName: string, lastName: string, @@ -205,6 +225,7 @@ export function setSessionData( ) { const sessionData: SessionData = { id, + sessionExpiry, accountId, symmetricKey, publicKey, @@ -219,13 +240,6 @@ export function setSessionData( window.localStorage.setItem('currentSessionId', id); return sessionData; } -export async function listTeams() { - return window.main.insomniaFetch({ - method: 'GET', - path: '/api/teams', - sessionId: getCurrentSessionId(), - }); -} // ~~~~~~~~~~~~~~~~ // // Helper Functions // @@ -247,7 +261,7 @@ async function _whoami(sessionId: string | null = null): Promise } function _getAuthSalts(email: string) { - return window.main.insomniaFetch({ + return window.main.insomniaFetch<{ saltKey: string; saltAuth: string }>({ method: 'POST', path: '/auth/login-s', data: { email }, diff --git a/packages/insomnia/src/common/__tests__/export.test.ts b/packages/insomnia/src/common/__tests__/export.test.ts index 9c388ad31e..4a8a68aa9d 100644 --- a/packages/insomnia/src/common/__tests__/export.test.ts +++ b/packages/insomnia/src/common/__tests__/export.test.ts @@ -10,6 +10,7 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { beforeEach(async () => { await globalBeforeEach(); await models.project.all(); + await models.settings.getOrCreate(); }); it('exports a single workspace and some requests only as an HTTP Archive', async () => { diff --git a/packages/insomnia/src/common/__tests__/har.test.ts b/packages/insomnia/src/common/__tests__/har.test.ts index c7dff88043..f7690b281a 100644 --- a/packages/insomnia/src/common/__tests__/har.test.ts +++ b/packages/insomnia/src/common/__tests__/har.test.ts @@ -14,6 +14,7 @@ describe('export', () => { beforeEach(async () => { await globalBeforeEach(); await models.project.all(); + await models.settings.getOrCreate(); }); describe('exportHar()', () => { diff --git a/packages/insomnia/src/common/__tests__/import.test.ts b/packages/insomnia/src/common/__tests__/import.test.ts index 25e9564f5c..691d01b100 100644 --- a/packages/insomnia/src/common/__tests__/import.test.ts +++ b/packages/insomnia/src/common/__tests__/import.test.ts @@ -3,8 +3,7 @@ import fs from 'fs'; import path from 'path'; import { globalBeforeEach } from '../../__jest__/before-each'; -import { request, requestGroup, workspace } from '../../models'; -import { DEFAULT_PROJECT_ID } from '../../models/project'; +import { project, request, requestGroup, workspace } from '../../models'; import * as importUtil from '../import'; describe('isApiSpecImport()', () => { @@ -39,20 +38,22 @@ describe('importRaw()', () => { const fixturePath = path.join(__dirname, '..', '__fixtures__', 'curl', 'complex-input.sh'); const content = fs.readFileSync(fixturePath, 'utf8').toString(); + const projectToImportTo = await project.create(); + const scanResult = await importUtil.scanResources({ content, }); - expect(scanResult.type.id).toBe('curl'); + expect(scanResult.type?.id).toBe('curl'); expect(scanResult.errors.length).toBe(0); await importUtil.importResourcesToProject({ - projectId: DEFAULT_PROJECT_ID, + projectId: projectToImportTo._id, }); const workspacesCount = await workspace.count(); const projectWorkspaces = await workspace.findByParentId( - DEFAULT_PROJECT_ID + projectToImportTo._id ); const curlRequests = await request.findByParentId(projectWorkspaces[0]._id); @@ -97,21 +98,21 @@ describe('importRaw()', () => { it('should import a postman collection to a new workspace', async () => { const fixturePath = path.join(__dirname, '..', '__fixtures__', 'postman', 'aws-signature-auth-v2_0-input.json'); const content = fs.readFileSync(fixturePath, 'utf8').toString(); - + const projectToImportTo = await project.create(); const scanResult = await importUtil.scanResources({ content, }); - expect(scanResult.type.id).toBe('postman'); + expect(scanResult.type?.id).toBe('postman'); expect(scanResult.errors.length).toBe(0); await importUtil.importResourcesToProject({ - projectId: DEFAULT_PROJECT_ID, + projectId: projectToImportTo._id, }); const workspacesCount = await workspace.count(); const projectWorkspaces = await workspace.findByParentId( - DEFAULT_PROJECT_ID + projectToImportTo._id ); const requestGroups = await requestGroup.findByParentId(projectWorkspaces[0]._id); diff --git a/packages/insomnia/src/common/__tests__/render.test.ts b/packages/insomnia/src/common/__tests__/render.test.ts index 575a353789..6dc56fb930 100644 --- a/packages/insomnia/src/common/__tests__/render.test.ts +++ b/packages/insomnia/src/common/__tests__/render.test.ts @@ -17,6 +17,7 @@ describe('render tests', () => { beforeEach(async () => { await globalBeforeEach(); await models.project.all(); + await models.settings.getOrCreate(); envBuilder.reset(); reqGroupBuilder.reset(); }); diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index 70c472181f..5e8737ef6e 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -5,6 +5,10 @@ import { KeyCombination } from './settings'; const env = process['env']; // App Stuff +export const getSkipOnboarding = () => env.INSOMNIA_SKIP_ONBOARDING; +export const getInsomniaSession = () => env.INSOMNIA_SESSION; +export const getInsomniaSecretKey = () => env.INSOMNIA_SECRET_KEY; +export const getInsomniaPublicKey = () => env.INSOMNIA_PUBLIC_KEY; export const getAppVersion = () => version; export const getProductName = () => appConfig.productName; export const getAppDefaultTheme = () => appConfig.theme; @@ -132,6 +136,7 @@ export enum UpdateURL { // API export const getApiBaseURL = () => env.INSOMNIA_API_URL || 'https://api.insomnia.rest'; +export const getAIServiceURL = () => env.INSOMNIA_AI_URL || 'https://ai.insomnia.rest'; export const getUpdatesBaseURL = () => env.INSOMNIA_UPDATES_URL || 'https://updates.insomnia.rest'; diff --git a/packages/insomnia/src/common/database.ts b/packages/insomnia/src/common/database.ts index a700b6cc53..956574489e 100644 --- a/packages/insomnia/src/common/database.ts +++ b/packages/insomnia/src/common/database.ts @@ -17,11 +17,12 @@ import { generateId } from './misc'; export interface Query { _id?: string | SpecificQuery; parentId?: string | SpecificQuery | null; - remoteId?: string | null; + remoteId?: string | SpecificQuery | null; plugin?: string; key?: string; environmentId?: string | null; protoFileId?: string; + name?: string | SpecificQuery; } type Sort = Record; @@ -35,6 +36,7 @@ export interface SpecificQuery { $gt?: number; $in?: string[]; $nin?: string[]; + $ne?: string | null; } export type ModelQuery = Partial>; diff --git a/packages/insomnia/src/common/export-all-data.ts b/packages/insomnia/src/common/export-all-data.ts new file mode 100644 index 0000000000..171380d449 --- /dev/null +++ b/packages/insomnia/src/common/export-all-data.ts @@ -0,0 +1,226 @@ +import { mkdir, writeFile } from 'fs/promises'; +import { join } from 'path'; + +import { apiSpec, cookieJar, environment, grpcRequest, project, protoDirectory, protoFile, request, requestGroup, unitTest, unitTestSuite, webSocketPayload, webSocketRequest, workspace } from '../models'; +import { ApiSpec } from '../models/api-spec'; +import { CookieJar } from '../models/cookie-jar'; +import { Environment } from '../models/environment'; +import { GrpcRequest } from '../models/grpc-request'; +import type { Project } from '../models/project'; +import { ProtoDirectory } from '../models/proto-directory'; +import { ProtoFile } from '../models/proto-file'; +import { Request } from '../models/request'; +import { RequestGroup } from '../models/request-group'; +import { UnitTest } from '../models/unit-test'; +import { UnitTestSuite } from '../models/unit-test-suite'; +import { WebSocketPayload } from '../models/websocket-payload'; +import { WebSocketRequest } from '../models/websocket-request'; +import { Workspace } from '../models/workspace'; +import type { Insomnia4Data } from '../utils/importers/importers'; +import { invariant } from '../utils/invariant'; +import { EXPORT_TYPE_API_SPEC, EXPORT_TYPE_COOKIE_JAR, EXPORT_TYPE_ENVIRONMENT, EXPORT_TYPE_GRPC_REQUEST, EXPORT_TYPE_PROTO_DIRECTORY, EXPORT_TYPE_PROTO_FILE, EXPORT_TYPE_REQUEST, EXPORT_TYPE_REQUEST_GROUP, EXPORT_TYPE_UNIT_TEST, EXPORT_TYPE_UNIT_TEST_SUITE, EXPORT_TYPE_WEBSOCKET_PAYLOAD, EXPORT_TYPE_WEBSOCKET_REQUEST, EXPORT_TYPE_WORKSPACE } from './constants'; +import { database } from './database'; + +const exportTypeByModelType = (type: string) => ({ + [request.type]: EXPORT_TYPE_REQUEST, + [webSocketPayload.type]: EXPORT_TYPE_WEBSOCKET_PAYLOAD, + [webSocketRequest.type]: EXPORT_TYPE_WEBSOCKET_REQUEST, + [grpcRequest.type]: EXPORT_TYPE_GRPC_REQUEST, + [requestGroup.type]: EXPORT_TYPE_REQUEST_GROUP, + [unitTestSuite.type]: EXPORT_TYPE_UNIT_TEST_SUITE, + [unitTest.type]: EXPORT_TYPE_UNIT_TEST, + [workspace.type]: EXPORT_TYPE_WORKSPACE, + [cookieJar.type]: EXPORT_TYPE_COOKIE_JAR, + [environment.type]: EXPORT_TYPE_ENVIRONMENT, + [apiSpec.type]: EXPORT_TYPE_API_SPEC, + [protoFile.type]: EXPORT_TYPE_PROTO_FILE, + [protoDirectory.type]: EXPORT_TYPE_PROTO_DIRECTORY, +}[type] || undefined); + +export async function exportWorkspaceData({ + workspaceId, + dirPath, +}: { + workspaceId: string; + dirPath: string; +}) { + const workspaceToExport = await database.get(workspace.type, workspaceId); + invariant(workspaceToExport, `Workspace ${workspaceId} not found`); + const workspaceIds = [workspaceId]; + + const getRequestGroups = async ({ $in }: { $in: string[] }): Promise => { + const requestGroups = await database.find(requestGroup.type, { + parentId: { + $in, + }, + }); + + const requestGroupIds = requestGroups.map(requestGroup => requestGroup._id); + + const childRequestGroups = requestGroupIds.length > 0 ? await getRequestGroups({ + $in: requestGroupIds, + }) : []; + + return [ + ...requestGroups, + ...childRequestGroups, + ]; + }; + + const allRequestGroups = await getRequestGroups({ + $in: workspaceIds, + }); + + const getProtoDirectories = async ({ $in }: { $in: string[] }): Promise => { + const protoDirectories = await database.find(protoDirectory.type, { + parentId: { + $in, + }, + }); + + const protoDirectoryIds = protoDirectories.map(protoDirectory => protoDirectory._id); + + const childProtoDirectories = protoDirectoryIds.length > 0 ? await getProtoDirectories({ + $in: protoDirectoryIds, + }) : []; + + return [ + ...protoDirectories, + ...childProtoDirectories, + ]; + }; + + const allProtoDirectories = await getProtoDirectories({ + $in: workspaceIds, + }); + + const protoFiles = await database.find(protoFile.type, { + parentId: { + $in: allProtoDirectories.map(protoDirectory => protoDirectory._id), + }, + }); + + const requests = await database.find(request.type, { + parentId: { + $in: [ + ...workspaceIds, + ...allRequestGroups.map(requestGroup => requestGroup._id), + ], + }, + }); + + const grpcRequests = await database.find(grpcRequest.type, { + parentId: { + $in: [ + ...workspaceIds, + ...allRequestGroups.map(requestGroup => requestGroup._id), + ], + }, + }); + + const webSocketRequests = await database.find(webSocketRequest.type, { + parentId: { + $in: [ + ...workspaceIds, + ...allRequestGroups.map(requestGroup => requestGroup._id), + ], + }, + }); + + const baseEnvironments = await database.find(environment.type, { + parentId: { + $in: workspaceIds, + }, + }); + + const environments = await database.find(environment.type, { + parentId: { + $in: baseEnvironments.map(environment => environment._id), + }, + }); + + const cookieJars = await database.find(cookieJar.type, { + parentId: { + $in: workspaceIds, + }, + }); + + const apiSpecs = await database.find(apiSpec.type, { + parentId: { + $in: workspaceIds, + }, + }); + + const unitTestSuites = await database.find(unitTestSuite.type, { + parentId: { + $in: workspaceIds, + }, + }); + + const unitTests = await database.find(unitTest.type, { + parentId: { + $in: unitTestSuites.map(unitTestSuite => unitTestSuite._id), + }, + }); + const webSocketPayloads = await database.find(webSocketPayload.type, { + parentId: { + $in: webSocketRequests.map(webSocketRequest => webSocketRequest._id), + }, + }); + + const insomniaExport: Insomnia4Data = { + _type: 'export', + __export_format: 4, + __export_date: new Date(), + __export_source: 'insomnia.desktop.app:v2021.5.0', + resources: [ + workspaceToExport, + ...requests, + ...allRequestGroups, + ...environments, + ...cookieJars, + ...apiSpecs, + ...protoFiles, + ...allProtoDirectories, + ...unitTests, + ...unitTestSuites, + ...webSocketPayloads, + ...webSocketRequests, + ...grpcRequests, + ].map(({ type, ...model }) => ({ + ...model, + _type: exportTypeByModelType(type), + })), + }; + + const filePath = join(dirPath, workspaceId + '.json'); + + try { + await writeFile(filePath, JSON.stringify(insomniaExport)); + } catch (error) { + console.error(error); + } +} + +export async function exportAllData({ + dirPath, +}: { + dirPath: string; +}): Promise { + const insomniaExportFolder = join(dirPath, `insomnia-export.${Date.now()}`); + await mkdir(insomniaExportFolder); + const projects = await database.all(project.type); + + for (const project of projects) { + const workspaces = await database.find(workspace.type, { + parentId: project._id, + }); + + for (const workspace of workspaces) { + await exportWorkspaceData({ + workspaceId: workspace._id, + dirPath: insomniaExportFolder, + }); + } + } +} diff --git a/packages/insomnia/src/common/export.tsx b/packages/insomnia/src/common/export.tsx index d2dd308a66..727fef639a 100644 --- a/packages/insomnia/src/common/export.tsx +++ b/packages/insomnia/src/common/export.tsx @@ -146,7 +146,6 @@ export async function exportRequestsData( format: 'json' | 'yaml', ) { const data: Insomnia4Data = { - // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? _type: 'export', __export_format: EXPORT_FORMAT, __export_date: new Date(), diff --git a/packages/insomnia/src/common/send-request.ts b/packages/insomnia/src/common/send-request.ts index b272aba4b6..7c2ad533bd 100644 --- a/packages/insomnia/src/common/send-request.ts +++ b/packages/insomnia/src/common/send-request.ts @@ -55,7 +55,7 @@ export async function getSendRequestCallbackMemDb(environmentId: string, memDB: const workspace = await models.workspace.getById(workspaceId); invariant(workspace, 'failed to find workspace'); - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); invariant(settings, 'failed to create settings'); const clientCertificates = await models.clientCertificate.findByParentId(workspaceId); const caCert = await models.caCertificate.findByParentId(workspaceId); diff --git a/packages/insomnia/src/index.html b/packages/insomnia/src/index.html index 9871473122..c001c522c7 100644 --- a/packages/insomnia/src/index.html +++ b/packages/insomnia/src/index.html @@ -4,7 +4,7 @@ @@ -37,7 +37,7 @@ cy="36" r="36" fill="none" - stroke="var(--hl)" + 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)" @@ -47,7 +47,7 @@ cy="36" r="36" fill="none" - stroke="var(--hl)" + stroke="var(--hl, rgb(130, 130, 130))" stroke-opacity="0.8" stroke-width="4px" stroke-dasharray="56,172,0,0" diff --git a/packages/insomnia/src/main.development.ts b/packages/insomnia/src/main.development.ts index 66245a46a9..3f7e6c8a2e 100644 --- a/packages/insomnia/src/main.development.ts +++ b/packages/insomnia/src/main.development.ts @@ -9,6 +9,7 @@ import { userDataFolder } from '../config/config.json'; import { changelogUrl, getAppVersion, isDevelopment, isMac } from './common/constants'; import { database } from './common/database'; import log, { initializeLogging } from './common/log'; +import { registerInsomniaStreamProtocol } from './main/api.protocol'; import { backupIfNewerVersionAvailable } from './main/backup'; import { registerElectronHandlers } from './main/ipc/electron'; import { registergRPCHandlers } from './main/ipc/grpc'; @@ -25,6 +26,7 @@ import type { ToastNotification } from './ui/components/toast'; initializeSentry(); +registerInsomniaStreamProtocol(); // Handle potential auto-update if (checkIfRestartNeeded()) { process.exit(0); diff --git a/packages/insomnia/src/main/analytics.ts b/packages/insomnia/src/main/analytics.ts index 2353718fa1..cb34236c4b 100644 --- a/packages/insomnia/src/main/analytics.ts +++ b/packages/insomnia/src/main/analytics.ts @@ -4,19 +4,18 @@ import { v4 as uuidv4 } from 'uuid'; import * as session from '../account/session'; import { getAccountId } from '../account/session'; import { - getApiBaseURL, getAppPlatform, getAppVersion, getProductName, getSegmentWriteKey, } from '../common/constants'; import * as models from '../models/index'; -import { axiosRequest } from './network/axios-request'; +import { insomniaFetch } from './insomniaFetch'; const analytics = new Analytics({ writeKey: getSegmentWriteKey() }); const getDeviceId = async () => { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); return settings.deviceId || (await models.settings.update(settings, { deviceId: uuidv4() })).deviceId; }; @@ -49,7 +48,7 @@ export async function trackSegmentEvent( event: SegmentEvent, properties?: Record, ) { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const allowAnalytics = settings.enableAnalytics || session.isLoggedIn(); if (allowAnalytics) { try { @@ -77,7 +76,7 @@ export async function trackSegmentEvent( } export async function trackPageView(name: string) { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const allowAnalytics = settings.enableAnalytics || session.isLoggedIn(); if (allowAnalytics) { try { @@ -101,12 +100,10 @@ export async function trackPageView(name: string) { export async function sendTelemetry() { if (session.isLoggedIn()) { - axiosRequest({ + insomniaFetch({ method: 'POST', - url: `${getApiBaseURL()}/v1/telemetry/`, - headers: { - 'X-Session-Id': session.getCurrentSessionId(), - }, + path: '/v1/telemetry/', + sessionId: session.getCurrentSessionId(), }).catch((error: unknown) => { console.warn('[analytics] Unexpected error while sending telemetry', error); }); diff --git a/packages/insomnia/src/main/api.protocol.ts b/packages/insomnia/src/main/api.protocol.ts new file mode 100644 index 0000000000..1b416188c6 --- /dev/null +++ b/packages/insomnia/src/main/api.protocol.ts @@ -0,0 +1,30 @@ +import { app, net, protocol } from 'electron'; + +import { getApiBaseURL } from '../common/constants'; +export interface RegisterProtocolOptions { + scheme: string; +} + +const scheme = 'insomnia-event-source'; + +export async function registerInsomniaStreamProtocol() { + protocol.registerSchemesAsPrivileged([{ + scheme, + privileges: { secure: true, standard: true, supportFetchAPI: true }, + }]); + + await app.whenReady(); + + if (protocol.isProtocolHandled(scheme)) { + return; + } + + protocol.handle(scheme, async function(request) { + const apiURL = getApiBaseURL(); + const url = new URL(`${apiURL}/${request.url.replace(`${scheme}://`, '')}`); + const sessionId = new URLSearchParams(url.search).get('sessionId'); + request.headers.append('X-Session-Id', sessionId || ''); + + return net.fetch(url.toString(), request); + }); +} diff --git a/packages/insomnia/src/main/authorizeUserInWindow.ts b/packages/insomnia/src/main/authorizeUserInWindow.ts index 8bbdce6df8..16525e0352 100644 --- a/packages/insomnia/src/main/authorizeUserInWindow.ts +++ b/packages/insomnia/src/main/authorizeUserInWindow.ts @@ -34,7 +34,7 @@ export function authorizeUserInWindow({ // Fetch user setting to determine whether to validate SSL certificates during auth const { validateAuthSSL, - } = await models.settings.getOrCreate(); + } = await models.settings.get(); // Create a child window const child = new BrowserWindow({ diff --git a/packages/insomnia/src/main/backup.ts b/packages/insomnia/src/main/backup.ts index 5728e9ed97..b7df9e8f5d 100644 --- a/packages/insomnia/src/main/backup.ts +++ b/packages/insomnia/src/main/backup.ts @@ -11,7 +11,7 @@ import { insomniaFetch } from './insomniaFetch'; export async function backupIfNewerVersionAvailable() { try { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); console.log('[main] Checking for newer version than ', version); const response = await insomniaFetch<{ url: string }>({ method: 'GET', diff --git a/packages/insomnia/src/main/insomniaFetch.ts b/packages/insomnia/src/main/insomniaFetch.ts index e26adee81c..d8b8ebd361 100644 --- a/packages/insomnia/src/main/insomniaFetch.ts +++ b/packages/insomnia/src/main/insomniaFetch.ts @@ -4,7 +4,7 @@ import { getApiBaseURL, getClientString } from '../common/constants'; import { delay } from '../common/misc'; interface FetchConfig { - method: 'POST' | 'PUT' | 'GET'; + method: 'POST' | 'PUT' | 'GET' | 'DELETE' | 'PATCH'; path: string; sessionId: string | null; data?: unknown; @@ -26,10 +26,13 @@ const exponentialBackOff = async (url: string, init: RequestInit, retries = 0): const response = await net.fetch(url, init); if (response.status === 502 && retries < 5) { retries++; - await delay(retries * 200); + await delay(retries * 1000); + console.log(`Received 502 from ${url} retrying`); return exponentialBackOff(url, init, retries); } if (!response.ok) { + // TODO: review error status code behaviour with backend, should we parse errors here and return response + // or should we rethrow an error with a response object inside? should we be exposing errors to the app UI? console.log(`Response not OK: ${response.status} for ${url}`); } return response; @@ -38,7 +41,7 @@ const exponentialBackOff = async (url: string, init: RequestInit, retries = 0): } }; -export async function insomniaFetch({ method, path, data, sessionId, origin }: FetchConfig): Promise { +export async function insomniaFetch({ method, path, data, sessionId, origin }: FetchConfig): Promise { const config: RequestInit = { method, headers: { @@ -51,13 +54,15 @@ export async function insomniaFetch({ method, path, data, sessionId, or if (sessionId === undefined) { throw new Error(`No session ID provided to ${method}:${path}`); } - const response = await exponentialBackOff(`${origin || getApiBaseURL()}${path}`, config); + + const apiURL = getApiBaseURL(); + const response = await exponentialBackOff(`${origin || apiURL}${path}`, config); const uri = response.headers.get('x-insomnia-command'); if (uri) { for (const window of BrowserWindow.getAllWindows()) { window.webContents.send('shell:open', uri); } } - const isJson = response.headers.get('content-type') === 'application/json' || path.match(/\.json$/); + const isJson = response.headers.get('content-type')?.includes('application/json') || path.match(/\.json$/); return isJson ? response.json() : response.text(); } diff --git a/packages/insomnia/src/main/network/__tests__/axios-request.test.ts b/packages/insomnia/src/main/network/__tests__/axios-request.test.ts index ff207ec0cd..5d93a9ef57 100644 --- a/packages/insomnia/src/main/network/__tests__/axios-request.test.ts +++ b/packages/insomnia/src/main/network/__tests__/axios-request.test.ts @@ -15,7 +15,7 @@ interface AxiosRequestMockUserSettings { } jest.mock('axios'); -jest.spyOn(models.settings, 'getOrCreate'); +jest.spyOn(models.settings, 'get'); describe('axiosRequest used for git-sync', () => { beforeEach(globalBeforeEach); @@ -46,7 +46,7 @@ describe('axiosRequest used for git-sync', () => { noProxy: '', validateSSL: false, }; - (models.settings.getOrCreate as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); + (models.settings.get as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); const response = await axiosRequest({ url: 'https://git.acme.com/username/repo-name.git/git-upload-pack' }); expect(response.config.proxy).toBe(false); }); @@ -59,7 +59,7 @@ describe('axiosRequest used for git-sync', () => { noProxy: 'localhost,127.0.0.1', validateSSL: false, }; - (models.settings.getOrCreate as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); + (models.settings.get as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); const response = await axiosRequest({ url: 'http://git.acme.com/username/repo-name.git/git-upload-pack' }); expect(response.config.proxy).toEqual({ host: 'some.proxy.com', port: 8080 }); }); @@ -72,7 +72,7 @@ describe('axiosRequest used for git-sync', () => { noProxy: 'localhost,127.0.0.1', validateSSL: false, }; - (models.settings.getOrCreate as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); + (models.settings.get as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); const response = await axiosRequest({ url: 'http://git.acme.com/username/repo-name.git/git-upload-pack' }); expect(response.config.proxy).toBe(false); }); @@ -85,7 +85,7 @@ describe('axiosRequest used for git-sync', () => { noProxy: 'localhost,127.0.0.1', validateSSL: false, }; - (models.settings.getOrCreate as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); + (models.settings.get as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); const response = await axiosRequest({ url: 'https://git.acme.com/username/repo-name.git/git-upload-pack' }); expect(response.config.proxy).toEqual({ host: 'some.proxy.com', port: 8080 }); }); @@ -98,7 +98,7 @@ describe('axiosRequest used for git-sync', () => { noProxy: 'localhost,127.0.0.1', validateSSL: false, }; - (models.settings.getOrCreate as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); + (models.settings.get as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); const response = await axiosRequest({ url: 'http://git.acme.com/username/repo-name.git/git-upload-pack' }); expect(response.config.proxy).toEqual({ host: 'some.proxy.com', port: 8081 }); }); @@ -111,7 +111,7 @@ describe('axiosRequest used for git-sync', () => { noProxy: 'localhost,127.0.0.1', validateSSL: false, }; - (models.settings.getOrCreate as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); + (models.settings.get as unknown as jest.Mock).mockImplementation(() => mockInsomniaConfigPanelUserSettings); const response = await axiosRequest({ url: 'https://git.acme.com/username/repo-name.git/git-upload-pack' }); expect(response.config.proxy).toEqual({ host: 'some.proxy.com', port: 8080 }); }); diff --git a/packages/insomnia/src/main/network/axios-request.ts b/packages/insomnia/src/main/network/axios-request.ts index 67b97c0697..6342ed155c 100644 --- a/packages/insomnia/src/main/network/axios-request.ts +++ b/packages/insomnia/src/main/network/axios-request.ts @@ -7,7 +7,7 @@ import * as models from '../../models'; import { isUrlMatchedInNoProxyRule } from '../../network/is-url-matched-in-no-proxy-rule'; import { setDefaultProtocol } from '../../utils/url/protocol'; export const axiosRequest = async (config: AxiosRequestConfig): Promise => { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const isHttps = config.url?.indexOf('https:') === 0; let proxyUrl: string | null = null; diff --git a/packages/insomnia/src/main/network/curl.ts b/packages/insomnia/src/main/network/curl.ts index 301ea25bb5..f9ad83158c 100644 --- a/packages/insomnia/src/main/network/curl.ts +++ b/packages/insomnia/src/main/network/curl.ts @@ -133,7 +133,7 @@ const openCurlConnection = async ( } const readyStateChannel = `curl.${request._id}.readyState`; - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const start = performance.now(); const clientCertificates = await models.clientCertificate.findByParentId(options.workspaceId); const filteredClientCertificates = clientCertificates.filter(c => !c.disabled && urlMatchesCertHost(setDefaultProtocol(c.host, 'https:'), options.url)); @@ -223,7 +223,7 @@ const openCurlConnection = async ( settingStoreCookies: request.settingStoreCookies, bodyCompression: null, }; - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const res = await models.response.create(responsePatch, settings.maxHistoryResponses); models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: res._id }); @@ -271,7 +271,7 @@ const openCurlConnection = async ( }; const createErrorResponse = async (responseId: string, requestId: string, environmentId: string | null, timelinePath: string, message: string) => { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const responsePatch = { _id: responseId, parentId: requestId, diff --git a/packages/insomnia/src/main/network/websocket.ts b/packages/insomnia/src/main/network/websocket.ts index 09bf1f6529..66944f607e 100644 --- a/packages/insomnia/src/main/network/websocket.ts +++ b/packages/insomnia/src/main/network/websocket.ts @@ -179,7 +179,7 @@ const openWebSocketConnection = async ( const lowerCasedEnabledHeaders = headers .filter(({ name, disabled }) => Boolean(name) && !disabled) .reduce(reduceArrayToLowerCaseKeyedDictionary, {}); - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const start = performance.now(); const clientCertificates = await models.clientCertificate.findByParentId(options.workspaceId); @@ -254,7 +254,7 @@ const openWebSocketConnection = async ( settingStoreCookies: request.settingStoreCookies, }; - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const res = await models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses); models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: res._id }); @@ -298,7 +298,7 @@ const openWebSocketConnection = async ( settingSendCookies: request.settingSendCookies, settingStoreCookies: request.settingStoreCookies, }; - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const res = await models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses); models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: res._id }); deleteRequestMaps(request._id, `Unexpected response ${incomingMessage.statusCode}`); @@ -383,7 +383,7 @@ const openWebSocketConnection = async ( }; const createErrorResponse = async (responseId: string, requestId: string, environmentId: string | null, timelinePath: string, message: string) => { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const responsePatch = { _id: responseId, parentId: requestId, diff --git a/packages/insomnia/src/main/sentry.ts b/packages/insomnia/src/main/sentry.ts index ff777bec40..3ff8346762 100644 --- a/packages/insomnia/src/main/sentry.ts +++ b/packages/insomnia/src/main/sentry.ts @@ -13,7 +13,7 @@ let enabled = false; * Watch setting for changes. This must be called after the DB is initialized. */ export function sentryWatchAnalyticsEnabled() { - models.settings.getOrCreate().then(settings => { + models.settings.get().then(settings => { enabled = settings.enableAnalytics || session.isLoggedIn(); }); diff --git a/packages/insomnia/src/main/updates.ts b/packages/insomnia/src/main/updates.ts index 3d396f7b30..816303251a 100644 --- a/packages/insomnia/src/main/updates.ts +++ b/packages/insomnia/src/main/updates.ts @@ -79,14 +79,14 @@ export const init = async () => { if (isUpdateSupported()) { // perhaps disable this method of upgrading just incase it trigger before backup is complete // on app start - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const updateUrl = getUpdateUrl(settings.updateChannel); if (settings.updateAutomatically && updateUrl) { _checkForUpdates(updateUrl); } // on an interval (3h) setInterval(async () => { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const updateUrl = getUpdateUrl(settings.updateChannel); if (settings.updateAutomatically && updateUrl) { _checkForUpdates(updateUrl); @@ -96,7 +96,7 @@ export const init = async () => { // on check now button pushed ipcMain.on('manualUpdateCheck', async () => { console.log('[updater] Manual update check'); - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const updateUrl = isUpdateSupported() && getUpdateUrl(settings.updateChannel); if (!updateUrl) { _sendUpdateStatus('Updates Not Supported'); diff --git a/packages/insomnia/src/models/environment.ts b/packages/insomnia/src/models/environment.ts index c7dc6c3cf6..3b29e60aa4 100644 --- a/packages/insomnia/src/models/environment.ts +++ b/packages/insomnia/src/models/environment.ts @@ -1,7 +1,7 @@ import * as crypto from 'crypto'; import { database as db } from '../common/database'; -import type { BaseModel } from './index'; +import { type BaseModel } from './index'; export const name = 'Environment'; export const type = 'Environment'; @@ -70,13 +70,26 @@ export async function getOrCreateForParentId(parentId: string) { }); if (!environments.length) { - return create({ - parentId, - name: 'Base Environment', - // Deterministic base env ID. It helps reduce sync complexity since we won't have to - // de-duplicate environments. - _id: `${prefix}_${crypto.createHash('sha1').update(parentId).digest('hex')}`, - }); + // Deterministic base env ID. It helps reduce sync complexity since we won't have to + // de-duplicate environments. + const baseEnvironmentId = `${prefix}_${crypto.createHash('sha1').update(parentId).digest('hex')}`; + try { + const baseEnvironment = await create({ + parentId, + name: 'Base Environment', + _id: baseEnvironmentId, + }); + + return baseEnvironment; + } catch (e) { + const existingEnvironment = await getById(baseEnvironmentId); + + if (existingEnvironment) { + return existingEnvironment; + } + + throw e; + } } return environments[environments.length - 1]; diff --git a/packages/insomnia/src/models/helpers/__tests__/project.test.ts b/packages/insomnia/src/models/helpers/__tests__/project.test.ts index 41ab70086f..ee327b93f5 100644 --- a/packages/insomnia/src/models/helpers/__tests__/project.test.ts +++ b/packages/insomnia/src/models/helpers/__tests__/project.test.ts @@ -1,12 +1,8 @@ import { describe, expect, it } from '@jest/globals'; -import { DEFAULT_PROJECT_ID } from '../../project'; import { sortProjects } from '../project'; -const defaultProject = { name: 'a', remoteId: null, _id: DEFAULT_PROJECT_ID }; - -const localA = { name: 'a', remoteId: null, _id: 'localA' }; -const localB = { name: 'b', remoteId: null, _id: 'localB' }; +const defaultOrgProject = { name: 'a', remoteId: 'proj_team_123456789345678987654', _id: 'not important' }; const remoteA = { name: 'a', remoteId: 'notNull', _id: 'remoteA' }; const remoteB = { name: 'b', remoteId: 'notNull', _id: 'remoteB' }; @@ -16,18 +12,14 @@ describe('sortProjects', () => { it('sorts projects by default > local > remote > name', () => { const unSortedProjects = [ remoteA, - localB, - defaultProject, + defaultOrgProject, remoteB, - localA, remote0, ]; const result = sortProjects(unSortedProjects); const sortedProjects = [ - defaultProject, - localA, - localB, + defaultOrgProject, remote0, remoteA, remoteB, diff --git a/packages/insomnia/src/models/helpers/project.ts b/packages/insomnia/src/models/helpers/project.ts index 6ed48fcd11..074dfcbfa0 100644 --- a/packages/insomnia/src/models/helpers/project.ts +++ b/packages/insomnia/src/models/helpers/project.ts @@ -1,8 +1,7 @@ -import { isDefaultProject, isLocalProject, isRemoteProject, Project } from '../project'; +import { isDefaultOrganizationProject, Project } from '../project'; export const sortProjects = (projects: Project[]) => [ - ...projects.filter(isDefaultProject), - ...projects.filter(p => isLocalProject(p) && !isDefaultProject(p)) + ...projects.filter(p => isDefaultOrganizationProject(p)) .sort((a, b) => a.name.localeCompare(b.name)), - ...projects.filter(isRemoteProject) + ...projects.filter(p => !isDefaultOrganizationProject(p)) .sort((a, b) => a.name.localeCompare(b.name)), ]; diff --git a/packages/insomnia/src/models/organization.ts b/packages/insomnia/src/models/organization.ts index cbac55c6e5..80a38f0753 100644 --- a/packages/insomnia/src/models/organization.ts +++ b/packages/insomnia/src/models/organization.ts @@ -1,17 +1,30 @@ - -export const type = 'Organization'; -export const prefix = 'org'; - -export interface Organization { - _id: string; - name: string; +interface Branding { + logo_url: string; } -export const DEFAULT_ORGANIZATION_ID = `${prefix}_default-project`; +export interface Metadata { + organizationType: string; + ownerAccountId: string; +} -export const defaultOrganization: Organization = { - _id: DEFAULT_ORGANIZATION_ID, - name: 'Personal Projects', -}; +export interface Organization { + id: string; + name: string; + display_name: string; + branding?: Branding; + metadata: Metadata; +} +export const SCRATCHPAD_ORGANIZATION_ID = 'org_scratchpad'; +export const isScratchpadOrganizationId = (organizationId: string) => + organizationId === SCRATCHPAD_ORGANIZATION_ID; +export const isPersonalOrganization = (organization: Organization) => + organization.metadata.organizationType === 'personal'; -export const isDefaultOrganization = (organization: Organization) => organization._id === DEFAULT_ORGANIZATION_ID; +export const isOwnerOfOrganization = ({ + organization, + accountId, +}: { + organization: Organization; + accountId: string; +}) => + organization.metadata.ownerAccountId === accountId; diff --git a/packages/insomnia/src/models/project.ts b/packages/insomnia/src/models/project.ts index 2832fb66b8..3a9bb1cbdf 100644 --- a/packages/insomnia/src/models/project.ts +++ b/packages/insomnia/src/models/project.ts @@ -1,6 +1,6 @@ import { database as db } from '../common/database'; import { generateId } from '../common/misc'; -import type { BaseModel } from './index'; +import { type BaseModel } from './index'; export const name = 'Project'; export const type = 'Project'; @@ -8,13 +8,12 @@ export const prefix = 'proj'; export const canDuplicate = false; export const canSync = false; -export const DEFAULT_PROJECT_ID = `${prefix}_default-project`; +export const SCRATCHPAD_PROJECT_ID = `${prefix}_scratchpad`; -export const isDefaultProject = (project: Pick) => project._id === DEFAULT_PROJECT_ID; -export const isNotDefaultProject = (project: Pick) => !isDefaultProject(project); +export const isScratchpadProject = (project: Pick) => project._id === SCRATCHPAD_PROJECT_ID; export const isLocalProject = (project: Pick): project is LocalProject => project.remoteId === null; export const isRemoteProject = (project: Pick): project is RemoteProject => !isLocalProject(project); -export const projectHasSettings = (project: Pick) => !isDefaultProject(project); +export const projectHasSettings = (project: Pick) => !isScratchpadProject(project); interface CommonProject { name: string; @@ -77,3 +76,9 @@ export async function all() { const projects = await db.all(type); return projects; } + +export function isDefaultOrganizationProject(project: Project) { + // legacy remoteId = proj_team_xxx + // new remoteId = proj_org_xxx + return project.remoteId?.startsWith('proj_team') || project.remoteId?.startsWith('proj_org'); +} diff --git a/packages/insomnia/src/models/response.ts b/packages/insomnia/src/models/response.ts index e6b4b274c9..20b8d26089 100644 --- a/packages/insomnia/src/models/response.ts +++ b/packages/insomnia/src/models/response.ts @@ -118,7 +118,7 @@ export async function all() { } export async function removeForRequest(parentId: string, environmentId?: string | null) { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const query: Record = { parentId, }; @@ -149,7 +149,7 @@ async function _findRecentForRequest( }; // Filter responses by environment if setting is enabled - if ((await models.settings.getOrCreate()).filterResponsesByEnv) { + if ((await models.settings.get()).filterResponsesByEnv) { query.environmentId = environmentId; } @@ -177,7 +177,7 @@ export async function create(patch: Partial = {}, maxResponses = 20): const requestVersion = request ? await models.requestVersion.create(request) : null; patch.requestVersionId = requestVersion ? requestVersion._id : null; // Filter responses by environment if setting is enabled - const shouldQueryByEnvId = (await models.settings.getOrCreate()).filterResponsesByEnv && patch.hasOwnProperty('environmentId'); + const shouldQueryByEnvId = (await models.settings.get()).filterResponsesByEnv && patch.hasOwnProperty('environmentId'); const query = { parentId, ...(shouldQueryByEnvId ? { environmentId: patch.environmentId } : {}), diff --git a/packages/insomnia/src/models/settings.ts b/packages/insomnia/src/models/settings.ts index f8024f8800..c117e94c78 100644 --- a/packages/insomnia/src/models/settings.ts +++ b/packages/insomnia/src/models/settings.ts @@ -117,6 +117,12 @@ export async function getOrCreate() { return results[0]; } +export async function get() { + const results = await db.all(type) || []; + + return results[0]; +} + /** * Ensure map is updated when new hotkeys are added */ diff --git a/packages/insomnia/src/models/websocket-response.ts b/packages/insomnia/src/models/websocket-response.ts index 31cf2a0a96..c1b6f51477 100644 --- a/packages/insomnia/src/models/websocket-response.ts +++ b/packages/insomnia/src/models/websocket-response.ts @@ -91,7 +91,7 @@ export async function all() { } export async function removeForRequest(parentId: string, environmentId?: string | null) { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const query: Record = { parentId, }; @@ -128,7 +128,7 @@ export async function create(patch: Partial = {}, maxResponse }; if ( - (await models.settings.getOrCreate()).filterResponsesByEnv && + (await models.settings.get()).filterResponsesByEnv && patch.hasOwnProperty('environmentId') ) { query.environmentId = patch.environmentId; @@ -158,7 +158,7 @@ async function _findRecentForRequest( }; // Filter responses by environment if setting is enabled - if ((await models.settings.getOrCreate()).filterResponsesByEnv) { + if ((await models.settings.get()).filterResponsesByEnv) { query.environmentId = environmentId; } diff --git a/packages/insomnia/src/models/workspace.ts b/packages/insomnia/src/models/workspace.ts index 4d87701f00..e99cd2664c 100644 --- a/packages/insomnia/src/models/workspace.ts +++ b/packages/insomnia/src/models/workspace.ts @@ -4,7 +4,7 @@ import { database as db } from '../common/database'; import { strings } from '../common/strings'; import type { BaseModel } from './index'; import * as models from './index'; -import { DEFAULT_PROJECT_ID, isProjectId } from './project'; +import { isProjectId } from './project'; export const name = 'Workspace'; export const type = 'Workspace'; @@ -51,7 +51,6 @@ export function migrate(doc: Workspace) { doc = _migrateExtractClientCertificates(doc); doc = _migrateEnsureName(doc); doc = _migrateScope(doc); - doc = _migrateIntoDefaultProject(doc); return doc; } catch (e) { console.log('[db] Error during workspace migration', e); @@ -147,17 +146,14 @@ function _migrateScope(workspace: MigrationWorkspace) { return workspace as Workspace; } -function _migrateIntoDefaultProject(workspace: Workspace) { - if (!workspace.parentId) { - console.log(`[db] No workspace parentId found for ${workspace._id} setting default ${DEFAULT_PROJECT_ID}`); - workspace.parentId = DEFAULT_PROJECT_ID; - } - - return workspace; -} - function expectParentToBeProject(parentId?: string | null) { if (parentId && !isProjectId(parentId)) { throw new Error('Expected the parent of a Workspace to be a Project'); } } + +export const SCRATCHPAD_WORKSPACE_ID = 'wrk_scratchpad'; + +export function isScratchpad(workspace: Workspace) { + return workspace._id === SCRATCHPAD_WORKSPACE_ID; +} diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index 5fb7f9d340..bc7272abaa 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -56,7 +56,7 @@ export const fetchRequestData = async (requestId: string) => { const environment = activeEnvironment || await models.environment.getOrCreateForParentId(workspace._id); invariant(environment, 'failed to find environment ' + activeEnvironmentId); - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); invariant(settings, 'failed to create settings'); const clientCertificates = await models.clientCertificate.findByParentId(workspaceId); const caCert = await models.caCertificate.findByParentId(workspaceId); diff --git a/packages/insomnia/src/plugins/context/__tests__/data.test.ts b/packages/insomnia/src/plugins/context/__tests__/data.test.ts index cdee400bb2..7a432573be 100644 --- a/packages/insomnia/src/plugins/context/__tests__/data.test.ts +++ b/packages/insomnia/src/plugins/context/__tests__/data.test.ts @@ -6,7 +6,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import { getAppVersion } from '../../../common/constants'; import { database as db } from '../../../common/database'; import * as models from '../../../models/index'; -import { DEFAULT_PROJECT_ID, Project } from '../../../models/project'; +import { Project } from '../../../models/project'; import { WorkspaceScopeKeys } from '../../../models/workspace'; import * as plugin from '../data'; @@ -16,7 +16,8 @@ describe('init()', () => { beforeEach(globalBeforeEach); it('initializes correctly', async () => { - const { data } = plugin.init(DEFAULT_PROJECT_ID); + const project = await models.project.create(); + const { data } = plugin.init(project._id); expect(Object.keys(data)).toEqual(['import', 'export']); expect(Object.keys(data.export).sort()).toEqual(['har', 'insomnia']); expect(Object.keys(data.import).sort()).toEqual(['raw', 'uri']); @@ -35,6 +36,7 @@ describe('app.import.*', () => { modified: 222, parentId: project._id, }); + await models.settings.getOrCreate(); }); it('uri', async () => { @@ -135,6 +137,7 @@ describe('app.export.*', () => { beforeEach(async () => { await globalBeforeEach(); project = await models.project.create(); + await models.settings.getOrCreate(); await models.workspace.create({ _id: 'wrk_1', created: 111, diff --git a/packages/insomnia/src/plugins/index.ts b/packages/insomnia/src/plugins/index.ts index 83f8d8d284..ecba2847e4 100644 --- a/packages/insomnia/src/plugins/index.ts +++ b/packages/insomnia/src/plugins/index.ts @@ -184,7 +184,7 @@ export async function getPlugins(force = false): Promise { } if (!plugins) { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); const allConfigs: PluginConfigMap = settings.pluginConfig; const extraPaths = settings.pluginPath .split(':') diff --git a/packages/insomnia/src/plugins/themes/default.ts b/packages/insomnia/src/plugins/themes/default.ts index 512fca1f20..a0e9382db0 100644 --- a/packages/insomnia/src/plugins/themes/default.ts +++ b/packages/insomnia/src/plugins/themes/default.ts @@ -4,7 +4,7 @@ const sidebarBackground = { notice: '#f0e137', warning: '#ff9a1f', danger: '#ff5631', - surprise: '#a896ff', + surprise: '#4000BF', info: '#46c1e6', }; @@ -62,7 +62,7 @@ export default { notice: '#d8c84d', warning: '#ec8702', danger: '#e15251', - surprise: '#8776d5', + surprise: '#4000BF', info: '#20aed9', }, }, diff --git a/packages/insomnia/src/sync/git/__tests__/ne-db-client.test.ts b/packages/insomnia/src/sync/git/__tests__/ne-db-client.test.ts index 129e2f8732..c1b484e0e5 100644 --- a/packages/insomnia/src/sync/git/__tests__/ne-db-client.test.ts +++ b/packages/insomnia/src/sync/git/__tests__/ne-db-client.test.ts @@ -7,7 +7,6 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import { database as db } from '../../../common/database'; import * as models from '../../../models'; import { workspaceModelSchema } from '../../../models/__schemas__/model-schemas'; -import { DEFAULT_PROJECT_ID } from '../../../models/project'; import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME } from '../git-vcs'; import { NeDBClient } from '../ne-db-client'; import { assertAsyncError, setupDateMocks } from './util'; @@ -21,9 +20,12 @@ describe('NeDBClient', () => { workspaceBuilder.reset(); setupDateMocks(); // Create some sample models - await models.project.all(); + await models.project.create({ + _id: 'proj_1', + }); await models.workspace.create({ _id: 'wrk_1', + parentId: 'proj_1', }); await models.request.create({ _id: 'req_1', @@ -43,7 +45,7 @@ describe('NeDBClient', () => { describe('readdir()', () => { it('reads model IDs from model type folders', async () => { - const neDbClient = new NeDBClient('wrk_1', DEFAULT_PROJECT_ID); + const neDbClient = new NeDBClient('wrk_1', 'proj_1'); const reqDir = path.join(GIT_INSOMNIA_DIR, models.request.type); const wrkDir = path.join(GIT_INSOMNIA_DIR, models.workspace.type); expect(await neDbClient.readdir(GIT_CLONE_DIR)).toEqual([GIT_INSOMNIA_DIR_NAME]); @@ -71,7 +73,7 @@ describe('NeDBClient', () => { const wrk1Yml = path.join(GIT_INSOMNIA_DIR, models.workspace.type, 'wrk_1.yml'); const req1Yml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_1.yml'); const reqXYml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_x.yml'); - const pNeDB = new NeDBClient('wrk_1', DEFAULT_PROJECT_ID); + const pNeDB = new NeDBClient('wrk_1', 'proj_1'); expect(YAML.parse((await pNeDB.readFile(wrk1Yml, 'utf8')).toString())).toEqual( expect.objectContaining({ _id: 'wrk_1', @@ -100,7 +102,7 @@ describe('NeDBClient', () => { type: 'file', }); // Act - const neDbClient = new NeDBClient('wrk_1', DEFAULT_PROJECT_ID); + const neDbClient = new NeDBClient('wrk_1', 'proj_1'); // Assert expect(await neDbClient.stat(GIT_CLONE_DIR)).toEqual(dirType); expect(await neDbClient.stat(GIT_INSOMNIA_DIR)).toEqual(dirType); @@ -115,7 +117,7 @@ describe('NeDBClient', () => { // Arrange const upsertSpy = jest.spyOn(db, 'upsert'); const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId, DEFAULT_PROJECT_ID); + const neDbClient = new NeDBClient(workspaceId, 'proj_1'); const env = { _id: 'env_1', type: models.environment.type, @@ -133,7 +135,7 @@ describe('NeDBClient', () => { it('should write files in GIT_INSOMNIA_DIR directory to db', async () => { // Arrange const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId, DEFAULT_PROJECT_ID); + const neDbClient = new NeDBClient(workspaceId, 'proj_1'); const upsertSpy = jest.spyOn(db, 'upsert'); const env = { _id: 'env_1', @@ -180,7 +182,7 @@ describe('NeDBClient', () => { it('should throw error if id does not match', async () => { // Arrange const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId, DEFAULT_PROJECT_ID); + const neDbClient = new NeDBClient(workspaceId, 'proj_1'); const env = { _id: 'env_1', type: models.environment.type, @@ -198,7 +200,7 @@ describe('NeDBClient', () => { it('should throw error if type does not match', async () => { // Arrange const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId, DEFAULT_PROJECT_ID); + const neDbClient = new NeDBClient(workspaceId, 'proj_1'); const env = { _id: 'env_1', type: models.environment.type, @@ -217,7 +219,7 @@ describe('NeDBClient', () => { describe('mkdir()', () => { it('should throw error', async () => { const workspaceId = 'wrk_1'; - const neDbClient = new NeDBClient(workspaceId, DEFAULT_PROJECT_ID); + const neDbClient = new NeDBClient(workspaceId, 'proj_1'); const promiseResult = neDbClient.mkdir(); await expect(promiseResult).rejects.toThrowError('NeDBClient is not writable'); }); diff --git a/packages/insomnia/src/sync/git/github-oauth-provider.ts b/packages/insomnia/src/sync/git/github-oauth-provider.ts index 81ae3da3fa..72a65668b3 100644 --- a/packages/insomnia/src/sync/git/github-oauth-provider.ts +++ b/packages/insomnia/src/sync/git/github-oauth-provider.ts @@ -4,7 +4,6 @@ import { getApiBaseURL, getAppWebsiteBaseURL, getGitHubGraphQLApiURL } from '../ export const GITHUB_TOKEN_STORAGE_KEY = 'github-oauth-token'; export const GITHUB_GRAPHQL_API_URL = getGitHubGraphQLApiURL(); -const getOauthPageURL = () => getAppWebsiteBaseURL() + '/oauth/github'; /** * This cache stores the states that are generated for the OAuth flow. @@ -18,7 +17,7 @@ export function generateAuthorizationUrl() { const scopes = ['repo', 'read:user', 'user:email']; const scope = scopes.join(' '); - const url = new URL(getOauthPageURL()); + const url = new URL(getAppWebsiteBaseURL() + '/oauth/github'); statesCache.add(state); @@ -43,8 +42,10 @@ export async function exchangeCodeForToken({ ); } + const apiURL = getApiBaseURL(); + return window.main.axiosRequest({ - url: getApiBaseURL() + '/v1/oauth/github', + url: apiURL + '/v1/oauth/github', method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/insomnia/src/sync/git/gitlab-oauth-provider.ts b/packages/insomnia/src/sync/git/gitlab-oauth-provider.ts index a7deccacb7..a7d9c02a6d 100644 --- a/packages/insomnia/src/sync/git/gitlab-oauth-provider.ts +++ b/packages/insomnia/src/sync/git/gitlab-oauth-provider.ts @@ -25,9 +25,10 @@ const getGitLabConfig = async () => { }; } + const apiURL = getApiBaseURL(); // Otherwise fetch the config for the GitLab API return window.main.axiosRequest({ - url: getApiBaseURL() + '/v1/oauth/gitlab/config', + url: apiURL + '/v1/oauth/gitlab/config', method: 'GET', }).then(({ data }) => { return { diff --git a/packages/insomnia/src/sync/vcs/__tests__/initialize-backend-project.test.ts b/packages/insomnia/src/sync/vcs/__tests__/initialize-backend-project.test.ts index 13cd8fa4bd..e004dc1c04 100644 --- a/packages/insomnia/src/sync/vcs/__tests__/initialize-backend-project.test.ts +++ b/packages/insomnia/src/sync/vcs/__tests__/initialize-backend-project.test.ts @@ -28,7 +28,7 @@ describe('initialize-backend-project', () => { const workspaceMeta = await models.workspaceMeta.create({ parentId: workspace._id }); vcs.clearBackendProject(); - await pushSnapshotOnInitialize({ vcs, project, workspace, workspaceMeta }); + await pushSnapshotOnInitialize({ vcs, project, workspace }); expect(pushSpy).not.toHaveBeenCalled(); await expect(models.workspaceMeta.getByParentId(workspace._id)).resolves.toStrictEqual(workspaceMeta); @@ -40,7 +40,7 @@ describe('initialize-backend-project', () => { const workspaceMeta = await models.workspaceMeta.create({ parentId: workspace._id }); vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name); - await pushSnapshotOnInitialize({ vcs, project, workspace, workspaceMeta }); + await pushSnapshotOnInitialize({ vcs, project, workspace }); expect(pushSpy).not.toHaveBeenCalled(); await expect(models.workspaceMeta.getByParentId(workspace._id)).resolves.toStrictEqual(workspaceMeta); @@ -50,36 +50,22 @@ describe('initialize-backend-project', () => { const project = await models.project.create({ remoteId: 'abc' }); const anotherProject = await models.project.create({ remoteId: 'def' }); const workspace = await models.workspace.create({ parentId: anotherProject._id }); - const workspaceMeta = await models.workspaceMeta.create({ parentId: workspace._id }); vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name); - await pushSnapshotOnInitialize({ vcs, project, workspace, workspaceMeta }); + await pushSnapshotOnInitialize({ vcs, project, workspace }); expect(pushSpy).not.toHaveBeenCalled(); - // await expect(models.workspaceMeta.getByParentId(workspace._id)).resolves.toStrictEqual(workspaceMeta); - }); - - it('should not push snapshot if not marked for push', async () => { - const project = await models.project.create({ remoteId: 'abc' }); - const workspace = await models.workspace.create({ parentId: project._id }); - const workspaceMeta = await models.workspaceMeta.create({ parentId: workspace._id, pushSnapshotOnInitialize: false }); - vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name); - - await pushSnapshotOnInitialize({ vcs, project, workspace, workspaceMeta }); - - expect(pushSpy).not.toHaveBeenCalled(); - await expect(models.workspaceMeta.getByParentId(workspace._id)).resolves.toStrictEqual(workspaceMeta); }); it('should push snapshot if conditions are met', async () => { - const project = await models.project.create({ remoteId: 'abc' }); + const project = await models.project.create({ remoteId: 'abc', parentId: 'team_abc' }); const workspace = await models.workspace.create({ parentId: project._id }); - const workspaceMeta = await models.workspaceMeta.create({ parentId: workspace._id, pushSnapshotOnInitialize: true }); + await models.workspaceMeta.create({ parentId: workspace._id, pushSnapshotOnInitialize: true }); vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name); - await pushSnapshotOnInitialize({ vcs, project, workspace, workspaceMeta }); + await pushSnapshotOnInitialize({ vcs, project, workspace }); - expect(pushSpy).toHaveBeenCalledWith(project.remoteId); + expect(pushSpy).toHaveBeenCalledWith({ teamId: 'team_abc', teamProjectId: project.remoteId }); const updatedMeta = await models.workspaceMeta.getByParentId(workspace._id); expect(updatedMeta?.pushSnapshotOnInitialize).toBe(false); }); diff --git a/packages/insomnia/src/sync/vcs/__tests__/initialize-model-from.test.ts b/packages/insomnia/src/sync/vcs/__tests__/initialize-model-from.test.ts deleted file mode 100644 index 00fa7bfff2..0000000000 --- a/packages/insomnia/src/sync/vcs/__tests__/initialize-model-from.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createBuilder } from '@develohpanda/fluent-builder'; -import { beforeEach, describe, expect, it } from '@jest/globals'; - -import { globalBeforeEach } from '../../../__jest__/before-each'; -import * as models from '../../../models'; -import { isRemoteProject, Project } from '../../../models/project'; -import { isCollection, Workspace, WorkspaceScopeKeys } from '../../../models/workspace'; -import { projectSchema, teamSchema } from '../../__schemas__/type-schemas'; -import { initializeProjectFromTeam, initializeWorkspaceFromBackendProject } from '../initialize-model-from'; - -const teamBuilder = createBuilder(teamSchema); -const backendProjectBuilder = createBuilder(projectSchema); - -describe('initializeProjectFromTeam', () => { - beforeEach(globalBeforeEach); - - it('should initialize project with properties from the team', async () => { - // Arrange - const team = teamBuilder.build(); - - // Act - const project = await initializeProjectFromTeam(team); - - // Assert - expect(isRemoteProject(project)).toBe(true); - expect(project).toMatchObject>({ - _id:`${models.project.prefix}_${team.id}`, - remoteId: team.id, - name: team.name, - }); - }); -}); - -describe('initializeWorkspaceFromProject', () => { - beforeEach(globalBeforeEach); - - it('should initialize workspac with properties from the backendProject and project', async () => { - // Arrange - const project = await models.project.create(); - const backendProject = backendProjectBuilder.build(); - - // Act - const workspace = await initializeWorkspaceFromBackendProject(backendProject, project); - - // Assert - expect(isCollection(workspace)).toBe(true); - expect(workspace).toMatchObject>({ - _id: backendProject.rootDocumentId, - name: backendProject.name, - parentId: project._id, - scope: WorkspaceScopeKeys.collection, - }); - }); -}); diff --git a/packages/insomnia/src/sync/vcs/__tests__/migrate-collections.test.ts b/packages/insomnia/src/sync/vcs/__tests__/migrate-collections.test.ts index 92113ad0ad..32ae3e4624 100644 --- a/packages/insomnia/src/sync/vcs/__tests__/migrate-collections.test.ts +++ b/packages/insomnia/src/sync/vcs/__tests__/migrate-collections.test.ts @@ -4,11 +4,9 @@ import { mocked } from 'jest-mock'; import { globalBeforeEach } from '../../../__jest__/before-each'; import { isLoggedIn as _isLoggedIn } from '../../../account/session'; -import { database } from '../../../common/database'; import * as models from '../../../models'; import { backendProjectWithTeamSchema, teamSchema } from '../../__schemas__/type-schemas'; import MemoryDriver from '../../store/drivers/memory-driver'; -import { initializeProjectFromTeam } from '../initialize-model-from'; import { migrateCollectionsIntoRemoteProject } from '../migrate-collections'; import { VCS } from '../vcs'; @@ -78,60 +76,4 @@ describe('migrateCollectionsIntoRemoteProject', () => { expect(vcs.remoteBackendProjectsInAnyTeam).not.toHaveBeenCalled(); await expect(models.workspace.getById(workspaceInLocal._id)).resolves.toStrictEqual(workspaceInLocal); }); - - it('does migrate if collection in non-remote project with local backend project - create remote project', async () => { - // Arrange - const vcs = newMockedVcs(); - const localProject = await models.project.create(); - const workspaceInLocal = await models.workspace.create({ parentId: localProject._id }); - - const team = teamBuilder.build(); - const remoteBackendProjectWithTeam = projectWithTeamBuilder - .rootDocumentId(workspaceInLocal._id) - .team(team) - .build(); - - vcs.hasBackendProjectForRootDocument.mockResolvedValue(true); // has local backend project - vcs.remoteBackendProjectsInAnyTeam.mockResolvedValue([remoteBackendProjectWithTeam]); // has local backend project - - // Act - await migrateCollectionsIntoRemoteProject(vcs); - - // Assert - expect(vcs.remoteBackendProjectsInAnyTeam).toHaveBeenCalledTimes(1); - const createdRemoteProject = await models.project.getByRemoteId(team.id); - await expect(models.workspace.getById(workspaceInLocal._id)).resolves.toMatchObject({ - ...workspaceInLocal, - parentId: createdRemoteProject?._id, - }); - }); - - it('does migrate if collection in non-remote project with local backend project - use existing remote project', async () => { - // Arrange - const vcs = newMockedVcs(); - const localProject = await models.project.create(); - const workspaceInLocal = await models.workspace.create({ parentId: localProject._id }); - - const team = teamBuilder.build(); - const existingRemoteProject = await initializeProjectFromTeam(team); - await database.batchModifyDocs({ upsert: [existingRemoteProject] }); - - const remoteProjectWithTeam = projectWithTeamBuilder - .rootDocumentId(workspaceInLocal._id) - .team(team) - .build(); - - vcs.hasBackendProjectForRootDocument.mockResolvedValue(true); // has local backend project - vcs.remoteBackendProjectsInAnyTeam.mockResolvedValue([remoteProjectWithTeam]); // has local backend project - - // Act - await migrateCollectionsIntoRemoteProject(vcs); - - // Assert - expect(vcs.remoteBackendProjectsInAnyTeam).toHaveBeenCalledTimes(1); - await expect(models.workspace.getById(workspaceInLocal._id)).resolves.toMatchObject({ - ...workspaceInLocal, - parentId: existingRemoteProject?._id, - }); - }); }); diff --git a/packages/insomnia/src/sync/vcs/__tests__/pull-backend-project.test.ts b/packages/insomnia/src/sync/vcs/__tests__/pull-backend-project.test.ts index 27d3e6ca0c..dace872530 100644 --- a/packages/insomnia/src/sync/vcs/__tests__/pull-backend-project.test.ts +++ b/packages/insomnia/src/sync/vcs/__tests__/pull-backend-project.test.ts @@ -5,7 +5,6 @@ import { mocked } from 'jest-mock'; import { globalBeforeEach } from '../../../__jest__/before-each'; import { DEFAULT_BRANCH_NAME } from '../../../common/constants'; import * as models from '../../../models'; -import { isRemoteProject } from '../../../models/project'; import { Workspace } from '../../../models/workspace'; import { backendProjectWithTeamSchema } from '../../__schemas__/type-schemas'; import MemoryDriver from '../../store/drivers/memory-driver'; @@ -49,7 +48,7 @@ describe('pullBackendProject()', () => { }); // Act - await pullBackendProject({ vcs, backendProject, remoteProjects: [project].filter(isRemoteProject) }); + await pullBackendProject({ vcs, backendProject, remoteProject: project }); // Assert expect(project?.name).not.toBe(backendProject.team.name); // should not rename if the project already exists @@ -57,51 +56,60 @@ describe('pullBackendProject()', () => { const workspaces = await models.workspace.all(); expect(workspaces).toHaveLength(1); const workspace = workspaces[0]; - expect(workspace).toStrictEqual(expect.objectContaining>({ + const expectedWorkspace: Partial = { _id: backendProject.rootDocumentId, name: backendProject.name, parentId: project._id, scope: 'collection', - })); + }; + + expect(workspace).toEqual(expect.objectContaining(expectedWorkspace)); }); it('should insert a project and workspace with parent', async () => { + const project = await models.project.create({ + name: `${backendProject.team.name} unique`, + remoteId: backendProject.team.id, + }); // Act - await pullBackendProject({ vcs, backendProject, remoteProjects: [] }); + await pullBackendProject({ vcs, backendProject, remoteProject: project }); // Assert - const project = await models.project.getByRemoteId(backendProject.team.id); - expect(project?.name).toBe(backendProject.team.name); - const workspaces = await models.workspace.all(); expect(workspaces).toHaveLength(1); const workspace = workspaces[0]; - expect(workspace).toStrictEqual(expect.objectContaining>({ + const expectedWorkspace: Partial = { _id: backendProject.rootDocumentId, name: backendProject.name, parentId: project?._id, scope: 'collection', - })); + }; + + expect(workspace).toStrictEqual(expect.objectContaining(expectedWorkspace)); }); it('should update a workspace if the name or parentId is different', async () => { // Arrange await models.workspace.create({ _id: backendProject.rootDocumentId, name: 'someName' }); - + const project = await models.project.create({ + name: `${backendProject.team.name} unique`, + remoteId: backendProject.team.id, + }); // Act - await pullBackendProject({ vcs, backendProject, remoteProjects: [] }); + await pullBackendProject({ vcs, backendProject, remoteProject: project }); // Assert - const project = await models.project.getByRemoteId(backendProject.team.id); const workspaces = await models.workspace.all(); expect(workspaces).toHaveLength(1); const workspace = workspaces[0]; - expect(workspace).toStrictEqual(expect.objectContaining>({ + const expectedWorkspace: Partial = { _id: backendProject.rootDocumentId, name: backendProject.name, parentId: project?._id, - })); + }; + + expect(workspace).toStrictEqual(expect.objectContaining(expectedWorkspace)); }); }); @@ -113,28 +121,31 @@ describe('pullBackendProject()', () => { const existingReq = await models.request.create({ parentId: existingWrk._id }); vcs.allDocuments.mockResolvedValue([existingWrk, existingReq]); - + const project = await models.project.create({ + name: `${backendProject.team.name} unique`, + remoteId: backendProject.team.id, + }); // Act - await pullBackendProject({ vcs, backendProject, remoteProjects: [] }); + await pullBackendProject({ vcs, backendProject, remoteProject: project }); // Assert - const project = await models.project.getByRemoteId(backendProject.team.id); - const workspaces = await models.workspace.all(); expect(workspaces).toHaveLength(1); const workspace = workspaces[0]; - expect(workspace).toStrictEqual(expect.objectContaining>({ + const expectedWorkspace: Partial = { _id: backendProject.rootDocumentId, name: backendProject.name, parentId: project?._id, - })); + }; + + expect(workspace).toStrictEqual(expect.objectContaining(expectedWorkspace)); const requests = await models.request.all(); expect(requests).toHaveLength(1); const request = requests[0]; expect(request).toStrictEqual(existingReq); - expect(vcs.pull).toHaveBeenCalledWith([], project?.remoteId); + expect(vcs.pull).toHaveBeenCalledWith({ candidates: [], teamId: project?.parentId, teamProjectId: project?._id }); }); }); @@ -143,7 +154,7 @@ describe('pullBackendProject()', () => { vcs.getRemoteBranches.mockRejectedValue(new Error('invalid access to project')); // Act - const action = () => pullBackendProject({ vcs, backendProject, remoteProjects: [] }); + const action = () => pullBackendProject({ vcs, backendProject, remoteProject: { remoteId: '' } }); // Assert expect(vcs.pull).not.toHaveBeenCalled(); diff --git a/packages/insomnia/src/sync/vcs/initialize-backend-project.ts b/packages/insomnia/src/sync/vcs/initialize-backend-project.ts index 262b339aa9..0d1c1c7e18 100644 --- a/packages/insomnia/src/sync/vcs/initialize-backend-project.ts +++ b/packages/insomnia/src/sync/vcs/initialize-backend-project.ts @@ -3,7 +3,6 @@ import * as models from '../../models'; import { BaseModel, canSync } from '../../models'; import { Project } from '../../models/project'; import { Workspace } from '../../models/workspace'; -import { WorkspaceMeta } from '../../models/workspace-meta'; import { StatusCandidate } from '../types'; import { VCS } from './vcs'; @@ -34,16 +33,13 @@ export const initializeLocalBackendProjectAndMarkForSync = async ({ vcs, workspa export const pushSnapshotOnInitialize = async ({ vcs, workspace, - workspaceMeta, - project: { _id: projectId, remoteId: projectRemoteId }, + project: { _id: projectId, remoteId: projectRemoteId, parentId }, }: { vcs: VCS; workspace: Workspace; - workspaceMeta?: WorkspaceMeta; project: Project; }) => { const projectIsForWorkspace = projectId === workspace.parentId; - const markedForPush = workspaceMeta?.pushSnapshotOnInitialize; // A race condition occurs in App.tsx when updating the active workspace // One code path is that a React Key updates, forcing all children to unmount and remount (https://github.com/Kong/insomnia/blob/9a943879060927d6ab1c21d3e12daba39ad05eea/packages/insomnia-app/app/ui/containers/app.tsx#L1514-L1514) @@ -51,8 +47,8 @@ export const pushSnapshotOnInitialize = async ({ // This race condition causes us to hit this codepath twice while activating a workspace but the first time it has no project so we shouldn't do anything const hasProject = vcs.hasBackendProject(); - if (markedForPush && projectIsForWorkspace && projectRemoteId && hasProject) { + if (projectIsForWorkspace && projectRemoteId && hasProject) { await models.workspaceMeta.updateByParentId(workspace._id, { pushSnapshotOnInitialize: false }); - await vcs.push(projectRemoteId); + await vcs.push({ teamId: parentId, teamProjectId: projectRemoteId }); } }; diff --git a/packages/insomnia/src/sync/vcs/initialize-model-from.ts b/packages/insomnia/src/sync/vcs/initialize-model-from.ts deleted file mode 100644 index 84a837efbd..0000000000 --- a/packages/insomnia/src/sync/vcs/initialize-model-from.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as models from '../../models'; -import { RemoteProject } from '../../models/project'; -import { Project } from '../../models/project'; -import { Workspace, WorkspaceScopeKeys } from '../../models/workspace'; -import { Team } from '../types'; -import { BackendProject } from '../types'; - -export const initializeProjectFromTeam = (team: Team) => models.initModel( - models.project.type, - { - _id: `${models.project.prefix}_${team.id}`, - remoteId: team.id, - name: team.name, - } -); - -export const initializeWorkspaceFromBackendProject = (backendProject: BackendProject, project: Project) => models.initModel( - models.workspace.type, - { - _id: backendProject.rootDocumentId, - name: backendProject.name, - parentId: project._id, - scope: WorkspaceScopeKeys.collection, - } -); diff --git a/packages/insomnia/src/sync/vcs/migrate-collections.ts b/packages/insomnia/src/sync/vcs/migrate-collections.ts index 26f48ee38e..8473e82768 100644 --- a/packages/insomnia/src/sync/vcs/migrate-collections.ts +++ b/packages/insomnia/src/sync/vcs/migrate-collections.ts @@ -4,8 +4,8 @@ import { isNotNullOrUndefined } from '../../common/misc'; import * as models from '../../models'; import { isRemoteProject, RemoteProject } from '../../models/project'; import { isCollection, Workspace } from '../../models/workspace'; +import { invariant } from '../../utils/invariant'; import { Team } from '../types'; -import { initializeProjectFromTeam } from './initialize-model-from'; import { VCS } from './vcs'; export const logCollectionMovedToProject = (collection: Workspace, remoteProject: RemoteProject) => { @@ -21,6 +21,7 @@ export const logCollectionMovedToProject = (collection: Workspace, remoteProject }); }; +// Migrate any collections with sync setup and no remote project into a remote project export const migrateCollectionsIntoRemoteProject = async (vcs: VCS) => { console.log('[sync] checking for collections which need to be moved into a remote project'); @@ -56,12 +57,9 @@ export const migrateCollectionsIntoRemoteProject = async (vcs: VCS) => { continue; } - let remoteProject = findRemoteProjectByTeam(remoteBackendProject.team); + const remoteProject = findRemoteProjectByTeam(remoteBackendProject.team); - if (!remoteProject) { - remoteProject = await initializeProjectFromTeam(remoteBackendProject.team); - upsert.push(remoteProject); - } + invariant(remoteProject, 'expected remote project to exist'); collection.parentId = remoteProject._id; upsert.push(collection); diff --git a/packages/insomnia/src/sync/vcs/migrate-to-cloud-projects.ts b/packages/insomnia/src/sync/vcs/migrate-to-cloud-projects.ts new file mode 100644 index 0000000000..1385346760 --- /dev/null +++ b/packages/insomnia/src/sync/vcs/migrate-to-cloud-projects.ts @@ -0,0 +1,115 @@ +import { getCurrentSessionId } from '../../account/session'; +import { database } from '../../common/database'; +import * as models from '../../models'; +import { Project, RemoteProject } from '../../models/project'; +import { Workspace } from '../../models/workspace'; +import { invariant } from '../../utils/invariant'; +import { initializeLocalBackendProjectAndMarkForSync, pushSnapshotOnInitialize } from './initialize-backend-project'; +import { VCS } from './vcs'; + +let status: 'idle' | 'pending' | 'error' | 'completed' = 'idle'; + +// TODO: +// Error handling and return type for errors + +// Migration: +// Team ~= Project > Workspaces +// In the previous API: { _id: 'proj_team_123', remoteId: 'team_123', parentId: null } + +// Organization > TeamProject > Workspaces +// In the new API: { _id: 'proj_team_123', remoteId: 'proj_team_123', parentId: 'team_123' } + +export const shouldRunMigration = async () => { + const localProjects = await database.find(models.project.type, { + remoteId: null, + _id: { $ne: models.project.SCRATCHPAD_PROJECT_ID }, + }); + const legacyRemoteProjects = await database.find(models.project.type, { + remoteId: { $ne: null }, + parentId: null, + }); + + return localProjects.length > 0 || legacyRemoteProjects.length > 0; +}; + +// Second time we run this, whats gonna happen? +// we will get duplicate projects in the cloud and local with the same but no workspaces in the duplicate + +export const migrateLocalToCloudProjects = async (vcs: VCS) => { + if (status !== 'idle' && status !== 'error') { + return; + } + + status = 'pending'; + + try { + const sessionId = getCurrentSessionId(); + invariant(sessionId, 'User must be logged in to migrate projects'); + + // Migrate legacy remote projects to new organization structure + const legacyRemoteProjects = await database.find(models.project.type, { + remoteId: { $ne: null }, + parentId: null, + }); + + for (const remoteProject of legacyRemoteProjects) { + await models.project.update(remoteProject, { + // Remote Id was previously the teamId + parentId: remoteProject.remoteId, + // _id was previously the remoteId + remoteId: remoteProject._id, + }); + } + + // Local projects except scratchpad + const localProjects = await database.find(models.project.type, { + remoteId: null, + _id: { $ne: models.project.SCRATCHPAD_PROJECT_ID }, + }); + for (const localProject of localProjects) { + // -- Unsafe to run twice, will cause duplicates unless we would need to match ids + const newCloudProject = await window.main.insomniaFetch<{ id: string; name: string; organizationId: string } | void>({ + path: '/v1/organizations/personal/team-projects', + method: 'POST', + data: { + name: localProject.name, + }, + sessionId, + }); + + invariant(typeof newCloudProject?.id === 'string', 'Failed to create remote project'); + + const project = await models.project.update(localProject, { + name: newCloudProject.name, + remoteId: newCloudProject.id, + parentId: newCloudProject.organizationId, + }); + + // For each workspace in the local project + const projectWorkspaces = (await database.find(models.workspace.type, { + parentId: localProject._id, + })); + + for (const workspace of projectWorkspaces) { + const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); + + // Initialize Sync on the workspace if it's not using Git sync + try { + if (!workspaceMeta.gitRepositoryId) { + invariant(vcs, 'VCS must be initialized'); + + await initializeLocalBackendProjectAndMarkForSync({ vcs, workspace }); + await pushSnapshotOnInitialize({ vcs, workspace, project }); + } + } catch (e) { + console.warn('Failed to initialize sync on workspace. This will be retried when the workspace is opened on the app.', e); + // TODO: here we should show the try again dialog + } + } + } + + status = 'completed'; + } catch (err) { + console.warn('Failed to migrate projects to cloud', err); + } +}; diff --git a/packages/insomnia/src/sync/vcs/pull-backend-project.ts b/packages/insomnia/src/sync/vcs/pull-backend-project.ts index 575a96acf4..e9da03f3b7 100644 --- a/packages/insomnia/src/sync/vcs/pull-backend-project.ts +++ b/packages/insomnia/src/sync/vcs/pull-backend-project.ts @@ -1,11 +1,8 @@ import { DEFAULT_BRANCH_NAME } from '../../common/constants'; import { database } from '../../common/database'; +import * as models from '../../models'; import { RemoteProject } from '../../models/project'; -import { isWorkspace } from '../../models/workspace'; -import { - initializeProjectFromTeam, - initializeWorkspaceFromBackendProject, -} from './initialize-model-from'; +import { isWorkspace, Workspace } from '../../models/workspace'; import { BackendProjectWithTeam } from './normalize-backend-project-team'; import { interceptAccessError } from './util'; import { VCS } from './vcs'; @@ -13,14 +10,10 @@ import { VCS } from './vcs'; interface Options { vcs: VCS; backendProject: BackendProjectWithTeam; - remoteProjects: RemoteProject[]; + remoteProject: RemoteProject; } -export const pullBackendProject = async ({ - vcs, - backendProject, - remoteProjects, -}: Options) => { +export const pullBackendProject = async ({ vcs, backendProject, remoteProject }: Options) => { // Set backend project, checkout master, and pull await vcs.setBackendProject(backendProject); await vcs.checkout([], DEFAULT_BRANCH_NAME); @@ -32,35 +25,32 @@ export const pullBackendProject = async ({ const defaultBranchMissing = !remoteBranches.includes(DEFAULT_BRANCH_NAME); - // Find or create the remote project locally - let project = remoteProjects.find( - ({ remoteId }) => remoteId === backendProject.team.id - ); - if (!project) { - project = await initializeProjectFromTeam(backendProject.team); - await database.upsert(project); - } - let workspaceId; // The default branch does not exist, so we create it and the workspace locally if (defaultBranchMissing) { - const workspace = await initializeWorkspaceFromBackendProject( - backendProject, - project + const workspace = await models.initModel( + models.workspace.type, + { + _id: backendProject.rootDocumentId, + name: backendProject.name, + parentId: remoteProject._id, + scope: 'collection', + }, ); + await database.upsert(workspace); workspaceId = workspace._id; } else { - await vcs.pull([], project.remoteId); // There won't be any existing docs since it's a new pull + await vcs.pull({ candidates: [], teamId: remoteProject.parentId, teamProjectId: remoteProject._id }); // There won't be any existing docs since it's a new pull const flushId = await database.bufferChanges(); // @ts-expect-error -- TSCONVERSION for (const doc of (await vcs.allDocuments()) || []) { if (isWorkspace(doc)) { - doc.parentId = project._id; + doc.parentId = remoteProject._id; workspaceId = doc._id; } await database.upsert(doc); @@ -69,5 +59,5 @@ export const pullBackendProject = async ({ await database.flushChanges(flushId); } - return { project, workspaceId }; + return { project: remoteProject, workspaceId }; }; diff --git a/packages/insomnia/src/sync/vcs/vcs.ts b/packages/insomnia/src/sync/vcs/vcs.ts index 5ae8f50f08..4a864dd925 100644 --- a/packages/insomnia/src/sync/vcs/vcs.ts +++ b/packages/insomnia/src/sync/vcs/vcs.ts @@ -122,10 +122,6 @@ export class VCS { await this.setBackendProject(project); } - async teams() { - return this._queryTeams(); - } - async backendProjectTeams() { return this._queryBackendProjectTeams(); } @@ -134,12 +130,33 @@ export class VCS { return this._allBackendProjects(); } - async remoteBackendProjects(teamId: string) { - return this._queryBackendProjects(teamId); + async remoteBackendProjects({ teamId, teamProjectId }: { teamId: string; teamProjectId: string }) { + return this._queryBackendProjects(teamId, teamProjectId); } async remoteBackendProjectsInAnyTeam() { - return this._queryBackendProjects(); + const { projects } = await this._runGraphQL( + ` + query ($teamId: ID, $teamProjectId: ID) { + projects(teamId: $teamId, teamProjectId: $teamProjectId) { + id + name + rootDocumentId + teams { + id + name + } + } + } + `, + { + teamId: '', + teamProjectId: '', + }, + 'projects', + ); + + return (projects as BackendProjectWithTeams[]).map(normalizeBackendProjectTeam); } async blobFromLastSnapshot(key: string) { @@ -479,8 +496,8 @@ export class VCS { console.log(`[sync] Created snapshot ${snapshot.id} (${name})`); } - async pull(candidates: StatusCandidate[], teamId: string | undefined | null) { - await this._getOrCreateRemoteBackendProject(teamId || ''); + async pull({ candidates, teamId, teamProjectId }: { candidates: StatusCandidate[]; teamId: string; teamProjectId: string }) { + await this._getOrCreateRemoteBackendProject({ teamId, teamProjectId }); const localBranch = await this._getCurrentBranch(); const tmpBranchForRemote = await this.customFetch(localBranch.name + '.hidden', localBranch.name); // Merge branch and ensure that we use the remote's history when merging @@ -498,29 +515,29 @@ export class VCS { return delta; } - async _getOrCreateRemoteBackendProject(teamId: string) { + async _getOrCreateRemoteBackendProject({ teamId, teamProjectId }: { teamId: string; teamProjectId: string }) { const localProject = await this._assertBackendProject(); let remoteProject = await this._queryProject(); if (!remoteProject) { - remoteProject = await this._createRemoteProject(localProject, teamId); + remoteProject = await this._createRemoteProject({ ...localProject, teamId, teamProjectId }); } await this._storeBackendProject(remoteProject); return remoteProject; } - async _createRemoteProject({ rootDocumentId, name }: BackendProject, teamId: string) { + async _createRemoteProject({ rootDocumentId, name, teamId, teamProjectId }: BackendProject & { teamId: string; teamProjectId: string }) { if (!teamId) { throw new Error('teamId should be defined'); } const teamKeys = await this._queryTeamMemberKeys(teamId); - return this._queryCreateProject(rootDocumentId, name, teamId, teamKeys.memberKeys); + return this._queryCreateProject(rootDocumentId, name, teamId, teamProjectId, teamKeys.memberKeys); } - async push(teamId: string | undefined | null) { - await this._getOrCreateRemoteBackendProject(teamId || ''); + async push({ teamId, teamProjectId }: { teamId: string; teamProjectId: string }) { + await this._getOrCreateRemoteBackendProject({ teamId, teamProjectId }); const branch = await this._getCurrentBranch(); // Check branch history to make sure there are no conflicts let lastMatchingIndex = 0; @@ -1011,27 +1028,11 @@ export class VCS { return projectKey.encSymmetricKey as string; } - async _queryTeams() { - const { teams } = await this._runGraphQL( - ` - query { - teams { - id - name - } - } - `, - {}, - 'teams', - ); - return teams as Team[]; - } - - async _queryBackendProjects(teamId?: string) { + async _queryBackendProjects(teamId: string, teamProjectId: string) { const { projects } = await this._runGraphQL( ` - query ($teamId: ID) { - projects(teamId: $teamId) { + query ($teamId: ID, $teamProjectId: ID) { + projects(teamId: $teamId, teamProjectId: $teamProjectId) { id name rootDocumentId @@ -1044,6 +1045,7 @@ export class VCS { `, { teamId, + teamProjectId, }, 'projects', ); @@ -1101,8 +1103,14 @@ export class VCS { memberKeys: { accountId: string; publicKey: string; + autoLinked: boolean; }[]; }> { + console.log('[sync] Fetching team member keys', { + teamId, + sessionId: session.getCurrentSessionId(), + }); + const { teamMemberKeys } = await this._runGraphQL( ` query ($teamId: ID!) { @@ -1110,6 +1118,7 @@ export class VCS { memberKeys { accountId publicKey + autoLinked } } } @@ -1126,24 +1135,27 @@ export class VCS { workspaceId: string, workspaceName: string, teamId: string, + teamProjectId: string, teamPublicKeys?: { accountId: string; publicKey: string; + autoLinked: boolean; }[], ) { // Generate symmetric key for ResourceGroup const symmetricKey = await crypt.generateAES256Key(); const symmetricKeyStr = JSON.stringify(symmetricKey); - const teamKeys: {accountId: string; encSymmetricKey: string}[] = []; + const teamKeys: { accountId: string; encSymmetricKey: string; autoLinked: boolean }[] = []; if (!teamId || !teamPublicKeys?.length) { throw new Error('teamId and teamPublicKeys must not be null or empty!'); } // Encrypt the symmetric key with the public keys of all the team members, ourselves included - for (const { accountId, publicKey } of teamPublicKeys) { + for (const { accountId, publicKey, autoLinked } of teamPublicKeys) { teamKeys.push({ + autoLinked, accountId, encSymmetricKey: crypt.encryptRSAWithJWK(JSON.parse(publicKey), symmetricKeyStr), }); @@ -1156,6 +1168,7 @@ export class VCS { $id: ID!, $rootDocumentId: ID!, $teamId: ID, + $teamProjectId: ID, $teamKeys: [ProjectCreateKeyInput!], ) { projectCreate( @@ -1164,6 +1177,7 @@ export class VCS { rootDocumentId: $rootDocumentId, teamId: $teamId, teamKeys: $teamKeys, + teamProjectId: $teamProjectId ) { id name @@ -1177,6 +1191,7 @@ export class VCS { rootDocumentId: workspaceId, teamId: teamId, teamKeys: teamKeys, + teamProjectId, }, 'createProject', ); diff --git a/packages/insomnia/src/ui/auth-session-provider.ts b/packages/insomnia/src/ui/auth-session-provider.ts index a426239f18..e3c54167b2 100644 --- a/packages/insomnia/src/ui/auth-session-provider.ts +++ b/packages/insomnia/src/ui/auth-session-provider.ts @@ -1,21 +1,29 @@ import { decodeBase64, encodeBase64 } from '@getinsomnia/api-client/base64'; import { keyPair, open } from '@getinsomnia/api-client/sealedbox'; -import * as Sentry from '@sentry/electron'; import * as session from '../account/session'; -import { getAppWebsiteBaseURL } from '../common/constants'; +import { getAppWebsiteBaseURL, getInsomniaPublicKey, getInsomniaSecretKey } from '../common/constants'; import { invariant } from '../utils/invariant'; interface AuthBox { token: string; key: string; } + const sessionKeyPair = keyPair(); encodeBase64(sessionKeyPair.publicKey).then(res => { - window.localStorage.setItem('insomnia.publicKey', res); + try { + window.localStorage.setItem('insomnia.publicKey', getInsomniaPublicKey() || res); + } catch (error) { + console.error('Failed to store public key in localStorage.'); + } }); encodeBase64(sessionKeyPair.secretKey).then(res => { - window.localStorage.setItem('insomnia.secretKey', res); + try { + window.localStorage.setItem('insomnia.secretKey', getInsomniaSecretKey() || res); + } catch (error) { + console.error('Failed to store secret key in localStorage.'); + } }); /** * Keypair used for the login handshake. @@ -33,16 +41,21 @@ export async function submitAuthCode(code: string) { const box: AuthBox = JSON.parse(decoder.decode(boxData)); await session.absorbKey(box.token, box.key); } catch (error) { - Sentry.captureException(error); - throw error; + return error.message; } } -export async function getLoginUrl() { +export function getLoginUrl() { const publicKey = window.localStorage.getItem('insomnia.publicKey'); if (!publicKey) { console.log('No public key found'); return ''; } - return `${getAppWebsiteBaseURL()}/app/auth-app/?loginKey=${encodeURIComponent(publicKey)}`; + + const url = new URL(getAppWebsiteBaseURL()); + + url.pathname = '/app/auth-app/'; + url.searchParams.set('loginKey', encodeURIComponent(publicKey)); + + return url.toString(); } diff --git a/packages/insomnia/src/ui/components/avatar.tsx b/packages/insomnia/src/ui/components/avatar.tsx new file mode 100644 index 0000000000..a7363867bf --- /dev/null +++ b/packages/insomnia/src/ui/components/avatar.tsx @@ -0,0 +1,186 @@ +import React, { Suspense } from 'react'; +import styled, { keyframes } from 'styled-components'; + +import { Tooltip } from './tooltip'; + +const getNameInitials = (name?: string) => { + // Split on whitespace and take first letter of each word + const words = name?.toUpperCase().split(' ') || []; + const firstWord = words[0]; + const lastWord = words[words.length - 1]; + + // If there is only one word, just take the first letter + if (words.length === 1) { + return firstWord.charAt(0); + } + + // If the first word is an emoji or an icon then just use that + const iconMatch = firstWord.match(/\p{Extended_Pictographic}/u); + if (iconMatch) { + return iconMatch[0]; + } + + return `${firstWord.charAt(0)}${lastWord ? lastWord.charAt(0) : ''}`; +}; + +const appearIn = keyframes` + 0% { + opacity: 0; + scale: 0.8; + } + + 50% { + opacity: 1; + scale: 1.1; + } + + 100% { + opacity: 1; + scale: 1; + } +`; + +const ImageElement = styled.img<{ + size: 'small' | 'medium'; + animate: boolean; +}>` + border: 2px solid var(--color-bg); + box-sizing: border-box; + outline: none; + border-radius: 50%; + object-fit: cover; + object-position: center; + background-size: cover; + background-position: center; + animation: ${({ animate }) => (animate ? appearIn : 'none')} 0.2s ease-in-out; + width: ${({ size }) => (size === 'small' ? '20px' : '24px')}; + height: ${({ size }) => (size === 'small' ? '20px' : '24px')}; +`; + +const AvatarImage = ({ src, alt, size, animate }: { src: string; alt: string; size: 'small' | 'medium'; animate: boolean }) => { + imgCache.read(src); + return ; +}; + +const AvatarPlaceholder = styled.div<{size: 'small' | 'medium'; animate: boolean}>` + border: 2px solid var(--color-bg); + box-sizing: border-box; + outline: none; + border-radius: 50%; + width: ${({ size }) => size === 'small' ? '20px' : '24px'}; + height: ${({ size }) => size === 'small' ? '20px' : '24px'}; + object-fit: cover; + object-position: center; + background-size: cover; + background-position: center; + margin: 0!important; + animation: ${({ animate }) => animate ? appearIn : 'none'} 0.2s ease-in-out; + background-color: var(--color-surprise); + color: var(--color-font-surprise); + display: flex; + margin-left: ${({ size }) => size === 'small' ? '-5px' : '-6px'}; + align-items: center; + justify-content: center; + font-size: ${({ size }) => size === 'small' ? 'var(--font-size-xxs)' : 'var(--font-size-xs)'}; + font-weight: bold; +`; + +// https://css-tricks.com/pre-caching-image-with-react-suspense/ +// can be improved when api sends image expiry headers +class ImageCache { + __cache: Record | string; timestamp: number }> = {}; + ttl: number; + + constructor({ ttl }: { ttl: number }) { + this.ttl = ttl; + } + + read(src: string) { + const now = Date.now(); + if (this.__cache[src] && typeof this.__cache[src].value !== 'string') { + // If the value is a Promise, throw it to indicate that the cache is still loading + throw this.__cache[src].value; + } else if (this.__cache[src] && now - this.__cache[src].timestamp < this.ttl) { + // If the value is a string and hasn't expired, return it + return this.__cache[src].value; + } else { + // Otherwise, load the image and add it to the cache + const promise = new Promise(resolve => { + const img = new Image(); + img.onload = () => { + const value = src; + this.__cache[src] = { value, timestamp: now }; + resolve(value); + }; + img.src = src; + }); + this.__cache[src] = { value: promise, timestamp: now }; + throw promise; + } + } +} + +// Cache images for 10 minutes +const imgCache = new ImageCache({ ttl: 1000 * 60 * 10 }); + +export const Avatar = ({ src, alt, size = 'medium', animate }: { src: string; alt: string; size?: 'small' | 'medium'; animate?: boolean }) => { + if (!src) { + return {getNameInitials(alt)}; + } + + return ( + + {getNameInitials(alt)}}> + + + + ); +}; + +export const AvatarGroup = ({ items, maxAvatars = 3, size = 'medium', animate = false }: { items: {src: string; alt: string}[]; maxAvatars?: number; size: 'small' | 'medium'; animate?: boolean }) => { + const avatars = items.slice(0, maxAvatars); + const overflow = items.length - maxAvatars; + + return ( + }> +
+ {avatars.map((avatar, index) => ( + + ))} + {overflow > 0 && ( + ( +
{avatar.alt}
+ )) + } + > + {`+${overflow}`} +
+ )} +
+
+ ); +}; diff --git a/packages/insomnia/src/ui/components/buttons/grpc-send-button.tsx b/packages/insomnia/src/ui/components/buttons/grpc-send-button.tsx index d58ed79d21..4388898629 100644 --- a/packages/insomnia/src/ui/components/buttons/grpc-send-button.tsx +++ b/packages/insomnia/src/ui/components/buttons/grpc-send-button.tsx @@ -21,7 +21,13 @@ const buttonProps: ButtonProps = { export const GrpcSendButton: FunctionComponent = ({ running, methodType, handleStart, handleCancel }) => { if (running) { return ( - ); @@ -29,14 +35,26 @@ export const GrpcSendButton: FunctionComponent = ({ running, methodType, if (!methodType) { return ( - ); } return ( - ); diff --git a/packages/insomnia/src/ui/components/codemirror/code-editor.tsx b/packages/insomnia/src/ui/components/codemirror/code-editor.tsx index 3f1ef3cfb0..91062c7db1 100644 --- a/packages/insomnia/src/ui/components/codemirror/code-editor.tsx +++ b/packages/insomnia/src/ui/components/codemirror/code-editor.tsx @@ -8,7 +8,6 @@ import { ModifiedGraphQLJumpOptions } from 'codemirror-graphql/jump'; import deepEqual from 'deep-equal'; import { JSONPath } from 'jsonpath-plus'; import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { useMount, useUnmount } from 'react-use'; import vkBeautify from 'vkbeautify'; @@ -20,7 +19,7 @@ import { NunjucksParsedTag } from '../../../templating/utils'; import { jsonPrettify } from '../../../utils/prettify/json'; import { queryXPath } from '../../../utils/xpath/query'; import { useGatedNunjucks } from '../../context/nunjucks/use-gated-nunjucks'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { Dropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown'; import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { FilterHelpModal } from '../modals/filter-help-modal'; @@ -172,7 +171,7 @@ export const CodeEditor = forwardRef(({ const [originalCode, setOriginalCode] = useState(''); const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const indentSize = settings.editorIndentSize; const indentWithTabs = shouldIndentWithTabs({ mode, indentWithTabs: settings.editorIndentWithTabs }); const indentChars = indentWithTabs ? '\t' : new Array((indentSize || TAB_SIZE) + 1).join(' '); diff --git a/packages/insomnia/src/ui/components/codemirror/extensions/clickable.ts b/packages/insomnia/src/ui/components/codemirror/extensions/clickable.ts index cb02913a79..25a600f5c2 100644 --- a/packages/insomnia/src/ui/components/codemirror/extensions/clickable.ts +++ b/packages/insomnia/src/ui/components/codemirror/extensions/clickable.ts @@ -32,16 +32,17 @@ CodeMirror.defineExtension('makeLinksClickable', function(this: CodeMirror.Edito movedDuringClick = false; }); el.addEventListener('mouseup', event => { - if (movedDuringClick) { + if (movedDuringClick || !event.target) { return; } - // @ts-expect-error -- type unsoundness - const cls = event.target.className; + if (event.target instanceof HTMLElement) { + const cls = event.target.className; - if (cls.indexOf('cm-clickable') >= 0) { - // @ts-expect-error -- mapping unsoundness - handleClick(decode(event.target.innerHTML)); + if (cls.indexOf('cm-clickable') >= 0) { + handleClick(decode(event.target.innerHTML)); + } } + }); }); diff --git a/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx b/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx index 5bd0cfde32..902155b7d0 100644 --- a/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx +++ b/packages/insomnia/src/ui/components/codemirror/one-line-editor.tsx @@ -4,7 +4,6 @@ import classnames from 'classnames'; import clone from 'clone'; import CodeMirror, { EditorConfiguration } from 'codemirror'; import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { useMount, useUnmount } from 'react-use'; import { DEBOUNCE_MILLIS } from '../../../common/constants'; @@ -13,7 +12,7 @@ import { KeyCombination } from '../../../common/settings'; import { getTagDefinitions } from '../../../templating/index'; import { NunjucksParsedTag } from '../../../templating/utils'; import { useNunjucks } from '../../context/nunjucks/use-nunjucks'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { isKeyCombinationInRegistry } from '../settings/shortcuts'; export interface OneLineEditorProps { defaultValue: string; @@ -46,7 +45,7 @@ export const OneLineEditor = forwardRef const codeMirror = useRef(null); const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { handleRender, handleGetRenderContext } = useNunjucks(); useMount(() => { diff --git a/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx index 8c6f4c2a82..40563e02be 100644 --- a/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/git-sync-dropdown.tsx @@ -258,12 +258,12 @@ export const GitSyncDropdown: FC = ({ className, gitRepository, isInsomni = ({ className, gitRepository, isInsomni triggerButton={ void; +} + export const ProjectDropdown: FC = ({ project, organizationId }) => { const [isProjectSettingsModalOpen, setIsProjectSettingsModalOpen] = useState(false); const deleteProjectFetcher = useFetcher(); - const projectActionList: { - id: string; - name: string; - icon: IconName; - action: (projectId: string) => void; - }[] = [ + const projectActionList: ProjectActionItem[] = [ { id: 'settings', name: 'Settings', icon: 'gear', action: () => setIsProjectSettingsModalOpen(true), }, - { + ...!isDefaultOrganizationProject(project) ? [{ id: 'delete', name: 'Delete', icon: 'trash', @@ -49,8 +53,18 @@ export const ProjectDropdown: FC = ({ project, organizationId }) => { action: `/organization/${organizationId}/project/${projectId}/delete`, } ), - }, + }] satisfies ProjectActionItem[] : [], ]; + + useEffect(() => { + if (deleteProjectFetcher.data && deleteProjectFetcher.data.error && deleteProjectFetcher.state === 'idle') { + showAlert({ + title: 'Could not delete project', + message: deleteProjectFetcher.data.error, + }); + } + }, [deleteProjectFetcher.data, deleteProjectFetcher.state]); + return ( diff --git a/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx index 8cec027541..3b986be2e0 100644 --- a/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/request-actions-dropdown.tsx @@ -1,7 +1,7 @@ import { IconName } from '@fortawesome/fontawesome-svg-core'; import React, { Fragment, useCallback, useState } from 'react'; import { Button, Item, Menu, MenuTrigger, Popover } from 'react-aria-components'; -import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom'; +import { useFetcher, useParams } from 'react-router-dom'; import { exportHarRequest } from '../../../common/har'; import { toKebabCase } from '../../../common/misc'; @@ -19,15 +19,14 @@ import type { RequestAction } from '../../../plugins'; import { getRequestActions } from '../../../plugins'; import * as pluginContexts from '../../../plugins/context/index'; import { useRequestMetaPatcher, useRequestPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; -import { type DropdownProps } from '../base/dropdown'; +import { useRootLoaderData } from '../../routes/root'; import { Icon } from '../icon'; import { showError, showModal, showPrompt } from '../modals'; import { AlertModal } from '../modals/alert-modal'; import { GenerateCodeModal } from '../modals/generate-code-modal'; import { RequestSettingsModal } from '../modals/request-settings-modal'; -interface Props extends Omit { +interface Props { activeEnvironment: Environment; activeProject: Project; isPinned: Boolean; @@ -43,7 +42,7 @@ export const RequestActionsDropdown = ({ }: Props) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const patchRequestMeta = useRequestMetaPatcher(); const patchRequest = useRequestPatcher(); const { hotKeyRegistry } = settings; diff --git a/packages/insomnia/src/ui/components/dropdowns/request-group-actions-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/request-group-actions-dropdown.tsx index 148f24d286..39eba321de 100644 --- a/packages/insomnia/src/ui/components/dropdowns/request-group-actions-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/request-group-actions-dropdown.tsx @@ -13,7 +13,7 @@ import type { RequestGroupAction } from '../../../plugins'; import { getRequestGroupActions } from '../../../plugins'; import * as pluginContexts from '../../../plugins/context/index'; import { CreateRequestType, useRequestGroupPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { WorkspaceLoaderData } from '../../routes/workspace'; import { type DropdownHandle, type DropdownProps } from '../base/dropdown'; import { Icon } from '../icon'; @@ -33,7 +33,7 @@ export const RequestGroupActionsDropdown = ({ } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { hotKeyRegistry } = settings; const [actionPlugins, setActionPlugins] = useState([]); const [loadingActions, setLoadingActions] = useState>({}); diff --git a/packages/insomnia/src/ui/components/dropdowns/sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/sync-dropdown.tsx index 3fc24ac8e5..9f3fd88cf8 100644 --- a/packages/insomnia/src/ui/components/dropdowns/sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/sync-dropdown.tsx @@ -3,12 +3,15 @@ import React, { FC, Fragment, useCallback, useEffect, useState } from 'react'; import { useNavigate, useParams, useRouteLoaderData } from 'react-router-dom'; import { useInterval, useMount } from 'react-use'; +import { getAccountId } from '../../../account/session'; import * as session from '../../../account/session'; +import { getAppWebsiteBaseURL } from '../../../common/constants'; import { DEFAULT_BRANCH_NAME } from '../../../common/constants'; import { database as db, Operation } from '../../../common/database'; import { docsVersionControl } from '../../../common/documentation'; import { strings } from '../../../common/strings'; import * as models from '../../../models'; +import { isOwnerOfOrganization } from '../../../models/organization'; import { isRemoteProject, Project } from '../../../models/project'; import type { Workspace } from '../../../models/workspace'; import { Snapshot, Status } from '../../../sync/types'; @@ -18,19 +21,24 @@ import { BackendProjectWithTeam } from '../../../sync/vcs/normalize-backend-proj import { pullBackendProject } from '../../../sync/vcs/pull-backend-project'; import { interceptAccessError } from '../../../sync/vcs/util'; import { VCS } from '../../../sync/vcs/vcs'; +import { type FeatureList, useOrganizationLoaderData } from '../../../ui/routes/organization'; +import { invariant } from '../../../utils/invariant'; import { WorkspaceLoaderData } from '../../routes/workspace'; import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; import { Link } from '../base/link'; import { HelpTooltip } from '../help-tooltip'; -import { showError, showModal } from '../modals'; +import { showModal } from '../modals'; +import { showError } from '../modals'; +import { AlertModal } from '../modals/alert-modal'; +import { AskModal } from '../modals/ask-modal'; import { GitRepositorySettingsModal } from '../modals/git-repository-settings-modal'; -import { LoginModal } from '../modals/login-modal'; import { SyncBranchesModal } from '../modals/sync-branches-modal'; import { SyncDeleteModal } from '../modals/sync-delete-modal'; import { SyncHistoryModal } from '../modals/sync-history-modal'; import { SyncStagingModal } from '../modals/sync-staging-modal'; import { Button } from '../themed-button'; import { Tooltip } from '../tooltip'; + // TODO: handle refetching logic in one place not here in a component // Refresh dropdown periodically @@ -83,10 +91,11 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { const navigate = useNavigate(); const { activeWorkspaceMeta, - projects, syncItems, } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; - const remoteProjects = projects.filter(isRemoteProject); + const { organizations } = useOrganizationLoaderData(); + const currentOrg = organizations.find(organization => (organization.id === organizationId)); + const { features } = useRouteLoaderData(':organizationId') as { features: FeatureList }; const refetchRemoteBranch = useCallback(async () => { if (session.isLoggedIn()) { @@ -146,8 +155,10 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { try { // NOTE pushes the first snapshot automatically - await pushSnapshotOnInitialize({ vcs, workspace, workspaceMeta: activeWorkspaceMeta, project }); - await refreshVCSAndRefetchRemote(); + if (activeWorkspaceMeta.pushSnapshotOnInitialize) { + await pushSnapshotOnInitialize({ vcs, workspace, project }); + await refreshVCSAndRefetchRemote(); + } } catch (err) { console.log('[sync_menu] Error refreshing sync state', err); } finally { @@ -174,7 +185,10 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { ...state, loadingProjectPull: true, })); - const pulledIntoProject = await pullBackendProject({ vcs, backendProject, remoteProjects }); + + invariant(project.remoteId, 'Project is not remote'); + + const pulledIntoProject = await pullBackendProject({ vcs, backendProject, remoteProject: project }); if (pulledIntoProject.project._id !== project._id) { // If pulled into a different project, reactivate the workspace navigate(`/organization/${organizationId}/project/${projectId}/workspace/${workspace._id}`); @@ -194,8 +208,9 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { try { const branch = await vcs.getBranch(); + invariant(project.remoteId, 'Project is not remote'); await interceptAccessError({ - callback: () => vcs.push(project.remoteId), + callback: () => vcs.push({ teamId: project.parentId, teamProjectId: project.remoteId }), action: 'push', resourceName: branch, resourceType: 'branch', @@ -222,9 +237,10 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { })); try { + invariant(project.remoteId, 'Project is not remote'); const branch = await vcs.getBranch(); const delta = await interceptAccessError({ - callback: () => vcs.pull(syncItems, project.remoteId), + callback: () => vcs.pull({ candidates: syncItems, teamId: project.parentId, teamProjectId: project.remoteId }), action: 'pull', resourceName: branch, resourceType: 'branch', @@ -348,6 +364,35 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { }, }]; + const isGitSyncEnabled = features.gitSync.enabled; + const accountId = getAccountId(); + + const showUpgradePlanModal = () => { + if (!currentOrg || !accountId) { + return; + } + const isOwner = isOwnerOfOrganization({ + organization: currentOrg, + accountId, + }); + + isOwner ? + showModal(AskModal, { + title: 'Upgrade Plan', + message: 'Git Sync is only enabled for Team plan or above, please upgrade your plan.', + yesText: 'Upgrade', + noText: 'Cancel', + onDone: async (isYes: boolean) => { + if (isYes) { + window.main.openInBrowser(`${getAppWebsiteBaseURL()}/app/subscription/update?plan=team`); + } + }, + }) : showModal(AlertModal, { + title: 'Upgrade Plan', + message: 'Git Sync is only enabled for Team plan or above, please ask the organization owner to upgrade.', + }); + }; + if (!vcs.hasBackendProject()) { return (
@@ -365,7 +410,6 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { style={{ width: '100%', borderRadius: '0', - borderTop: '1px solid var(--hl-md)', height: 'var(--line-height-sm)', }} > @@ -392,7 +436,9 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { variant='contained' bg='surprise' onClick={async () => { - setIsGitRepoSettingsModalOpen(true); + isGitSyncEnabled ? + setIsGitRepoSettingsModalOpen(true) : + showUpgradePlanModal(); }} style={{ width: '100%', @@ -463,7 +509,7 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => {
refreshVCSAndRefetchRemote()} closeOnSelect={false} isDisabled={initializing} @@ -472,6 +518,7 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { Sync : = ({ vcs, workspace, project }) => { style={{ width: '100%', borderRadius: '0', - borderTop: '1px solid var(--hl-md)', height: 'var(--line-height-sm)', }} > @@ -551,7 +597,9 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { variant='contained' bg='surprise' onClick={async () => { - setIsGitRepoSettingsModalOpen(true); + isGitSyncEnabled ? + setIsGitRepoSettingsModalOpen(true) : + showUpgradePlanModal(); }} style={{ width: '100%', @@ -570,17 +618,6 @@ export const SyncDropdown: FC = ({ vcs, workspace, project }) => { aria-label='Sync Branches List' title={syncMenuHeader} > - - - {!session.isLoggedIn() && ( - showModal(LoginModal)} - /> - )} - - void; +} + export const WorkspaceDropdown: FC = () => { const { organizationId, projectId, workspaceId } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); invariant(organizationId, 'Expected organizationId'); @@ -47,10 +56,8 @@ export const WorkspaceDropdown: FC = () => { const [actionPlugins, setActionPlugins] = useState([]); const [loadingActions, setLoadingActions] = useState>({}); - const dropdownRef = useRef(null); const { - generating: loading, access, generateTests, } = useAIContext(); @@ -84,7 +91,6 @@ export const WorkspaceDropdown: FC = () => { }); } setLoadingActions({ ...loadingActions, [label]: false }); - dropdownRef.current?.hide(); }, [activeProject._id, loadingActions]); const handleDropdownOpen = useCallback(async () => { @@ -102,159 +108,117 @@ export const WorkspaceDropdown: FC = () => { }); }, [activeApiSpec]); + const isScratchpadWorkspace = isScratchpad(activeWorkspace); + + const workspaceActionsList: WorkspaceActionItem[] = [ + ...!isScratchpadWorkspace ? [{ + id: 'duplicate', + name: 'Duplicate', + icon: , + action: () => setIsDuplicateModalOpen(true), + }, + { + id: 'rename', + name: 'Rename', + icon: , + action: () => { + showPrompt({ + title: `Rename ${getWorkspaceLabel(activeWorkspace).singular}`, + defaultValue: activeWorkspaceName, + submitName: 'Rename', + selectText: true, + label: 'Name', + onComplete: name => + fetcher.submit( + { name, workspaceId: activeWorkspace._id }, + { + action: `/organization/${organizationId}/project/${activeWorkspace.parentId}/workspace/update`, + method: 'post', + encType: 'application/json', + } + ), + }); + }, + }] : [], + { + id: 'import', + name: 'Import', + icon: , + action: () => setIsImportModalOpen(true), + }, + { + id: 'export', + name: 'Export', + icon: , + action: () => setIsExportModalOpen(true), + }, + { + id: 'settings', + name: 'Settings', + icon: , + action: () => setIsSettingsModalOpen(true), + }, + ...actionPlugins.map((p: WorkspaceAction) => ({ + id: p.label, + name: p.label, + icon: , + action: () => handlePluginClick(p, activeWorkspace), + })), + ...isDesign(activeWorkspace) ? configGenerators.map(generator => ({ + id: generator.label, + name: generator.label, + icon: , + action: () => handleGenerateConfig(generator.label), + } satisfies WorkspaceActionItem)) : [], + ...isLoggedIn() && access.enabled && activeWorkspace.scope === 'design' ? [{ + id: 'insomnia-ai/generate-test-suite', + name: 'Auto-generate Tests For Collection', + action: generateTests, + icon: + + , + }] : [], + ]; + return ( <> - -
- {activeWorkspaceName} -
- -
- } - > - - setIsDuplicateModalOpen(true)} - /> - - - { - showPrompt({ - title: `Rename ${getWorkspaceLabel(activeWorkspace).singular}`, - defaultValue: activeWorkspaceName, - submitName: 'Rename', - selectText: true, - label: 'Name', - onComplete: name => - fetcher.submit( - { name, workspaceId: activeWorkspace._id }, - { - action: `/organization/${organizationId}/project/${activeWorkspace.parentId}/workspace/update`, - method: 'post', - encType: 'application/json', - } - ), - }); + isOpen && handleDropdownOpen()}> + + + { + const item = workspaceActionsList.find( + item => item.id === key + ); + if (item) { + item.action(); + } }} - /> - - - - - setIsImportModalOpen(true)} - /> - - - - setIsExportModalOpen(true)} - /> - - - - setIsSettingsModalOpen(true)} - /> - - - - {actionPlugins.map((p: WorkspaceAction) => ( - - handlePluginClick(p, activeWorkspace)} - /> - - ))} - - - - {p => - - handleGenerateConfig(p.label)} - /> - - } - - - - {item => - - - - } - isDisabled={loading} - label={item.label} - onClick={item.action} - /> - - } - - + items={workspaceActionsList} + className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none" + > + {item => ( + + {item.icon} + {item.name} + + )} + + + {isDuplicateModalOpen && ( setIsDuplicateModalOpen(false)} diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx index 88102fa5ec..8634d13cd2 100644 --- a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx @@ -2,6 +2,7 @@ import { FC, useEffect, useRef, useState } from 'react'; import React from 'react'; import { useRouteLoaderData } from 'react-router-dom'; +import { isLoggedIn } from '../../../account/session'; import { generateId } from '../../../common/misc'; import { isRemoteProject } from '../../../models/project'; import FileSystemDriver from '../../../sync/store/drivers/file-system-driver'; @@ -76,6 +77,10 @@ export const WorkspaceSyncDropdown: FC = () => { workspaceId: activeWorkspace?._id, }); + if (!isLoggedIn()) { + return null; + } + if (isRemoteProject(activeProject) && vcs && !activeWorkspaceMeta?.gitRepositoryId) { return ( , 'getAutocompl export const AuthInputRow: FC = ({ label, getAutocompleteConstants, property, mask, help, disabled = false }) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { showPasswords } = settings; const { activeRequest: { authentication, _id: requestId } } = useRouteLoaderData('request/:requestId') as RequestLoaderData; const patchRequest = useRequestPatcher(); diff --git a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx index 1d0b4883b5..b4a4b7c7c7 100644 --- a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx @@ -8,7 +8,6 @@ import { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities'; import { Maybe } from 'graphql-language-service'; import React, { FC, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; -import { useRouteLoaderData } from 'react-router-dom'; import { useLocalStorage } from 'react-use'; import { CONTENT_TYPE_JSON } from '../../../../common/constants'; @@ -21,7 +20,7 @@ import type { Request } from '../../../../models/request'; import { fetchRequestData, responseTransform, sendCurlAndWriteTimeline, tryToInterpolateRequest, tryToTransformRequestWithPlugins } from '../../../../network/network'; import { invariant } from '../../../../utils/invariant'; import { jsonPrettify } from '../../../../utils/prettify/json'; -import { RootLoaderData } from '../../../routes/root'; +import { useRootLoaderData } from '../../../routes/root'; import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../../base/dropdown'; import { CodeEditor, CodeEditorHandle } from '../../codemirror/code-editor'; import { GraphQLExplorer } from '../../graph-ql-explorer/graph-ql-explorer'; @@ -256,7 +255,7 @@ export const GraphQLEditor: FC = ({ }, [automaticFetch, environmentId, request._id, request.url, workspaceId]); const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { editorIndentWithTabs, editorIndentSize } = settings; const beautifyRequestBody = async () => { const { body } = state; diff --git a/packages/insomnia/src/ui/components/insomnia-icon.tsx b/packages/insomnia/src/ui/components/insomnia-icon.tsx index 2fc1c5277f..7aa5c38f4f 100644 --- a/packages/insomnia/src/ui/components/insomnia-icon.tsx +++ b/packages/insomnia/src/ui/components/insomnia-icon.tsx @@ -85,6 +85,68 @@ const LoadingBarIndicator = styled.div` transform: translateX(-100%); `; +export const InsomniaLogo = ({ + loading, + ...props +}: { + loading?: boolean; +} & React.SVGProps) => ( + + + + + {loading && ( + + )} + + + + + + + + +); + export const InsomniaAILogo = ({ ...props }: React.SVGProps) => { @@ -98,60 +160,7 @@ export const InsomniaAILogo = ({ return ( - - - - - {loading && ( - - )} - - - - - - - - + {loading && - {'AI is thinking...'} + {'AI is thinking...'} )} diff --git a/packages/insomnia/src/ui/components/modals/import-modal.tsx b/packages/insomnia/src/ui/components/modals/import-modal.tsx index a8b6516818..558a3ab441 100644 --- a/packages/insomnia/src/ui/components/modals/import-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/import-modal.tsx @@ -80,7 +80,7 @@ const Radio: FC<{ defaultChecked={defaultChecked} onChange={onChange} /> - {children} + {children}
); }; @@ -484,6 +484,7 @@ const ScanResourcesForm = ({ return (
void; -} - -export interface LoginModalHandle extends ModalHandle { - show: (options?: Options) => void; -} - -export const LoginModal = forwardRef(function LoginModal({ ...props }, ref) { - const modalRef = useRef(null); - const tokenInputRef = useRef(null); - - const [state, setState] = useState({ - state: 'ready', - error: '', - title: '', - message: '', - reauth: false, - url: '', - }); - - useEffect(() => { - session.onLoginLogout(() => { - modalRef.current?.hide(); - }); - }, []); - - useImperativeHandle(ref, () => ({ - hide: () => { - modalRef.current?.hide; - }, - toggle: () => modalRef.current?.toggle(), - isOpen: () => Boolean(modalRef.current?.isOpen()), - show: async (options: Partial = {}) => { - const { title, message, reauth } = options; - const url = await getLoginUrl(); - setState({ - error: '', - state: 'ready', - title: title ?? '', - message: message ?? '', - reauth: reauth ?? false, - url, - }); - - modalRef.current?.show(); - - if (!reauth) { - window.main.openInBrowser(url); - } - }, - }), []); - - const reset = useCallback(() => { - setState(state => ({ ...state, state: 'ready', error: '' })); - }, []); - - const goToTokenEntry = useCallback(() => { - setState(state => ({ ...state, state: 'token-entry' })); - }, []); - - const submitToken = useCallback(async (event: FormEvent) => { - event.preventDefault(); - try { - await submitAuthCode(tokenInputRef.current?.value ?? ''); - } catch (e) { - setState(state => ({ ...state, state: 'error', error: e.message })); - } - }, []); - - const copyUrl = useCallback(() => { - window.clipboard.writeText(state.url); - }, [state.url]); - - const openUrl = useCallback(() => { - window.main.openInBrowser(state.url); - }, [state.url]); - - const renderBody = () => { - switch (state.state) { - case 'ready': - return <> - Please login via your web browser. - {state.reauth ? - <> -

- To continue logging in, click the button and continue in your web browser. -

- -

- If it did not open, copy and paste the following URL into your browser: -

- - : - <> -

- A new page should have opened in your default web browser. - To continue, please login via the browser. -

-

- If it did not open, copy and paste the following URL into your browser: -

- - } -
- - -
- ; - - case 'token-entry': - return -
- -
- - ; - - case 'loading-session': - return

Loading...

; - - case 'error': - return
An error occurred: {state.error}
; - - default: - return
Unknown state.
; - } - }; - - return ( - - {state.title || 'Log Into Your Account'} - - {state.message ?

{state.message}

: null} - {renderBody()} -
- - {state.state === 'error' ? - : null} - {state.state === 'ready' && state.reauth ? - : null} - {state.state === 'ready' ? - : null} - -
- ); -}); - -LoginModal.displayName = 'LoginModal'; - -export const showLoginModal = () => showModal(LoginModal); diff --git a/packages/insomnia/src/ui/components/modals/project-settings-modal.tsx b/packages/insomnia/src/ui/components/modals/project-settings-modal.tsx index 7b40580977..2d5ea658cc 100644 --- a/packages/insomnia/src/ui/components/modals/project-settings-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/project-settings-modal.tsx @@ -1,14 +1,14 @@ -import React, { FC, useEffect, useRef } from 'react'; +import React, { FC, Fragment, useEffect, useRef } from 'react'; import { OverlayContainer } from 'react-aria'; import { useFetcher, useParams } from 'react-router-dom'; import { strings } from '../../../common/strings'; -import { isRemoteProject, Project } from '../../../models/project'; +import { isDefaultOrganizationProject, Project } from '../../../models/project'; import { Modal, type ModalHandle, ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalHeader } from '../base/modal-header'; import { PromptButton } from '../base/prompt-button'; -import { HelpTooltip } from '../help-tooltip'; +import { showAlert } from '.'; export interface ProjectSettingsModalProps extends ModalProps { project: Project; @@ -17,13 +17,20 @@ export interface ProjectSettingsModalProps extends ModalProps { export const ProjectSettingsModal: FC = ({ project, onHide }) => { const modalRef = useRef(null); const { organizationId } = useParams<{organizationId: string}>(); - const { submit } = useFetcher(); + const { submit, Form, data, state } = useFetcher(); useEffect(() => { modalRef.current?.show(); }, []); - const isRemote = isRemoteProject(project); + useEffect(() => { + if (data && data.error && state === 'idle') { + showAlert({ + title: 'Could not rename project', + message: data.error, + }); + } + }, [data, state]); return ( @@ -33,55 +40,41 @@ export const ProjectSettingsModal: FC = ({ project, o
{project._id}
-
+
-
-

Actions

-
- - submit( - {}, - { method: 'post', action: `/organization/${organizationId}/project/${project._id}/delete` } - ) - } - className="width-auto btn btn--clicky inline-block" - > - Delete - -
+ + + {!isDefaultOrganizationProject(project) && +

Actions

+
+ + submit( + {}, + { method: 'post', action: `/organization/${organizationId}/project/${project._id}/delete` } + ) + } + className="width-auto btn btn--clicky inline-block" + > + Delete + +
+
} +
diff --git a/packages/insomnia/src/ui/components/modals/settings-modal.tsx b/packages/insomnia/src/ui/components/modals/settings-modal.tsx index 77d8992936..ec37c86022 100644 --- a/packages/insomnia/src/ui/components/modals/settings-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/settings-modal.tsx @@ -1,12 +1,11 @@ import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'; -import * as session from '../../../account/session'; +import { getEmail, isLoggedIn } from '../../../account/session'; import { getAppVersion, getProductName } from '../../../common/constants'; import { Modal, type ModalHandle, ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalHeader } from '../base/modal-header'; import { PanelContainer, TabItem, Tabs } from '../base/tabs'; -import { Account } from '../settings/account'; import { AI } from '../settings/ai'; import { General } from '../settings/general'; import { ImportExport } from '../settings/import-export'; @@ -29,8 +28,8 @@ export const TAB_INDEX_AI = 'ai'; export const SettingsModal = forwardRef((props, ref) => { const [defaultTabKey, setDefaultTabKey] = useState('general'); const modalRef = useRef(null); - const email = session.isLoggedIn() ? session.getFullName() : null; - + const email = getEmail(); + const showEmail = isLoggedIn(); useImperativeHandle(ref, () => ({ hide: () => { modalRef.current?.hide(); @@ -47,7 +46,7 @@ export const SettingsModal = forwardRef((props, {getProductName()} Preferences   â€“  v{getAppVersion()} - {email ? ` – ${email}` : null} + {(showEmail && email) ? ` – ${email}` : null} @@ -72,11 +71,6 @@ export const SettingsModal = forwardRef((props, - - - - - diff --git a/packages/insomnia/src/ui/components/modals/workspace-duplicate-modal.tsx b/packages/insomnia/src/ui/components/modals/workspace-duplicate-modal.tsx index d9f8490910..7dd9e0eb65 100644 --- a/packages/insomnia/src/ui/components/modals/workspace-duplicate-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/workspace-duplicate-modal.tsx @@ -4,7 +4,7 @@ import { useFetcher, useParams } from 'react-router-dom'; import { getWorkspaceLabel } from '../../../common/get-workspace-label'; import { strings } from '../../../common/strings'; -import { isDefaultProject, isLocalProject, Project } from '../../../models/project'; +import { Project } from '../../../models/project'; import { Workspace } from '../../../models/workspace'; import { Modal, type ModalHandle, ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; @@ -48,7 +48,7 @@ export const WorkspaceDuplicateModal: FC = ({ work diff --git a/packages/insomnia/src/ui/components/modals/workspace-settings-modal.tsx b/packages/insomnia/src/ui/components/modals/workspace-settings-modal.tsx index 42ade58e56..b572cfdbf3 100644 --- a/packages/insomnia/src/ui/components/modals/workspace-settings-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/workspace-settings-modal.tsx @@ -1,6 +1,6 @@ import React, { FC, ReactNode, useEffect, useRef, useState } from 'react'; import { OverlayContainer } from 'react-aria'; -import { useFetcher, useRevalidator } from 'react-router-dom'; +import { useFetcher } from 'react-router-dom'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; @@ -10,9 +10,8 @@ import { CaCertificate } from '../../../models/ca-certificate'; import type { ClientCertificate } from '../../../models/client-certificate'; import * as models from '../../../models/index'; import { isRequest } from '../../../models/request'; -import { Workspace } from '../../../models/workspace'; +import { isScratchpad, Workspace } from '../../../models/workspace'; import { WorkspaceMeta } from '../../../models/workspace-meta'; -import { invariant } from '../../../utils/invariant'; import { FileInputButton } from '../base/file-input-button'; import { Modal, type ModalHandle, ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; @@ -22,6 +21,7 @@ import { PanelContainer, TabItem, Tabs } from '../base/tabs'; import { HelpTooltip } from '../help-tooltip'; import { MarkdownEditor } from '../markdown-editor'; import { PasswordViewer } from '../viewers/password-viewer'; + const CertificateFields = styled.div({ display: 'flex', flexDirection: 'column', @@ -74,9 +74,10 @@ interface Props extends ModalProps { clientCertificates: ClientCertificate[]; caCertificate: CaCertificate | null; } -export const WorkspaceSettingsModal = ({ workspace, workspaceMeta, clientCertificates, caCertificate, onHide }: Props) => { - const hasDescription = !!workspace.description; +export const WorkspaceSettingsModal = ({ workspace, clientCertificates, caCertificate, onHide }: Props) => { + const hasDescription = !!workspace.description; + const isScratchpadWorkspace = isScratchpad(workspace); const modalRef = useRef(null); const [state, setState] = useState({ showAddCertificateForm: false, @@ -89,7 +90,6 @@ export const WorkspaceSettingsModal = ({ workspace, workspaceMeta, clientCertifi showDescription: hasDescription, defaultPreviewMode: hasDescription, }); - const { revalidate } = useRevalidator(); const activeWorkspaceName = workspace.name; useEffect(() => { modalRef.current?.show(); @@ -255,14 +255,16 @@ export const WorkspaceSettingsModal = ({ workspace, workspaceMeta, clientCertifi : null} {workspace ? - - - +

Actions

- - Delete - + {!isScratchpadWorkspace && ( + + Delete + + )} Clear All Responses
-
-
- - -
- -
- -
- - - - + , + }, { + title: 'Client Certificates', + children: +
+ +
+ +
+ + + + +
-
- {!showAddCertificateForm ? ( -
- {clientCertificates.length === 0 ? ( -

- You have not yet added any client certificates -

- ) : null} + {!showAddCertificateForm ? ( +
+ {clientCertificates.length === 0 ? ( +

+ You have not yet added any client certificates +

+ ) : null} - {!!sharedCertificates.length && ( -
- - {sharedCertificates.map(renderCertificate)} -
- )} + {!!sharedCertificates.length && ( +
+ + {sharedCertificates.map(renderCertificate)} +
+ )} - {!!privateCertificates.length && ( -
- - {privateCertificates.map(renderCertificate)} + {!!privateCertificates.length && ( +
+ + {privateCertificates.map(renderCertificate)} +
+ )} +
+
+
- )} -
-
-
-
- ) : ( -
-
- -
-
-
+ ) : ( + +
-
-
-
-   Or   -
-
-
+
+
-
- +
+
+
+   Or   +
+
+
+ +
+
+ +
-
-
- -
-
- -
-
-
- - -
- - )} - - - - -
- -

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

-
-
-
+
+ +
+
+ +
+
+
+ + +
+ + )} + , + }]} + aria-label="Workspace settings tabs" + > + {props => ( + + )} : null} diff --git a/packages/insomnia/src/ui/components/organization-avatar.tsx b/packages/insomnia/src/ui/components/organization-avatar.tsx new file mode 100644 index 0000000000..0e321f27d5 --- /dev/null +++ b/packages/insomnia/src/ui/components/organization-avatar.tsx @@ -0,0 +1,87 @@ +import React, { Suspense } from 'react'; + +const getNameInitials = (name: string) => { + // Split on whitespace and take first letter of each word + const words = name.toUpperCase().split(' '); + const firstWord = words[0]; + const lastWord = words[words.length - 1]; + + // If there is only one word, just take the first letter + if (words.length === 1) { + return firstWord.charAt(0); + } + + // If the first word is an emoji or an icon then just use that + const iconMatch = firstWord.match(/\p{Extended_Pictographic}/u); + if (iconMatch) { + return iconMatch[0]; + } + + return `${firstWord.charAt(0)}${lastWord ? lastWord.charAt(0) : ''}`; +}; + +// https://css-tricks.com/pre-caching-image-with-react-suspense/ +// can be improved when api sends image expiry headers +class ImageCache { + __cache: Record | string; timestamp: number }> = {}; + ttl: number; + + constructor({ ttl }: { ttl: number }) { + this.ttl = ttl; + } + + read(src: string) { + const now = Date.now(); + if (this.__cache[src] && typeof this.__cache[src].value !== 'string') { + // If the value is a Promise, throw it to indicate that the cache is still loading + throw this.__cache[src].value; + } else if (this.__cache[src] && now - this.__cache[src].timestamp < this.ttl) { + // If the value is a string and hasn't expired, return it + return this.__cache[src].value; + } else { + // Otherwise, load the image and add it to the cache + const promise = new Promise(resolve => { + const img = new Image(); + img.onload = () => { + const value = src; + this.__cache[src] = { value, timestamp: now }; + resolve(value); + }; + img.src = src; + }); + this.__cache[src] = { value: promise, timestamp: now }; + throw promise; + } + } +} + +// Cache images for 10 minutes +const imgCache = new ImageCache({ ttl: 1000 * 60 * 10 }); + +const Avatar = ({ src, alt }: { src: string; alt: string }) => { + imgCache.read(src); + return {alt}; +}; + +export const OrganizationAvatar = ({ + src, + alt, +}: { + src: string; + alt: string; +}) => { + if (!src) { + return
{getNameInitials(alt)}
; + } + + return ( + {getNameInitials(alt)}
}> + + + ); +}; diff --git a/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx b/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx index 16ebeed18f..0d32366f18 100644 --- a/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx @@ -220,12 +220,14 @@ export const GrpcRequestPane: FunctionComponent = ({ - window.main.grpc.cancel(requestId)} - handleStart={handleRequestSend} - /> +
+ window.main.grpc.cancel(requestId)} + handleStart={handleRequestSend} + /> +
diff --git a/packages/insomnia/src/ui/components/panes/placeholder-request-pane.tsx b/packages/insomnia/src/ui/components/panes/placeholder-request-pane.tsx index 352264b26e..ce5a80e167 100644 --- a/packages/insomnia/src/ui/components/panes/placeholder-request-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/placeholder-request-pane.tsx @@ -1,15 +1,14 @@ import React, { FC, useCallback } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { useFetcher, useParams } from 'react-router-dom'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { Hotkey } from '../hotkey'; import { Pane, PaneBody, PaneHeader } from './pane'; export const PlaceholderRequestPane: FC = () => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { hotKeyRegistry } = settings; const requestFetcher = useFetcher(); const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string }; diff --git a/packages/insomnia/src/ui/components/panes/placeholder-response-pane.tsx b/packages/insomnia/src/ui/components/panes/placeholder-response-pane.tsx index b0ab2af435..f54cfe540e 100644 --- a/packages/insomnia/src/ui/components/panes/placeholder-response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/placeholder-response-pane.tsx @@ -1,10 +1,9 @@ import React, { FC, PropsWithChildren } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import styled from 'styled-components'; import { keyboardShortcutDescriptions } from '../../../common/hotkeys'; import { KeyboardShortcut } from '../../../common/settings'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { Hotkey } from '../hotkey'; import { Pane, PaneBody, PaneHeader } from './pane'; @@ -31,7 +30,7 @@ const Description = styled.div({ export const PlaceholderResponsePane: FC> = ({ children }) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { hotKeyRegistry } = settings; return ( diff --git a/packages/insomnia/src/ui/components/panes/project-empty-state-pane.tsx b/packages/insomnia/src/ui/components/panes/project-empty-state-pane.tsx index 5d8bcfb582..6d0da5f819 100644 --- a/packages/insomnia/src/ui/components/panes/project-empty-state-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/project-empty-state-pane.tsx @@ -1,12 +1,52 @@ import React, { FC } from 'react'; +import { useParams, useRouteLoaderData } from 'react-router-dom'; import styled from 'styled-components'; +import { getAccountId } from '../../../account/session'; +import { getAppWebsiteBaseURL } from '../../../common/constants'; +import { isOwnerOfOrganization } from '../../../models/organization'; +import { type FeatureList, useOrganizationLoaderData } from '../../../ui/routes/organization'; +import { showModal } from '../modals'; +import { AlertModal } from '../modals/alert-modal'; +import { AskModal } from '../modals/ask-modal'; import { Button } from '../themed-button'; +const PostmanIcon = (props: React.SVGProps) => { + return ( + + + + + + + ); +}; + const Wrapper = styled.div({ height: '100%', + width: '100%', boxSizing: 'border-box', display: 'flex', + flexWrap: 'wrap', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', @@ -44,6 +84,22 @@ const Title = styled.div({ fontWeight: 'bold', }); +const SquareButton = styled(Button)({ + flexDirection: 'column', + padding: 'var(--padding-xl)', + gap: 'var(--padding-md)', + maxWidth: 180, + background: 'linear-gradient(120.49deg, var(--color-bg) 9.66%, var(--hl-md) 107.02%)', +}); + +const AlmostSquareButton = styled(Button)({ + flexDirection: 'column', + maxWidth: 130, + padding: '4em var(--padding-xl)', + gap: 'var(--padding-md)', + background: 'linear-gradient(120.49deg, var(--color-bg) 9.66%, var(--hl-md) 107.02%)', +}); + interface Props { createRequestCollection: () => void; createDesignDocument: () => void; @@ -52,51 +108,142 @@ interface Props { } export const EmptyStatePane: FC = ({ createRequestCollection, createDesignDocument, importFrom, cloneFromGit }) => { + const { organizationId } = useParams<{ organizationId: string }>(); + const { organizations } = useOrganizationLoaderData(); + const currentOrg = organizations.find(organization => (organization.id === organizationId)); + const { features } = useRouteLoaderData(':organizationId') as { features: FeatureList }; + + const isGitSyncEnabled = features.gitSync.enabled; + const accountId = getAccountId(); + + const showUpgradePlanModal = () => { + if (!currentOrg || !accountId) { + return; + } + const isOwner = isOwnerOfOrganization({ + organization: currentOrg, + accountId, + }); + + isOwner ? + showModal(AskModal, { + title: 'Upgrade Plan', + message: 'Git Sync is only enabled for Team plan or above, please upgrade your plan.', + yesText: 'Upgrade', + noText: 'Cancel', + onDone: async (isYes: boolean) => { + if (isYes) { + window.main.openInBrowser(`${getAppWebsiteBaseURL()}/app/subscription/update?plan=team`); + } + }, + }) : showModal(AlertModal, { + title: 'Upgrade Plan', + message: 'Git Sync is only enabled for Team plan or above, please ask the organization owner to upgrade.', + }); + }; + return ( This is an empty project, to get started create your first resource: -
-
- - -
- or -
-
+ + or + +
+ + - Import - - -
+ /> Url + + + Clipboard + + { + isGitSyncEnabled ? + cloneFromGit() : + showUpgradePlanModal(); + } + } + > + Git Clone + + + Postman +
); diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index deb1c64186..149f683069 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -10,7 +10,7 @@ import { cancelRequestById } from '../../../network/cancellation'; import { jsonPrettify } from '../../../utils/prettify/json'; import { useRequestMetaPatcher } from '../../hooks/use-request'; import { RequestLoaderData } from '../../routes/request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { PanelContainer, TabItem, Tabs } from '../base/tabs'; import { PreviewModeDropdown } from '../dropdowns/preview-mode-dropdown'; import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown'; @@ -40,7 +40,7 @@ export const ResponsePane: FC = ({ const patchRequestMeta = useRequestMetaPatcher(); const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const previewMode = activeRequestMeta.previewMode || PREVIEW_MODE_SOURCE; const handleSetFilter = async (responseFilter: string) => { if (!activeResponse) { diff --git a/packages/insomnia/src/ui/components/present-users.tsx b/packages/insomnia/src/ui/components/present-users.tsx new file mode 100644 index 0000000000..2ab57f626b --- /dev/null +++ b/packages/insomnia/src/ui/components/present-users.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { useParams, useRouteLoaderData } from 'react-router-dom'; + +import { getAccountId } from '../../account/session'; +import { usePresenceContext } from '../context/app/presence-context'; +import { ProjectsLoaderData } from '../routes/project'; +import { AvatarGroup } from './avatar'; + +export const PresentUsers = () => { + const { presence } = usePresenceContext(); + const { workspaceId } = useParams() as { workspaceId: string }; + const projectData = useRouteLoaderData('/project') as ProjectsLoaderData | null; + const remoteId = projectData?.activeProject.remoteId; + + if (!presence || !remoteId) { + return null; + } + + const accountId = getAccountId(); + + const activeUsers = presence + .filter(p => { + return p.project === remoteId && p.file === workspaceId; + }) + .filter(p => p.acct !== accountId) + .map(user => { + return { + key: user.acct, + alt: + user.firstName || user.lastName + ? `${user.firstName} ${user.lastName}` + : user.acct, + src: user.avatar, + }; + }); + + return ( + + ); +}; diff --git a/packages/insomnia/src/ui/components/request-url-bar.tsx b/packages/insomnia/src/ui/components/request-url-bar.tsx index cb9c735b13..45b5bfdf3b 100644 --- a/packages/insomnia/src/ui/components/request-url-bar.tsx +++ b/packages/insomnia/src/ui/components/request-url-bar.tsx @@ -16,7 +16,7 @@ import { useRequestPatcher } from '../hooks/use-request'; import { useRequestMetaPatcher } from '../hooks/use-request'; import { useTimeoutWhen } from '../hooks/useTimeoutWhen'; import { ConnectActionParams, RequestLoaderData, SendActionParams } from '../routes/request'; -import { RootLoaderData } from '../routes/root'; +import { useRootLoaderData } from '../routes/root'; import { WorkspaceLoaderData } from '../routes/workspace'; import { Dropdown, DropdownButton, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from './base/dropdown'; import { OneLineEditor, OneLineEditorHandle } from './codemirror/one-line-editor'; @@ -59,7 +59,7 @@ export const RequestUrlBar = forwardRef(({ } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { hotKeyRegistry } = settings; const { activeRequest, activeRequestMeta: { downloadPath } } = useRouteLoaderData('request/:requestId') as RequestLoaderData; const patchRequestMeta = useRequestMetaPatcher(); @@ -222,127 +222,137 @@ export const RequestUrlBar = forwardRef(({ })} onPaste={onPaste} /> - {isCancellable ? ( - - ) : (<> +
+ {isCancellable ? ( - {isEventStreamRequest(activeRequest) ? null : ( - - - } - > - + ) : ( + <> + + {isEventStreamRequest(activeRequest) ? null : ( + - )} - - sendOrConnect(true)} /> - - - )} - )} + removeBorderRadius={true} + > + + + } + > + + + + + + showModal(GenerateCodeModal, { request: activeRequest })} + /> + + + + + showPrompt({ + inputType: 'decimal', + title: 'Send After Delay', + label: 'Delay in seconds', + defaultValue: '3', + onComplete: seconds => { + setCurrentTimeout(+seconds * 1000); + }, + })} + /> + + + showPrompt({ + inputType: 'decimal', + title: 'Send on Interval', + label: 'Interval in seconds', + defaultValue: '3', + submitName: 'Start', + onComplete: seconds => { + setCurrentInterval(+seconds * 1000); + }, + })} + /> + + {downloadPath + ? ( + patchRequestMeta(activeRequest._id, { downloadPath: null })} + /> + ) + : ( + { + const { canceled, filePaths } = await window.dialog.showOpenDialog({ + title: 'Select Download Location', + buttonLabel: 'Select', + properties: ['openDirectory'], + }); + if (canceled) { + return; + } + patchRequestMeta(activeRequest._id, { downloadPath: filePaths[0] }); + }} + /> + )} + + sendOrConnect(true)} /> + + + + )} + + )} +
); diff --git a/packages/insomnia/src/ui/components/settings/account.tsx b/packages/insomnia/src/ui/components/settings/account.tsx deleted file mode 100644 index 44e891f90b..0000000000 --- a/packages/insomnia/src/ui/components/settings/account.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import React, { FC, Fragment, useCallback, useState } from 'react'; - -import * as session from '../../../account/session'; -import { Link } from '../base/link'; -import { PromptButton } from '../base/prompt-button'; -import { HelpTooltip } from '../help-tooltip'; -import { hideAllModals, showModal } from '../modals/index'; -import { LoginModal } from '../modals/login-modal'; - -export const Account: FC = () => { - const [codeSent, setCodeSent] = useState(false); - const [error, setError] = useState(''); - const [showChangePassword, setShowChangePassword] = useState(false); - const [finishedResetting, setFinishedResetting] = useState(false); - - const handleLogin = useCallback((event: React.SyntheticEvent) => { - event.preventDefault(); - hideAllModals(); - showModal(LoginModal); - }, []); - - const handleSubmitPasswordChange = useCallback(async (event: React.SyntheticEvent) => { - event.preventDefault(); - const formData = new FormData(event.currentTarget); - const password = formData.get('password') as string; - const confirmPassword = formData.get('confirmPassword') as string; - const code = formData.get('code') as string; - setError(''); - if (!password) { - setError('Password was not provided'); - return; - } - if (password !== confirmPassword) { - setError('Passwords did not match'); - return; - } - if (!code) { - setError('Code was not provided'); - return; - } - try { - await session.changePasswordWithToken(password, code); - } catch (err) { - setError(err.message); - return; - } - setError(''); - setFinishedResetting(true); - setShowChangePassword(false); - }, []); - - const openChangePasswordDialog = useCallback(() => { - setShowChangePassword(!showChangePassword); - setFinishedResetting(false); - }, [showChangePassword]); - - const logout = useCallback(() => session.logout(), []); - - return session.isLoggedIn() ? ( - -
-

Welcome {session.getFirstName()}!

-

- You are currently logged in as{' '} - {session.getEmail()} -

-
- - Manage Account - - - Sign Out - - -
- - {finishedResetting && ( -

Your password was changed successfully

- )} - - {showChangePassword && ( -
-
- {error &&

{error}

} -
- -
-
- -
-
- -
-
-
- {codeSent ? 'A code was sent to your email' : 'Looking for a code?'}{' '} - { - event.preventDefault(); - try { - session.sendPasswordChangeCode(); - } catch (err) { - setError(err.message); - return; - } - setCodeSent(true); - setError(''); - }} - > - Email Me a Code - -
-
- -
-
-
- )} -
- ) : ( - -
-

Try Insomnia Sync!

-

- Sync your data across devices or with a team -
- Keep synced data safe with end-to-end encryption -
- Prioritized email support -
-

-
-
- - View plans - -
-
-

- Or{' '} - Log In - -

-
); -}; diff --git a/packages/insomnia/src/ui/components/settings/ai.tsx b/packages/insomnia/src/ui/components/settings/ai.tsx index 670c30f0c3..4036ec49ae 100644 --- a/packages/insomnia/src/ui/components/settings/ai.tsx +++ b/packages/insomnia/src/ui/components/settings/ai.tsx @@ -1,20 +1,10 @@ -import React, { Fragment, useCallback } from 'react'; +import React, { Fragment } from 'react'; -import { isLoggedIn } from '../../../account/session'; import { useAIContext } from '../../context/app/ai-context'; import { Link } from '../base/link'; import { InsomniaAI } from '../insomnia-ai-icon'; -import { hideAllModals, showModal } from '../modals'; -import { LoginModal } from '../modals/login-modal'; export const AI = () => { - const loggedIn = isLoggedIn(); - - const handleLogin = useCallback((event: React.SyntheticEvent) => { - event.preventDefault(); - hideAllModals(); - showModal(LoginModal); - }, []); const { access: { @@ -27,7 +17,7 @@ export const AI = () => { return
Loading...
; } - if (loggedIn && enabled) { + if (enabled) { return
{ return (
-

Try Insomnia AI

+

Try Insomnia AI

Improve your productivity with Insomnia AI and perform complex operations in 1-click, like auto-generating API tests for your documents and collections.
@@ -94,11 +79,6 @@ export const AI = () => {

- {!loggedIn ?

- Or{' '} - Log In - -

: null}
); }; diff --git a/packages/insomnia/src/ui/components/settings/boolean-setting.tsx b/packages/insomnia/src/ui/components/settings/boolean-setting.tsx index 41cf3e2ded..274b0c4b38 100644 --- a/packages/insomnia/src/ui/components/settings/boolean-setting.tsx +++ b/packages/insomnia/src/ui/components/settings/boolean-setting.tsx @@ -1,10 +1,9 @@ import React, { FC, ReactNode } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import styled from 'styled-components'; import { SettingsOfType } from '../../../common/settings'; import { useSettingsPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { HelpTooltip } from '../help-tooltip'; const Descriptions = styled.div({ fontSize: 'var(--font-size-sm)', @@ -32,7 +31,7 @@ export const BooleanSetting: FC<{ }) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); if (!settings.hasOwnProperty(setting)) { throw new Error(`Invalid boolean setting name ${setting}`); } diff --git a/packages/insomnia/src/ui/components/settings/enum-setting.tsx b/packages/insomnia/src/ui/components/settings/enum-setting.tsx index 7859fde8f3..5eb4eca539 100644 --- a/packages/insomnia/src/ui/components/settings/enum-setting.tsx +++ b/packages/insomnia/src/ui/components/settings/enum-setting.tsx @@ -1,9 +1,8 @@ import React, { PropsWithChildren, ReactNode } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { SettingsOfType } from '../../../common/settings'; import { useSettingsPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { HelpTooltip } from '../help-tooltip'; interface Props { help?: ReactNode; @@ -23,7 +22,7 @@ export const EnumSetting = ({ }: PropsWithChildren>) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const patchSettings = useSettingsPatcher(); diff --git a/packages/insomnia/src/ui/components/settings/general.tsx b/packages/insomnia/src/ui/components/settings/general.tsx index 0643b580ec..db9d6ff338 100644 --- a/packages/insomnia/src/ui/components/settings/general.tsx +++ b/packages/insomnia/src/ui/components/settings/general.tsx @@ -1,5 +1,4 @@ import React, { FC, Fragment } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import * as session from '../../../account/session'; import { @@ -15,7 +14,7 @@ import { docsKeyMaps } from '../../../common/documentation'; import { HttpVersion, HttpVersions, UpdateChannel } from '../../../common/settings'; import { strings } from '../../../common/strings'; import { initNewOAuthSession } from '../../../network/o-auth-2/get-token'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { Link } from '../base/link'; import { CheckForUpdatesButton } from '../check-for-updates-button'; import { Tooltip } from '../tooltip'; @@ -41,7 +40,7 @@ const RestartTooltip: FC<{ message: string }> = ({ message }) => ( export const General: FC = () => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const isLoggedIn = session.isLoggedIn(); return ( diff --git a/packages/insomnia/src/ui/components/settings/import-export.tsx b/packages/insomnia/src/ui/components/settings/import-export.tsx index 5ec8586143..d3f5b02d25 100644 --- a/packages/insomnia/src/ui/components/settings/import-export.tsx +++ b/packages/insomnia/src/ui/components/settings/import-export.tsx @@ -2,11 +2,13 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { useFetcher, useParams } from 'react-router-dom'; import { useRouteLoaderData } from 'react-router-dom'; +import { isLoggedIn } from '../../../account/session'; import { getProductName } from '../../../common/constants'; import { docsImportExport } from '../../../common/documentation'; import { exportAllToFile } from '../../../common/export'; import { getWorkspaceLabel } from '../../../common/get-workspace-label'; import { strings } from '../../../common/strings'; +import { isScratchpad } from '../../../models/workspace'; import { ProjectLoaderData } from '../../routes/project'; import { WorkspaceLoaderData } from '../../routes/workspace'; import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; @@ -57,8 +59,11 @@ export const ImportExport: FC = ({ hideSettingsModal }) => {

Your format isn't supported? Add Your Own.

-
+
{workspaceData?.activeWorkspace ? + isScratchpad(workspaceData.activeWorkspace) ? + + : ( = ({ hideSettingsModal }) => { ) : () } -    -    - + +
{isImportModalOpen && ( diff --git a/packages/insomnia/src/ui/components/settings/masked-setting.tsx b/packages/insomnia/src/ui/components/settings/masked-setting.tsx index 8dcfee06a1..64e3fe506b 100644 --- a/packages/insomnia/src/ui/components/settings/masked-setting.tsx +++ b/packages/insomnia/src/ui/components/settings/masked-setting.tsx @@ -1,10 +1,9 @@ import React, { FC } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { useToggle } from 'react-use'; import { SettingsOfType } from '../../../common/settings'; import { useSettingsPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { HelpTooltip } from '../help-tooltip'; export const MaskedSetting: FC<{ @@ -24,7 +23,7 @@ export const MaskedSetting: FC<{ const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); if (!settings.hasOwnProperty(setting)) { throw new Error(`Invalid setting name ${setting}`); diff --git a/packages/insomnia/src/ui/components/settings/number-setting.tsx b/packages/insomnia/src/ui/components/settings/number-setting.tsx index 024c304535..f11c6127c0 100644 --- a/packages/insomnia/src/ui/components/settings/number-setting.tsx +++ b/packages/insomnia/src/ui/components/settings/number-setting.tsx @@ -1,9 +1,8 @@ import React, { ChangeEventHandler, FC, InputHTMLAttributes, useCallback } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { SettingsOfType } from '../../../common/settings'; import { useSettingsPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { HelpTooltip } from '../help-tooltip'; interface Props { @@ -35,7 +34,7 @@ export const NumberSetting: FC = ({ }) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); if (!Object.prototype.hasOwnProperty.call(settings, setting)) { throw new Error(`Invalid setting name ${setting}`); diff --git a/packages/insomnia/src/ui/components/settings/plugins.tsx b/packages/insomnia/src/ui/components/settings/plugins.tsx index 4618f8f5da..564dcfa151 100644 --- a/packages/insomnia/src/ui/components/settings/plugins.tsx +++ b/packages/insomnia/src/ui/components/settings/plugins.tsx @@ -1,7 +1,8 @@ import * as path from 'path'; import React, { FC, useEffect, useState } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +import { isLoggedIn } from '../../../account/session'; import { NPM_PACKAGE_BASE, PLUGIN_HUB_BASE, @@ -12,7 +13,7 @@ import type { Plugin } from '../../../plugins/index'; import { getPlugins } from '../../../plugins/index'; import { reload } from '../../../templating/index'; import { useSettingsPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { CopyButton } from '../base/copy-button'; import { Link } from '../base/link'; import { HelpTooltip } from '../help-tooltip'; @@ -26,7 +27,8 @@ interface State { isInstallingFromNpm: boolean; isRefreshingPlugins: boolean; } -export const Plugins: FC = () => { + +export const PluginsSettings: FC = () => { const [state, setState] = useState({ plugins: [], npmPluginValue: '', @@ -45,7 +47,7 @@ export const Plugins: FC = () => { } = state; const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); useEffect(() => { refreshPlugins(); @@ -269,3 +271,36 @@ export const Plugins: FC = () => {
); }; + +export const Plugins = () => { + const navigate = useNavigate(); + if (isLoggedIn()) { + return ; + } + + return ( +
+
+ Log in to Insomnia to enable Plugins +
+
+ + +
+
+ ); +}; diff --git a/packages/insomnia/src/ui/components/settings/shortcuts.tsx b/packages/insomnia/src/ui/components/settings/shortcuts.tsx index 3bcff86291..42943f77a4 100644 --- a/packages/insomnia/src/ui/components/settings/shortcuts.tsx +++ b/packages/insomnia/src/ui/components/settings/shortcuts.tsx @@ -1,5 +1,4 @@ import React, { FC } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { areSameKeyCombinations, @@ -10,7 +9,7 @@ import { } from '../../../common/hotkeys'; import { HotKeyRegistry, KeyboardShortcut, KeyCombination } from '../../../common/settings'; import { useSettingsPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; import { PromptButton } from '../base/prompt-button'; import { Hotkey } from '../hotkey'; @@ -25,7 +24,7 @@ export const isKeyCombinationInRegistry = (pressedKeyComb: KeyCombination, hotKe export const Shortcuts: FC = () => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { hotKeyRegistry } = settings; const patchSettings = useSettingsPatcher(); diff --git a/packages/insomnia/src/ui/components/settings/text-setting.tsx b/packages/insomnia/src/ui/components/settings/text-setting.tsx index 72469615c2..205c296505 100644 --- a/packages/insomnia/src/ui/components/settings/text-setting.tsx +++ b/packages/insomnia/src/ui/components/settings/text-setting.tsx @@ -1,9 +1,8 @@ import React, { ChangeEventHandler, FC, InputHTMLAttributes, useCallback } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { SettingsOfType } from '../../../common/settings'; import { useSettingsPatcher } from '../../hooks/use-request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { HelpTooltip } from '../help-tooltip'; export const TextSetting: FC<{ @@ -21,8 +20,7 @@ export const TextSetting: FC<{ }) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; - + } = useRootLoaderData(); if (!Object.prototype.hasOwnProperty.call(settings, setting)) { throw new Error(`Invalid setting name ${setting}`); } diff --git a/packages/insomnia/src/ui/components/sidebar-layout.tsx b/packages/insomnia/src/ui/components/sidebar-layout.tsx index ecd9ccba25..3405e83e6c 100644 --- a/packages/insomnia/src/ui/components/sidebar-layout.tsx +++ b/packages/insomnia/src/ui/components/sidebar-layout.tsx @@ -5,7 +5,7 @@ import styled from 'styled-components'; import { COLLAPSE_SIDEBAR_REMS, DEFAULT_PANE_HEIGHT, DEFAULT_PANE_WIDTH, DEFAULT_SIDEBAR_WIDTH, MAX_PANE_HEIGHT, MAX_PANE_WIDTH, MAX_SIDEBAR_REMS, MIN_PANE_HEIGHT, MIN_PANE_WIDTH, MIN_SIDEBAR_REMS } from '../../common/constants'; import { debounce } from '../../common/misc'; import * as models from '../../models'; -import { RootLoaderData } from '../routes/root'; +import { useRootLoaderData } from '../routes/root'; import { WorkspaceLoaderData } from '../routes/workspace'; import { ErrorBoundary } from './error-boundary'; import { Sidebar } from './sidebar/sidebar'; @@ -205,7 +205,7 @@ export const SidebarLayout: FC = ({ }) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { forceVerticalLayout } = settings; const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined; diff --git a/packages/insomnia/src/ui/components/sync-pull-button.tsx b/packages/insomnia/src/ui/components/sync-pull-button.tsx index da04d45552..a15ad9f3ab 100644 --- a/packages/insomnia/src/ui/components/sync-pull-button.tsx +++ b/packages/insomnia/src/ui/components/sync-pull-button.tsx @@ -2,6 +2,7 @@ import React, { FC, ReactNode, useState } from 'react'; import { useRouteLoaderData } from 'react-router-dom'; import { VCS } from '../../sync/vcs/vcs'; +import { invariant } from '../../utils/invariant'; import { WorkspaceLoaderData } from '../routes/workspace'; import { showError } from './modals'; interface Props { @@ -26,9 +27,10 @@ export const SyncPullButton: FC = props => { const oldBranch = await newVCS.getBranch(); let failed = false; try { + invariant(activeProject.remoteId, 'Project is not remote'); // Clone old VCS so we don't mess anything up while working on other projects await newVCS.checkout([], branch); - await newVCS.pull([], activeProject.remoteId); + await newVCS.pull({ candidates: [], teamId: activeProject.parentId, teamProjectId: activeProject.remoteId }); } catch (err) { showError({ title: 'Pull Error', diff --git a/packages/insomnia/src/ui/components/themed-button/button.tsx b/packages/insomnia/src/ui/components/themed-button/button.tsx index 04f7bb3175..7d0f1377e9 100644 --- a/packages/insomnia/src/ui/components/themed-button/button.tsx +++ b/packages/insomnia/src/ui/components/themed-button/button.tsx @@ -59,10 +59,10 @@ export const Button = styled.button` margin: ${({ margin }) => (margin || 0)}; text-align: center; font-size: var(--font-size-sm); - display: inline-flex !important; - flex-direction: row !important; - align-items: center !important; - justify-content: center !important; + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: center; border: 1px solid transparent; ${({ radius }) => css` diff --git a/packages/insomnia/src/ui/components/toast.tsx b/packages/insomnia/src/ui/components/toast.tsx index 5541c255c5..e07f819418 100644 --- a/packages/insomnia/src/ui/components/toast.tsx +++ b/packages/insomnia/src/ui/components/toast.tsx @@ -89,7 +89,7 @@ export const Toast: FC = () => { disableUpdateNotification, updateAutomatically, updateChannel, - } = await models.settings.getOrCreate(); + } = await models.settings.get(); let updatedNotification: ToastNotification | null = null; // Try fetching user notification try { diff --git a/packages/insomnia/src/ui/components/trail-lines-animation.ts b/packages/insomnia/src/ui/components/trail-lines-animation.ts new file mode 100644 index 0000000000..8850ebb32e --- /dev/null +++ b/packages/insomnia/src/ui/components/trail-lines-animation.ts @@ -0,0 +1,94 @@ +import { gsap } from 'gsap'; +import { MotionPathPlugin } from 'gsap/dist/MotionPathPlugin'; +import type { RefObject } from 'react'; + +export const internals = { + LINE_WIDTH: 1.5, + DURATION: 2, + TRIAL_SIZE_PERCENTAGE: '100%', + GRADIENT_SUFFIXES: ['01', '02', '03', '04'], + COMPACT_VERTICAL_SPACE: 20, +}; + +gsap.registerPlugin(MotionPathPlugin); + +gsap.registerEffect({ + name: 'trail', + /** + * Left from @marckong here. + * targets is TweenTarget. + * + * type TweenTarget = string | object | null; + * + * Therefore the below code is not typed correctly, but hey it works for free from the mentioned PR! + * */ + effect(targets: any[], config: gsap.TimelineVars | undefined) { + const group = targets[0]; + const dot = group?.querySelector('.dot'); + const rect = group?.querySelector('rect'); + + return gsap + .timeline({ + defaults: { + ease: 'none', + duration: internals.DURATION, + }, + ...config, + }) + .set(dot, { opacity: 1 }) + .to( + dot, + { + motionPath: { + path: group.dataset.motionPath, + }, + onUpdate() { + const x = +gsap.getProperty(this.targets()[0], 'x'); + rect.x.baseVal.value = x - rect?.width?.baseVal?.value; + }, + }, + '<' + ) + .to(rect, { attr: { x: '100%' } }, '>'); + }, +}); + +export function animateTrailPaths(totalActiveLines: number, scope: RefObject[]) { + const animations = new Set(); + + const ctx = gsap.context(() => { + const groups: Node[] = gsap.utils.toArray('.group-elements'); + const availableGroups = new Set([...groups]); + + function animate(delay = 0) { + const group: Node = gsap.utils.random([...availableGroups]); + availableGroups.delete(group); + + if (!group) { + return; + } + + return gsap.effects.trail(group, { + delay, + onComplete() { + availableGroups.add(group); + animations.delete(this); + animations.add(animate()); + }, + }); + } + + for (let i = 0; i < totalActiveLines; i++) { + animations.add(animate(gsap.utils.random(1, 3))); + } + }, scope); + + return () => { + ctx.kill(); + Array.from(animations).forEach((a: GSAPAnimation) => { + a?.kill(); + }); + }; +} + +export const random = gsap.utils.random; diff --git a/packages/insomnia/src/ui/components/trail-lines-container.tsx b/packages/insomnia/src/ui/components/trail-lines-container.tsx new file mode 100644 index 0000000000..27bc854919 --- /dev/null +++ b/packages/insomnia/src/ui/components/trail-lines-container.tsx @@ -0,0 +1,76 @@ +// Left from @marckong here: slightly modified from this PR - https://github.com/Kong/insomnia-website/pull/41 +import type { PropsWithChildren } from 'react'; +import React from 'react'; +import { useEffect, useRef, useState } from 'react'; + +import TrailLines, { TrailsLineHandle } from './trail-lines'; + +interface Size { + width: number; + height: number; +} + +export const TrailLinesContainer = ({ children }: PropsWithChildren) => { + const startTailRef = useRef(null); + const endTailRef = useRef(null); + const containerRef = useRef(null); + const childrenContainerRef = useRef(null); + const [dimensions, setDimensions] = useState(); + + useEffect(() => { + startTailRef.current?.toggle(true); + endTailRef.current?.toggle(true); + }, [dimensions]); + + useEffect(() => { + updateDimensions(); + + window.addEventListener('resize', updateDimensions); + + return () => { + window.removeEventListener('resize', updateDimensions); + }; + }, []); + + const updateDimensions = () => { + startTailRef.current?.toggle(false); + endTailRef.current?.toggle(false); + + const containerWidth = containerRef.current?.clientWidth; + const containerHeight = containerRef.current?.clientHeight; + + const childrenWidth = childrenContainerRef.current?.clientWidth; + const childrenHeight = childrenContainerRef.current?.clientHeight; + + if (!containerWidth || !containerHeight || !childrenWidth || !childrenHeight) { + return; + } + + if (containerWidth < 500) { + return; + } + + const matrix: Size = { width: (containerWidth - childrenWidth) / 2, height: containerHeight }; + + setDimensions(matrix); + }; + + return ( +
+
+ {dimensions && } +
+
+ {children} +
+
+ {dimensions && ( + + )} +
+
+ ); +}; diff --git a/packages/insomnia/src/ui/components/trail-lines.tsx b/packages/insomnia/src/ui/components/trail-lines.tsx new file mode 100644 index 0000000000..0bf51b8f8b --- /dev/null +++ b/packages/insomnia/src/ui/components/trail-lines.tsx @@ -0,0 +1,178 @@ +// Left from @marckong here: slightly modified from this PR - https://github.com/Kong/insomnia-website/pull/41 +import React, { forwardRef, memo, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react'; +import styled from 'styled-components'; + +import { animateTrailPaths, internals, random } from './trail-lines-animation'; + +function renderPaths({ + id, + width, + height, + totalLines, +}: { + id: string; + width: number; + height: number; + totalLines: number; +}) { + const aControlPointX = width / 2; + const bControlPointX = width - width / 3; + const ySpace = height / (totalLines - 1); + const ySpaceScaled = internals.COMPACT_VERTICAL_SPACE / 100; + const yMid = Math.round(height / 2); + const trim = (str: string) => + str + .split(/\n/) + .map(s => s.trim()) + .join(' ') + .trim(); + + return ( + <> + {Array.from({ length: totalLines }, (_, i) => { + const y = ySpace * i; + const yScaled = Math.round((y - yMid) * ySpaceScaled + yMid); + const motionPath = trim(` + M 0 ${y} + C ${aControlPointX} ${y} + ${bControlPointX} ${yScaled} + ${width + 20} ${yScaled} + `); + const motionPathReverse = trim(` + v ${internals.LINE_WIDTH} + C ${bControlPointX} ${yScaled + internals.LINE_WIDTH} + ${aControlPointX} ${y + internals.LINE_WIDTH} + 0 ${y + internals.LINE_WIDTH} + `); + + return ( + + + + + + + {/* eslint-disable-next-line no-template-curly-in-string */} + + + + + + + ); + })} + + ); +} + +const Lines = styled('svg')<{ reverse?: boolean }>(({ reverse = false }) => ({ + pointerEvents: 'none', + maskSize: 'var(--trail-lines-width, auto) var(--trail-lines-height, auto)', + maskPosition: 'top right', + maskImage: 'linear-gradient(-90deg, rgba(0, 0, 0, 1) 80%, transparent)', + WebkitMaskSize: 'var(--trail-lines-width, auto) var(--trail-lines-height, auto)', + WebkitMaskPosition: 'top right', + WebkitMaskImage: 'linear-gradient(-90deg, rgba(0, 0, 0, 1) 80%, transparent)', + transform: reverse ? 'scaleX(-1)' : 'none', +})); + +interface Props { + id: string; + reverse?: boolean; + width?: number | undefined; + height?: number | undefined; + totalLines?: number | undefined; + totalActiveLines?: number | undefined; +} +export interface TrailsLineHandle { + toggle: (show: boolean) => void; +} + +const TrailLines = forwardRef( + ({ id, width = 442, height = 820, totalLines = 19, totalActiveLines = 2, reverse }, ref) => { + const refRoot = useRef(null); + const [showPaths, setShowPaths] = useState(false); + + useImperativeHandle( + ref, + () => ({ + toggle: (show: boolean) => { + setShowPaths(show); + }, + }), + [] + ); + + useEffect(() => { + refRoot?.current?.style.setProperty('--trail-lines-width', `${width}px`); + refRoot?.current?.style.setProperty('--trail-lines-height', `${height}px`); + }, [width, height]); + + useLayoutEffect(() => { + const stopAnimation = animateTrailPaths(totalActiveLines, [refRoot]); + + return () => { + stopAnimation(); + }; + }, [refRoot, showPaths, totalActiveLines]); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {showPaths && renderPaths({ id, width, height, totalLines })} + + ); + } +); + +TrailLines.displayName = 'TrailLines'; + +export default memo(TrailLines); diff --git a/packages/insomnia/src/ui/components/viewers/password-viewer.tsx b/packages/insomnia/src/ui/components/viewers/password-viewer.tsx index cf84aaa9ee..36b39edcfd 100644 --- a/packages/insomnia/src/ui/components/viewers/password-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/password-viewer.tsx @@ -1,8 +1,7 @@ import React, { FC, useCallback, useEffect, useState } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import styled from 'styled-components'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; const EyeIcon = styled.i({ cursor: 'pointer', @@ -34,7 +33,7 @@ export const PasswordViewer: FC<{ }) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { showPasswords } = settings; const [mask, setMask] = useState(null); useEffect(() => { diff --git a/packages/insomnia/src/ui/components/viewers/response-error-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-error-viewer.tsx index a1448b1e21..5aa6b2439b 100644 --- a/packages/insomnia/src/ui/components/viewers/response-error-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-error-viewer.tsx @@ -1,8 +1,7 @@ import React, { FC, memo } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { docsBase } from '../../../common/documentation'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { Link } from '../base/link'; import { showModal } from '../modals/index'; import { SettingsModal } from '../modals/settings-modal'; @@ -14,7 +13,7 @@ export const ResponseErrorViewer: FC = memo(({ error }) => { let msg: React.ReactNode = null; const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { editorFontSize } = settings; if (error?.toLowerCase().indexOf('certificate') !== -1) { diff --git a/packages/insomnia/src/ui/components/websockets/action-bar.tsx b/packages/insomnia/src/ui/components/websockets/action-bar.tsx index 7eda3ae32d..adf1b897a5 100644 --- a/packages/insomnia/src/ui/components/websockets/action-bar.tsx +++ b/packages/insomnia/src/ui/components/websockets/action-bar.tsx @@ -12,12 +12,12 @@ import { createKeybindingsHandler, useDocBodyKeyboardShortcuts } from '../keydow import { DisconnectButton } from './disconnect-button'; const Button = styled.button<{ warning?: boolean }>(({ warning }) => ({ + borderRadius: 'var(--radius-sm)', paddingRight: 'var(--padding-md)', paddingLeft: 'var(--padding-md)', textAlign: 'center', background: warning ? 'var(--color-danger)' : 'var(--color-surprise)', color: 'var(--color-font-surprise)', - flex: '0 0 100px', ':hover': { filter: 'brightness(0.8)', }, @@ -149,9 +149,11 @@ export const WebSocketActionBar: FC = ({ request, environmentId, type="text" /> - {isConnectingOrClosed - ? - : } +
+ {isConnectingOrClosed + ? + : } +
); diff --git a/packages/insomnia/src/ui/components/websockets/disconnect-button.tsx b/packages/insomnia/src/ui/components/websockets/disconnect-button.tsx index 4608757a44..cd695cbd8a 100644 --- a/packages/insomnia/src/ui/components/websockets/disconnect-button.tsx +++ b/packages/insomnia/src/ui/components/websockets/disconnect-button.tsx @@ -4,6 +4,8 @@ import styled from 'styled-components'; import { Dropdown as OriginalDropdown, DropdownButton, DropdownItem, ItemContent } from '../base/dropdown'; const SplitButton = styled.div({ + borderRadius: 'var(--radius-sm)', + overflow: 'hidden', display: 'flex', color: 'var(--color-font-surprise)', }); diff --git a/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx b/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx index 92394842ad..edc5054591 100644 --- a/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx +++ b/packages/insomnia/src/ui/components/websockets/websocket-request-pane.tsx @@ -12,7 +12,7 @@ import { useReadyState } from '../../hooks/use-ready-state'; import { useRequestPatcher } from '../../hooks/use-request'; import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version'; import { WebSocketRequestLoaderData } from '../../routes/request'; -import { RootLoaderData } from '../../routes/root'; +import { useRootLoaderData } from '../../routes/root'; import { TabItem, Tabs } from '../base/tabs'; import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor'; import { AuthDropdown } from '../dropdowns/auth-dropdown'; @@ -212,7 +212,7 @@ export const WebSocketRequestPane: FC = ({ environment }) => { const readyState = useReadyState({ requestId: activeRequest._id, protocol: 'webSocket' }); const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { useBulkParametersEditor } = settings; const disabled = readyState; diff --git a/packages/insomnia/src/ui/containers/app-hooks.tsx b/packages/insomnia/src/ui/containers/app-hooks.tsx index c494cd629c..93032dce16 100644 --- a/packages/insomnia/src/ui/containers/app-hooks.tsx +++ b/packages/insomnia/src/ui/containers/app-hooks.tsx @@ -2,11 +2,9 @@ import { FC, useEffect } from 'react'; import { useGlobalKeyboardShortcuts } from '../hooks/use-global-keyboard-shortcuts'; import { useSettingsSideEffects } from '../hooks/use-settings-side-effects'; -import { useSyncMigration } from '../hooks/use-sync-migration'; import { useThemeChange } from '../hooks/use-theme-change'; export const AppHooks: FC = () => { - useSyncMigration(); useSettingsSideEffects(); useGlobalKeyboardShortcuts(); useThemeChange(); diff --git a/packages/insomnia/src/ui/context/app/ai-context.tsx b/packages/insomnia/src/ui/context/app/ai-context.tsx index 27faccb14c..ec513f0dca 100644 --- a/packages/insomnia/src/ui/context/app/ai-context.tsx +++ b/packages/insomnia/src/ui/context/app/ai-context.tsx @@ -1,6 +1,5 @@ -import React, { createContext, FC, PropsWithChildren, useContext, useEffect } from 'react'; +import React, { createContext, FC, PropsWithChildren, useContext, useEffect, useRef } from 'react'; import { useFetcher, useFetchers, useParams } from 'react-router-dom'; -import { usePrevious } from 'react-use'; import { isLoggedIn } from '../../../account/session'; @@ -39,26 +38,26 @@ export const AIProvider: FC = ({ children }) => { const loading = useFetchers().filter(loader => loader.formAction?.includes('/ai/generate/')).some(loader => loader.state !== 'idle'); const loggedIn = isLoggedIn(); - - const prevProjectId = usePrevious(projectId); + const previousOrganizationIdRef = useRef(organizationId); useEffect(() => { if (!loggedIn) { return; } + const organizationIdHasChanged = previousOrganizationIdRef.current !== organizationId; const fetcherHasNotRun = aiAccessFetcher.state === 'idle' && !aiAccessFetcher.data; - const projectIdHasChanged = prevProjectId !== projectId; - if (fetcherHasNotRun || projectIdHasChanged) { + if (fetcherHasNotRun && organizationIdHasChanged) { + previousOrganizationIdRef.current = organizationId; aiAccessFetcher.submit({}, { method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/ai/access`, + action: `/organization/${organizationId}/ai/access`, }); } - }, [aiAccessFetcher, organizationId, projectId, workspaceId, loggedIn, prevProjectId]); + }, [aiAccessFetcher, organizationId, loggedIn]); - const isAIEnabled = aiAccessFetcher.data?.enabled ?? false; + const isAIEnabled = Boolean(aiAccessFetcher.data?.enabled); const aiGenerateTestsProgressStream = aiGenerateTestsFetcher.data as TransformStream; diff --git a/packages/insomnia/src/ui/context/app/presence-context.tsx b/packages/insomnia/src/ui/context/app/presence-context.tsx new file mode 100644 index 0000000000..92001aa161 --- /dev/null +++ b/packages/insomnia/src/ui/context/app/presence-context.tsx @@ -0,0 +1,162 @@ +import React, { createContext, FC, PropsWithChildren, useContext, useEffect, useState } from 'react'; +import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom'; +import { useInterval } from 'react-use'; + +import { getCurrentSessionId } from '../../../account/session'; +import { ProjectsLoaderData } from '../../routes/project'; + +const PresenceContext = createContext<{ + presence: UserPresence[]; +}>({ + presence: [], +}); + +// This happens because the API accepts teamIds as team_xxx +function sanitizeTeamId(teamId: string) { + return teamId.replace('proj_', ''); +} + +export interface UserPresence { + acct: string; + avatar: string; + branch: string; + file: string; + firstName: string; + lastName: string; + project: string; + team: string; +} + +interface UserPresenceEvent extends UserPresence { + type: 'PresentUserLeave' | 'PresentStateChanged' | 'OrganizationChanged'; +} + +export const PresenceProvider: FC = ({ children }) => { + const { + organizationId, + workspaceId, + } = useParams() as { + organizationId: string; + workspaceId: string; + }; + + const projectData = useRouteLoaderData('/project') as ProjectsLoaderData | null; + const remoteId = projectData?.activeProject.remoteId; + const [presence, setPresence] = useState([]); + const syncOrganizationsFetcher = useFetcher(); + + // Update presence when the user switches org, projects, workspaces + useEffect(() => { + async function updatePresence() { + const sessionId = getCurrentSessionId(); + if (sessionId && remoteId) { + try { + const response = await window.main.insomniaFetch<{ + data?: UserPresence[]; + }>({ + path: `/v1/organizations/${sanitizeTeamId(organizationId)}/collaborators`, + method: 'POST', + sessionId, + data: { + project: remoteId, + file: workspaceId, + }, + }); + + const rows = response?.data || []; + if (rows.length > 0) { + setPresence(rows); + } + } catch (e) { + console.log('Error parsing response', e); + } + } + } + + updatePresence(); + }, [organizationId, remoteId, workspaceId]); + + // Update presence every minute + useInterval(async () => { + const sessionId = getCurrentSessionId(); + if (sessionId && remoteId) { + try { + const response = await window.main.insomniaFetch<{ + data?: UserPresence[]; + }>({ + path: `/v1/organizations/${sanitizeTeamId(organizationId)}/collaborators`, + method: 'POST', + sessionId, + data: { + project: remoteId, + file: workspaceId, + }, + }); + + const rows = response?.data || []; + if (rows.length > 0) { + setPresence(rows); + } + } catch (e) { + console.log('Error parsing response', e); + } + } + }, 1000 * 60); + + useEffect(() => { + const sessionId = getCurrentSessionId(); + if (sessionId && remoteId) { + try { + const source = new EventSource(`insomnia-event-source://v1/teams/${sanitizeTeamId(organizationId)}/streams?sessionId=${sessionId}`); + + source.addEventListener('message', e => { + try { + const presenceEvent = JSON.parse(e.data) as UserPresenceEvent; + + if (presenceEvent.type === 'PresentUserLeave') { + setPresence(prev => prev.filter(p => { + const isSameUser = p.acct === presenceEvent.acct; + const isSameProjectFile = p.file === presenceEvent.file && p.project === presenceEvent.project; + + // Remove any presence events we have for the same user in this project/file + if (isSameUser && isSameProjectFile) { + return false; + } + + return true; + })); + } else if (presenceEvent.type === 'PresentStateChanged') { + setPresence(prev => [...prev.filter(p => p.acct !== presenceEvent.acct), presenceEvent]); + } else if (presenceEvent.type === 'OrganizationChanged') { + syncOrganizationsFetcher.submit({}, { + action: '/organization/sync', + method: 'POST', + }); + } + } catch (e) { + console.log('Error parsing response from SSE', e); + } + }); + return () => { + source.close(); + }; + } catch (e) { + console.log('ERROR', e); + return; + } + } + return; + }, [organizationId, remoteId, syncOrganizationsFetcher]); + + return ( + + {children} + + ); +}; + +export const usePresenceContext = () => useContext(PresenceContext); diff --git a/packages/insomnia/src/ui/css/main.css b/packages/insomnia/src/ui/css/main.css index c7b823bad7..9eb099e03d 100644 --- a/packages/insomnia/src/ui/css/main.css +++ b/packages/insomnia/src/ui/css/main.css @@ -804,7 +804,6 @@ strong { animation: AIfadeIn 0.5s 0.5s ease-in-out forwards; } /* Styles */ - /* Layout */ .form-control { outline: none; @@ -3466,3 +3465,11 @@ input.editable { bottom: 0; z-index: 9999; } + +.servers { + margin-top: 1rem; +} + +.pane-two { + z-index: 0; +} diff --git a/packages/insomnia/src/ui/css/styles.css b/packages/insomnia/src/ui/css/styles.css index 1650e3b157..5265d9795f 100644 --- a/packages/insomnia/src/ui/css/styles.css +++ b/packages/insomnia/src/ui/css/styles.css @@ -12,6 +12,19 @@ @tailwind components; @tailwind utilities; +html { + font-size: 11px; +} + +.grid-template-app-layout-with-banner { + grid-template: + 'Header Header' auto + 'Banner Banner' 30px + 'Navbar Content' 1fr + 'Statusbar Statusbar' 30px [row-end] + / 50px 1fr; +} + .grid-template-app-layout { grid-template: 'Header Header' auto @@ -25,6 +38,7 @@ flex-wrap: wrap; list-style: none; align-items: center; + flex-wrap: nowrap; margin: 0; padding: 0; gap: 0.5rem; @@ -36,10 +50,12 @@ align-items: center; gap: 0.5rem; color: var(--color-font); + outline: none; + box-shadow: none; } -.react-aria-Breadcrumbs .react-aria-Item:not(:last-child):after { - content: '/'; +.react-aria-Breadcrumbs .react-aria-Item:focus { + outline: none; } input[type="search"]::-webkit-search-cancel-button { diff --git a/packages/insomnia/src/ui/hooks/theme.ts b/packages/insomnia/src/ui/hooks/theme.ts index 339033cc8d..4dcbefa2d6 100644 --- a/packages/insomnia/src/ui/hooks/theme.ts +++ b/packages/insomnia/src/ui/hooks/theme.ts @@ -1,17 +1,16 @@ import { ChangeEvent, useCallback, useState } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { useAsync } from 'react-use'; import { ThemeSettings } from '../../models/settings'; import { ColorScheme, getThemes } from '../../plugins'; import { applyColorScheme, PluginTheme } from '../../plugins/misc'; -import { RootLoaderData } from '../routes/root'; +import { useRootLoaderData } from '../routes/root'; import { useSettingsPatcher } from './use-request'; export const useThemes = () => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { lightTheme, darkTheme, diff --git a/packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts b/packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts index 0034cdaccc..fe8f957a24 100644 --- a/packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts +++ b/packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts @@ -4,14 +4,14 @@ import * as plugins from '../../plugins'; import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; import { showModal } from '../components/modals'; import { SettingsModal, TAB_INDEX_SHORTCUTS } from '../components/modals/settings-modal'; -import { RootLoaderData } from '../routes/root'; +import { useRootLoaderData } from '../routes/root'; import { WorkspaceLoaderData } from '../routes/workspace'; import { useSettingsPatcher, useWorkspaceMetaPatcher } from './use-request'; export const useGlobalKeyboardShortcuts = () => { const workspaceData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData | undefined; const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const { activeWorkspaceMeta } = workspaceData || {}; const patchSettings = useSettingsPatcher(); const patchWorkspaceMeta = useWorkspaceMetaPatcher(); diff --git a/packages/insomnia/src/ui/hooks/use-settings-side-effects.ts b/packages/insomnia/src/ui/hooks/use-settings-side-effects.ts index b7457e6c64..137b8f148d 100644 --- a/packages/insomnia/src/ui/hooks/use-settings-side-effects.ts +++ b/packages/insomnia/src/ui/hooks/use-settings-side-effects.ts @@ -1,14 +1,13 @@ import { useEffect, useLayoutEffect } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import { usePrevious } from 'react-use'; import { Settings } from '../../models/settings'; -import { RootLoaderData } from '../routes/root'; +import { useRootLoaderData } from '../routes/root'; const useRestartSetting = (setting: keyof Settings) => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); const nextValue = settings[setting]; const previousValue = usePrevious(nextValue); @@ -33,7 +32,7 @@ const updateFontStyle = (key: string, value: string | null) => document?.querySe export const useSettingsSideEffects = () => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); useLayoutEffect(() => { updateFontStyle('--font-default', settings.fontInterface); diff --git a/packages/insomnia/src/ui/hooks/use-sync-migration.ts b/packages/insomnia/src/ui/hooks/use-sync-migration.ts deleted file mode 100644 index 240aac76cf..0000000000 --- a/packages/insomnia/src/ui/hooks/use-sync-migration.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useAsync } from 'react-use'; - -import { onLoginLogout } from '../../account/session'; -import FileSystemDriver from '../../sync/store/drivers/file-system-driver'; -import { migrateCollectionsIntoRemoteProject } from '../../sync/vcs/migrate-collections'; -import { VCS } from '../../sync/vcs/vcs'; - -const check = async () => { - const driver = FileSystemDriver.create(process.env['INSOMNIA_DATA_PATH'] || window.app.getPath('userData')); - await migrateCollectionsIntoRemoteProject(new VCS(driver)); -}; - -// Check on login / logout -onLoginLogout(isLoggedIn => { - if (isLoggedIn) { - check(); - } -}); - -export const useSyncMigration = () => { - // Check once on mount - useAsync(check, []); -}; diff --git a/packages/insomnia/src/ui/hooks/use-theme-change.ts b/packages/insomnia/src/ui/hooks/use-theme-change.ts index 20b497f787..3607e1b2ab 100644 --- a/packages/insomnia/src/ui/hooks/use-theme-change.ts +++ b/packages/insomnia/src/ui/hooks/use-theme-change.ts @@ -1,13 +1,12 @@ import { useEffect } from 'react'; -import { useRouteLoaderData } from 'react-router-dom'; import * as themes from '../../plugins/misc'; -import { RootLoaderData } from '../routes/root'; +import { useRootLoaderData } from '../routes/root'; export const useThemeChange = () => { const { settings, - } = useRouteLoaderData('root') as RootLoaderData; + } = useRootLoaderData(); // Handle System Theme change useEffect(() => { const matches = window.matchMedia('(prefers-color-scheme: dark)'); diff --git a/packages/insomnia/src/ui/images/onboarding/collaborators_rbac.png b/packages/insomnia/src/ui/images/onboarding/collaborators_rbac.png new file mode 100644 index 0000000000..bc57e5cb91 Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/collaborators_rbac.png differ diff --git a/packages/insomnia/src/ui/images/onboarding/insomnia_ai.png b/packages/insomnia/src/ui/images/onboarding/insomnia_ai.png new file mode 100644 index 0000000000..24c86c018a Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/insomnia_ai.png differ diff --git a/packages/insomnia/src/ui/images/onboarding/introducing_organizations.png b/packages/insomnia/src/ui/images/onboarding/introducing_organizations.png new file mode 100644 index 0000000000..6c9ec018ff Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/introducing_organizations.png differ diff --git a/packages/insomnia/src/ui/images/onboarding/new_cloud_dashboard.png b/packages/insomnia/src/ui/images/onboarding/new_cloud_dashboard.png new file mode 100644 index 0000000000..03dda90c95 Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/new_cloud_dashboard.png differ diff --git a/packages/insomnia/src/ui/images/onboarding/real_time_collaboration.png b/packages/insomnia/src/ui/images/onboarding/real_time_collaboration.png new file mode 100644 index 0000000000..d3742462e9 Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/real_time_collaboration.png differ diff --git a/packages/insomnia/src/ui/images/onboarding/sharing_projects.png b/packages/insomnia/src/ui/images/onboarding/sharing_projects.png new file mode 100644 index 0000000000..549fc33900 Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/sharing_projects.png differ diff --git a/packages/insomnia/src/ui/images/onboarding/social_and_enterprise_sso.png b/packages/insomnia/src/ui/images/onboarding/social_and_enterprise_sso.png new file mode 100644 index 0000000000..5564f6a861 Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/social_and_enterprise_sso.png differ diff --git a/packages/insomnia/src/ui/images/onboarding/sse_api_support.png b/packages/insomnia/src/ui/images/onboarding/sse_api_support.png new file mode 100644 index 0000000000..262c5b9a8d Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/sse_api_support.png differ diff --git a/packages/insomnia/src/ui/images/onboarding/unlimited_projects.png b/packages/insomnia/src/ui/images/onboarding/unlimited_projects.png new file mode 100644 index 0000000000..84d3005d28 Binary files /dev/null and b/packages/insomnia/src/ui/images/onboarding/unlimited_projects.png differ diff --git a/packages/insomnia/src/ui/index.tsx b/packages/insomnia/src/ui/index.tsx index 41e58f423f..1df77d312a 100644 --- a/packages/insomnia/src/ui/index.tsx +++ b/packages/insomnia/src/ui/index.tsx @@ -9,27 +9,35 @@ import { RouterProvider, } from 'react-router-dom'; +import { isLoggedIn, SessionData, setSessionData } from '../account/session'; import { ACTIVITY_DEBUG, ACTIVITY_SPEC, + getInsomniaSession, getProductName, + getSkipOnboarding, isDevelopment, } from '../common/constants'; import { database } from '../common/database'; import { initializeLogging } from '../common/log'; import * as models from '../models'; -import { DEFAULT_ORGANIZATION_ID } from '../models/organization'; -import { DEFAULT_PROJECT_ID } from '../models/project'; import { initNewOAuthSession } from '../network/o-auth-2/get-token'; import { init as initPlugins } from '../plugins'; import { applyColorScheme } from '../plugins/misc'; import { invariant } from '../utils/invariant'; import { AppLoadingIndicator } from './components/app-loading-indicator'; +import Auth from './routes/auth'; +import Authorize from './routes/auth.authorize'; +import Login from './routes/auth.login'; +import { Migrate } from './routes/auth.migrate'; import { ErrorRoute } from './routes/error'; +import Onboarding from './routes/onboarding'; +import { OnboardingCloudMigration } from './routes/onboarding.cloud-migration'; import { shouldOrganizationsRevalidate } from './routes/organization'; import Root from './routes/root'; import { initializeSentry } from './sentry'; +const Organization = lazy(() => import('./routes/organization')); const Project = lazy(() => import('./routes/project')); const Workspace = lazy(() => import('./routes/workspace')); const UnitTest = lazy(() => import('./routes/unit-test')); @@ -42,24 +50,87 @@ initializeLogging(); document.body.setAttribute('data-platform', process.platform); document.title = getProductName(); -let locationHistoryEntry = `/organization/${DEFAULT_ORGANIZATION_ID}/project/${DEFAULT_PROJECT_ID}`; -const prevLocationHistoryEntry = localStorage.getItem('locationHistoryEntry'); +try { + // In order to run playwight tests that simulate a logged in user + // we need to inject state into localStorage + const skipOnboarding = getSkipOnboarding(); + const insomniaSession = getInsomniaSession(); + if (skipOnboarding) { + window.localStorage.setItem('hasSeenOnboarding', skipOnboarding.toString()); + window.localStorage.setItem('hasUserLoggedInBefore', skipOnboarding.toString()); + } -if (prevLocationHistoryEntry && matchPath({ path: '/organization/:organizationId', end: false }, prevLocationHistoryEntry)) { - locationHistoryEntry = prevLocationHistoryEntry; + if (insomniaSession) { + const session = JSON.parse(insomniaSession) as SessionData; + setSessionData( + session.id, + session.sessionExpiry, + session.accountId, + session.firstName, + session.lastName, + session.email, + session.symmetricKey, + session.publicKey, + session.encPrivateKey + ); + } +} catch (e) { + console.log('Failed to parse session data', e); } +function getInitialEntry() { + // If the user has not seen the onboarding, then show it + // Otherwise if the user is not logged in and has not logged in before, then show the login + // Otherwise if the user is logged in, then show the organization + try { + const hasSeenOnboarding = Boolean(window.localStorage.getItem('hasSeenOnboarding')); + + if (!hasSeenOnboarding) { + return '/onboarding'; + } + + const hasUserLoggedInBefore = window.localStorage.getItem('hasUserLoggedInBefore'); + + if (isLoggedIn()) { + return '/organization'; + } + + if (hasUserLoggedInBefore) { + return '/auth/login'; + } + + return '/scratchpad'; + } catch (e) { + return '/scratchpad'; + } +} + +const initialEntry = getInitialEntry(); + const router = createMemoryRouter( // @TODO - Investigate file based routing to generate these routes: [ { path: '/', id: 'root', - loader: async (...args) => - (await import('./routes/root')).loader(...args), element: , + loader: async (...args) => (await import('./routes/root')).loader(...args), errorElement: , children: [ + { + path: '/scratchpad', + loader: async (...args) => (await import('./routes/scratchpad')).loader(...args), + }, + { + path: 'onboarding/*', + element: , + }, + { + path: 'onboarding/cloud-migration', + loader: async (...args) => (await import('./routes/onboarding.cloud-migration')).loader(...args), + action: async (...args) => (await import('./routes/onboarding.cloud-migration')).action(...args), + element: , + }, { path: 'import', children: [ @@ -87,19 +158,42 @@ const router = createMemoryRouter( { path: 'organization', id: '/organization', - shouldRevalidate: shouldOrganizationsRevalidate, loader: async (...args) => (await import('./routes/organization')).loader(...args), + element: }>, children: [ + { + index: true, + loader: async (...args) => (await import('./routes/organization')).indexLoader(...args), + }, + { + path: 'sync', + action: async (...args) => (await import('./routes/organization')).syncOrganizationsAction(...args), + }, { path: ':organizationId', + id: ':organizationId', + shouldRevalidate: shouldOrganizationsRevalidate, + loader: async (...args) => + ( + await import('./routes/organization') + ).singleOrgLoader(...args), children: [ { index: true, loader: async (...args) => (await import('./routes/project')).indexLoader(...args), }, + { + path: 'ai/access', + action: async (...args) => + ( + await import('./routes/actions') + ).accessAIApiAction(...args), + }, { path: 'project', + id: '/project', + loader: async (...args) => (await import('./routes/project')).projectLoader(...args), children: [ { path: ':projectId', @@ -579,13 +673,6 @@ const router = createMemoryRouter( }, ], }, - { - path: 'access', - action: async (...args) => - ( - await import('./routes/actions') - ).accessAIApiAction(...args), - }, ], }, { @@ -778,12 +865,40 @@ const router = createMemoryRouter( }, ], }, + { + path: 'auth', + element: }> + + , + children: [ + { + path: 'login', + action: async (...args) => (await import('./routes/auth.login')).action(...args), + element: , + }, + { + path: 'logout', + action: async (...args) => (await import('./routes/auth.logout')).action(...args), + }, + { + path: 'authorize', + action: async (...args) => (await import('./routes/auth.authorize')).action(...args), + element: , + }, + { + path: 'migrate', + loader: async (...args) => (await import('./routes/auth.migrate')).loader(...args), + action: async (...args) => (await import('./routes/auth.migrate')).action(...args), + element: , + }, + ], + }, ], }, ], { - initialEntries: [locationHistoryEntry], - }, + initialEntries: [initialEntry], + } ); // Store the last location in local storage @@ -802,7 +917,6 @@ router.subscribe(({ location }) => { async function renderApp() { await database.initClient(); - await initPlugins(); const settings = await models.settings.getOrCreate(); diff --git a/packages/insomnia/src/ui/rendererListeners.ts b/packages/insomnia/src/ui/rendererListeners.ts index 7f94352d24..be30953e81 100644 --- a/packages/insomnia/src/ui/rendererListeners.ts +++ b/packages/insomnia/src/ui/rendererListeners.ts @@ -2,7 +2,6 @@ import { isDevelopment } from '../common/constants'; import { database } from '../common/database'; import * as models from '../models'; -import { isNotDefaultProject } from '../models/project'; import * as plugins from '../plugins'; import * as themes from '../plugins/misc'; import * as templating from '../templating'; @@ -33,9 +32,7 @@ if (isDevelopment()) { const bufferId = await database.bufferChanges(); console.log(`[developer] clearing all "${type}" entities`); const allEntities = await database.all(type); - const filteredEntites = allEntities - .filter(isNotDefaultProject); // don't clear the default project - await database.batchModifyDocs({ remove: filteredEntites }); + await database.batchModifyDocs({ remove: allEntities }); database.flushChanges(bufferId); } }, @@ -57,9 +54,7 @@ if (isDevelopment()) { .reverse().map(async type => { console.log(`[developer] clearing all "${type}" entities`); const allEntities = await database.all(type); - const filteredEntites = allEntities - .filter(isNotDefaultProject); // don't clear the default project - await database.batchModifyDocs({ remove: filteredEntites }); + await database.batchModifyDocs({ remove: allEntities }); }); await Promise.all(promises); database.flushChanges(bufferId); @@ -70,7 +65,7 @@ if (isDevelopment()) { } window.main.on('reload-plugins', async () => { - const settings = await models.settings.getOrCreate(); + const settings = await models.settings.get(); await plugins.reloadPlugins(); await themes.applyColorScheme(settings); templating.reload(); diff --git a/packages/insomnia/src/ui/routes/actions.tsx b/packages/insomnia/src/ui/routes/actions.tsx index 33d7f328c6..8dee1907f3 100644 --- a/packages/insomnia/src/ui/routes/actions.tsx +++ b/packages/insomnia/src/ui/routes/actions.tsx @@ -5,15 +5,13 @@ import { ActionFunction, redirect } from 'react-router-dom'; import * as session from '../../account/session'; import { parseApiSpec, resolveComponentSchemaRefs } from '../../common/api-specs'; -import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../../common/constants'; +import { ACTIVITY_DEBUG, ACTIVITY_SPEC, getAIServiceURL } from '../../common/constants'; import { database } from '../../common/database'; import { database as db } from '../../common/database'; import { importResourcesToWorkspace, scanResources } from '../../common/import'; import { generateId } from '../../common/misc'; import * as models from '../../models'; import { getById, update } from '../../models/helpers/request-operations'; -import { DEFAULT_ORGANIZATION_ID } from '../../models/organization'; -import { DEFAULT_PROJECT_ID, isRemoteProject } from '../../models/project'; import { isRequest, Request } from '../../models/request'; import { isRequestGroup, isRequestGroupId } from '../../models/request-group'; import { UnitTest } from '../../models/unit-test'; @@ -32,9 +30,51 @@ export const createNewProjectAction: ActionFunction = async ({ request, params } const formData = await request.formData(); const name = formData.get('name'); invariant(typeof name === 'string', 'Name is required'); - const project = await models.project.create({ name }); - window.main.trackSegmentEvent({ event: SegmentEvent.projectLocalCreate }); - return redirect(`/organization/${organizationId}/project/${project._id}`); + + const sessionId = session.getCurrentSessionId(); + invariant(sessionId, 'User must be logged in to create a project'); + + try { + const newCloudProject = await window.main.insomniaFetch<{ + id: string; + name: string; + } | { + error: string; + message?: string; + }>({ + path: `/v1/organizations/${organizationId}/team-projects`, + method: 'POST', + data: { + name, + }, + sessionId, + }); + + if (!newCloudProject || 'error' in newCloudProject) { + let error = 'An unexpected error occurred while creating the project. Please try again.'; + if (newCloudProject.error === 'FORBIDDEN' || newCloudProject.error === 'NEEDS_TO_UPGRADE') { + error = newCloudProject.error; + } + + return { + error, + }; + } + + const project = await models.project.create({ + _id: newCloudProject.id, + name: newCloudProject.name, + remoteId: newCloudProject.id, + parentId: organizationId, + }); + + return redirect(`/organization/${organizationId}/project/${project._id}`); + } catch (err) { + console.log(err); + return { + error: err instanceof Error ? err.message : `An unexpected error occurred while creating the project. Please try again. ${err}`, + }; + } }; export const renameProjectAction: ActionFunction = async ({ @@ -53,14 +93,36 @@ export const renameProjectAction: ActionFunction = async ({ invariant(project, 'Project not found'); - invariant( - !isRemoteProject(project), - 'Cannot rename remote project', - ); + const sessionId = session.getCurrentSessionId(); + invariant(sessionId, 'User must be logged in to rename a project'); - await models.project.update(project, { name }); + try { + const response = await window.main.insomniaFetch({ + path: `/v1/organizations/${project.parentId}/team-projects/${project.remoteId}`, + method: 'PATCH', + sessionId, + data: { + name, + }, + }); - return null; + if (response && 'error' in response) { + return { + error: response.error === 'FORBIDDEN' ? 'You do not have permission to rename this project.' : 'An unexpected error occurred while renaming the project. Please try again.', + }; + } + + await models.project.update(project, { name }); + return null; + } catch (err) { + console.log(err); + return { + error: err instanceof Error ? err.message : `An unexpected error occurred while renaming the project. Please try again. ${err}`, + }; + } }; export const deleteProjectAction: ActionFunction = async ({ params }) => { @@ -70,12 +132,35 @@ export const deleteProjectAction: ActionFunction = async ({ params }) => { const project = await models.project.getById(projectId); invariant(project, 'Project not found'); - await models.stats.incrementDeletedRequestsForDescendents(project); - await models.project.remove(project); + const sessionId = session.getCurrentSessionId(); + invariant(sessionId, 'User must be logged in to delete a project'); - window.main.trackSegmentEvent({ event: SegmentEvent.projectLocalDelete }); + try { + const response = await window.main.insomniaFetch({ + path: `/v1/organizations/${organizationId}/team-projects/${project.remoteId}`, + method: 'DELETE', + sessionId, + }); - return redirect(`/organization/${DEFAULT_ORGANIZATION_ID}/project/${DEFAULT_PROJECT_ID}`); + if (response && 'error' in response) { + return { + error: response.error === 'FORBIDDEN' ? 'You do not have permission to delete this project.' : 'An unexpected error occurred while deleting the project. Please try again.', + }; + } + + await models.stats.incrementDeletedRequestsForDescendents(project); + await models.project.remove(project); + + return redirect(`/organization/${organizationId}`); + } catch (err) { + console.log(err); + return { + error: err instanceof Error ? err.message : `An unexpected error occurred while deleting the project. Please try again. ${err}`, + }; + } }; // Workspace @@ -107,8 +192,6 @@ export const createNewWorkspaceAction: ActionFunction = async ({ parentId: projectId, }); - console.log({ workspace, name }); - if (scope === 'design') { await models.apiSpec.getOrCreateForParentId(workspace._id); } @@ -119,7 +202,7 @@ export const createNewWorkspaceAction: ActionFunction = async ({ const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); await database.flushChanges(flushId); - if (session.isLoggedIn() && isRemoteProject(project) && !workspaceMeta.gitRepositoryId) { + if (session.isLoggedIn() && !workspaceMeta.gitRepositoryId) { const vcs = getVCS(); if (vcs) { await initializeLocalBackendProjectAndMarkForSync({ @@ -222,7 +305,7 @@ export const duplicateWorkspaceAction: ActionFunction = async ({ request, params try { // Mark for sync if logged in and in the expected project const vcs = getVCS(); - if (session.isLoggedIn() && vcs && isRemoteProject(duplicateToProject)) { + if (session.isLoggedIn() && vcs) { await initializeLocalBackendProjectAndMarkForSync({ vcs: vcs.newInstance(), workspace: newWorkspace, @@ -627,10 +710,9 @@ export const generateCollectionAndTestsAction: ActionFunction = async ({ params } const methodInfo = resolveComponentSchemaRefs(spec, getMethodInfo(request)); - const response = await window.main.insomniaFetch<{ test: { requestId: string } }>({ method: 'POST', - origin: 'https://ai.insomnia.rest', + origin: getAIServiceURL(), path: '/v1/generate-test', sessionId: session.getCurrentSessionId(), data: { @@ -706,12 +788,12 @@ export const generateTestsAction: ActionFunction = async ({ params }) => { total, }); - for (const test of tests) { - async function generateTest() { + async function generateTests() { + async function generateTest(test: Partial) { try { const response = await window.main.insomniaFetch<{ test: { requestId: string } }>({ method: 'POST', - origin: 'https://ai.insomnia.rest', + origin: getAIServiceURL(), path: '/v1/generate-test', sessionId: session.getCurrentSessionId(), data: { @@ -737,30 +819,31 @@ export const generateTestsAction: ActionFunction = async ({ params }) => { } } - generateTest(); + for (const test of tests) { + await generateTest(test); + } } + generateTests(); + return progressStream; }; export const accessAIApiAction: ActionFunction = async ({ params }) => { - const { organizationId, projectId, workspaceId } = params; + const { organizationId } = params; invariant(typeof organizationId === 'string', 'Organization ID is required'); - invariant(typeof projectId === 'string', 'Project ID is required'); - invariant(typeof workspaceId === 'string', 'Workspace ID is required'); try { const response = await window.main.insomniaFetch<{ enabled: boolean }>({ method: 'POST', - origin: 'https://ai.insomnia.rest', + origin: getAIServiceURL(), path: '/v1/access', sessionId: session.getCurrentSessionId(), data: { teamId: organizationId, }, }); - return { enabled: response.enabled, }; diff --git a/packages/insomnia/src/ui/routes/auth.authorize.tsx b/packages/insomnia/src/ui/routes/auth.authorize.tsx new file mode 100644 index 0000000000..c0d113c69f --- /dev/null +++ b/packages/insomnia/src/ui/routes/auth.authorize.tsx @@ -0,0 +1,170 @@ +import React, { Fragment } from 'react'; +import { Heading } from 'react-aria-components'; +import { ActionFunction, redirect, useFetcher, useFetchers, useNavigate } from 'react-router-dom'; + +import { isLoggedIn } from '../../account/session'; +import { shouldRunMigration } from '../../sync/vcs/migrate-to-cloud-projects'; +import { invariant } from '../../utils/invariant'; +import { getLoginUrl, submitAuthCode } from '../auth-session-provider'; +import { Icon } from '../components/icon'; +import { Button } from '../components/themed-button'; + +export const action: ActionFunction = async ({ + request, +}) => { + const data = await request.json(); + + invariant(typeof data?.code === 'string', 'Expected code to be a string'); + const fetchError = await submitAuthCode(data.code); + if (fetchError) { + return { + errors: { + message: 'Invalid code: ' + fetchError, + }, + }; + } + console.log('Login successful'); + window.localStorage.setItem('hasLoggedIn', 'true'); + + if (isLoggedIn() && await shouldRunMigration()) { + throw redirect('/auth/migrate'); + } + + return redirect('/organization'); +}; + +const Authorize = () => { + const url = getLoginUrl(); + const copyUrl = () => { + window.clipboard.writeText(url); + }; + + const authorizeFetcher = useFetcher(); + const navigate = useNavigate(); + + const allFetchers = useFetchers(); + const authFetchers = allFetchers.filter(f => f.formAction === '/auth/authorize'); + + const isAuthenticating = authFetchers.some(f => f.state !== 'idle'); + + return ( +
+ + Authorizing Insomnia + + {!isAuthenticating && ( + +

+ A new page should have opened in your default web browser. To + continue, please log in via the browser. +

+
+ +