diff --git a/docs/build-your-software-catalog/custom-integration/api/_category_.json b/docs/build-your-software-catalog/custom-integration/api/_category_.json index a1e3abeaca..f3c5fb72d4 100644 --- a/docs/build-your-software-catalog/custom-integration/api/_category_.json +++ b/docs/build-your-software-catalog/custom-integration/api/_category_.json @@ -1,4 +1,4 @@ { "label": "API", - "position": 1 + "position": 2 } diff --git a/docs/build-your-software-catalog/custom-integration/ocean-http/_category_.json b/docs/build-your-software-catalog/custom-integration/ocean-http/_category_.json new file mode 100644 index 0000000000..95a43fddf2 --- /dev/null +++ b/docs/build-your-software-catalog/custom-integration/ocean-http/_category_.json @@ -0,0 +1,19 @@ +{ + "position": 1, + "label": "Generic HTTP (New โญ)", + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index" + } +} + + + + + + + + + + diff --git a/docs/build-your-software-catalog/custom-integration/ocean-http/build-your-integration.md b/docs/build-your-software-catalog/custom-integration/ocean-http/build-your-integration.md new file mode 100644 index 0000000000..90575f4453 --- /dev/null +++ b/docs/build-your-software-catalog/custom-integration/ocean-http/build-your-integration.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 2 +title: Build Your Integration +description: Interactive guide to configure and install your integration +--- + +import PortApiRegionTip from "/docs/generalTemplates/_port_api_available_regions.md" +import { IntegrationBuilderProvider, Step1ApiConfig, Step2DataMapping, Step3Installation } from '@site/src/components/GenericHttp'; + +# Build Your Integration + +This interactive guide will help you generate everything you need to connect your API to Port. + +**How it works:** +1. Configure your API connection settings +2. Choose an endpoint and select which fields to sync +3. Get your installation commands, blueprint, and mapping configuration + + + +--- + +## Step 1: Configure Your API + +Set up the connection details for your API. These settings apply globally to all endpoints you'll sync from this API. + +**What you're configuring:** +- **Base URL**: The root URL that all endpoint paths will be appended to +- **Authentication**: How your API verifies requests (bearer token, API key, basic auth, or none) +- **Pagination** (optional): How to fetch data across multiple pages if your API uses pagination +- **Performance settings** (optional): Timeouts, concurrent requests, SSL verification + +Think of this as setting up the "connection" - these settings will be used for every API call the integration makes. + + + +--- + +## Step 2: Choose What Data to Sync + +Now that your API connection is configured, let's define what data to sync. This step helps you map a specific API endpoint to a Port blueprint. + +**What you'll do:** +1. **Specify the endpoint path** (e.g., `/api/v1/users`) that you want to sync +2. **Paste a sample API response** so we can detect the data structure +3. **Select the data path** - tell us where the array of items is in the response (e.g., `.data`, `.users`, or root array) +4. **Configure the blueprint** - give it an identifier and title +5. **Choose which fields to sync** - select the fields you want to ingest and mark which field is the unique identifier + +The builder will automatically detect field types (string, number, boolean, email, date, URL) from your sample response. + + + +--- + +## Step 3: Install and Create in Port + +You're all set! Based on your configuration, we've generated everything you need: + +**What you'll get:** +- **Installation commands** (Helm or Docker) with all your settings pre-configured +- **Blueprint JSON** to create the data model in Port +- **Mapping YAML** to configure which fields to sync + +Simply copy and run these in order to complete your integration setup. + + + +:::info Port credentials needed +Get your `PORT_CLIENT_ID` and `PORT_CLIENT_SECRET` from [Port Settings โ†’ Credentials](https://app.getport.io/settings). +::: + + diff --git a/docs/build-your-software-catalog/custom-integration/ocean-http/overview.md b/docs/build-your-software-catalog/custom-integration/ocean-http/overview.md new file mode 100644 index 0000000000..cda49c858b --- /dev/null +++ b/docs/build-your-software-catalog/custom-integration/ocean-http/overview.md @@ -0,0 +1,338 @@ +--- +sidebar_position: 1 +title: Overview +description: Understanding the Generic HTTP Integration +--- + +# Overview + +This integration allows Port customers to connect to any custom API, internal system, or HTTP service without requiring custom development. Each integration instance connects to one API backend, and users can map multiple endpoints through standard Ocean resource configuration. + +--- + +## When to Use This Integration? + +This integration is ideal when: + +- **No native Port integration exists** for your tool or service +- You're working with **internal or custom-built APIs** +- Your API follows **REST conventions** (JSON responses, HTTP methods) +- You want a **configuration-only solution** without custom code + +--- + +## Prerequisites + +Before installing, gather this information about your API: + +### 1. Authentication + +How does your API verify requests? + +- **Bearer Token:** OAuth2 tokens, personal access tokens (most modern APIs) +- **API Key:** Custom header like `X-API-Key` or `Authorization` +- **Basic Auth:** Username and password (legacy systems) +- **None:** Public APIs + +**Where to find it:** Check your API's documentation or settings page. Look for sections titled "API Keys," "Access Tokens," or "Authentication." + +### 2. Endpoints + +Which API endpoint returns the data you want to ingest? + +**Example:** `/api/v1/users`, `/v2/projects`, `/tickets` + +**How to find it:** Check your API documentation for available endpoints. Look for GET endpoints that return lists of resources. + +### 3. Data Structure + +Where is the actual data in your API's response? + +**Direct array:** +```json +[ + {"id": 1, "name": "Alice"}, + {"id": 2, "name": "Bob"} +] +``` + +**Nested data:** +```json +{ + "data": [ + {"id": 1, "name": "Alice"}, + {"id": 2, "name": "Bob"} + ] +} +``` + +**Deeply nested:** +```json +{ + "response": { + "users": { + "items": [ + {"id": 1, "name": "Alice"} + ] + } + } +} +``` + +You'll use a [JQ](https://jqlang.org/manual/) `data_path` expression in your mapping to tell the integration where to find the array of items (e.g., `.data`, `.users.items`). + +--- + +## How It Works + +The Generic HTTP integration uses a [**two-step setup**](/build-your-software-catalog/sync-data-to-catalog/) similar to other Ocean integrations you've used: + +### Step 1: Installation (Global Configuration) + +During installation, you configure the **connection settings** that apply to all API calls: + +- **Base URL:** The root URL of your API (e.g., `https://api.yourcompany.com`) +- **Authentication:** How to authenticate (bearer token, API key, basic auth, or none) +- **Pagination:** How your API handles large datasets (offset, page, cursor, or none) +- **Rate limiting:** Timeout, concurrent requests, SSL verification + +Think of this as setting up the "connection" to your API - these settings are used for every endpoint you'll sync. + +**Installation methods:** Docker or Helm (just like other Ocean integrations) + +#### Example: Installing with Helm + +```bash +helm repo add --force-update port-labs https://port-labs.github.io/helm-charts +helm install generic-http port-labs/port-ocean \ + --set port.clientId="" \ + --set port.clientSecret="" \ + --set port.baseUrl="https://api.getport.io" \ + --set initializePortResources=true \ + --set scheduledResyncInterval=60 \ + --set integration.identifier="generic-http" \ + --set integration.type="generic-http" \ + --set integration.eventListener.type="POLLING" \ + --set integration.config.baseUrl="https://api.yourcompany.com" \ + --set integration.config.authType="bearer_token" \ + --set integration.secrets.authValue="" \ + --set integration.config.paginationType="page" \ + --set integration.config.pageSize=100 +``` + +### Step 2: Resource Mapping + +After installation, you define **which endpoints to sync** in your `port-app-config.yml` file (or using the integration's configuration in Port). + +This is where you map each API endpoint to Port entities - similar to how you've mapped GitHub repositories or Jira issues in other integrations. + +#### ๐Ÿ†• Endpoint-as-Kind Feature + +The `kind` field is now the **endpoint path itself**! This provides better visibility in Port's UI, allowing you to: + +- โœ… Track each endpoint's sync status individually +- โœ… Debug mapping issues per endpoint +- โœ… Monitor data ingestion per API call + +#### Example: Mapping Two Endpoints + +```yaml +resources: + # First endpoint: users + - kind: /api/v1/users + selector: + query: 'true' # JQ filter - 'true' means sync all entities + data_path: '.users' # Where to find the data array in the response + query_params: # Optional: add query parameters to the API call + active: "true" + department: "engineering" + port: + entity: + mappings: + identifier: .id + title: .name + blueprint: '"user"' + properties: + email: .email + department: .department + active: .is_active + created: .created_at + + # Second endpoint: projects + - kind: /api/v1/projects + selector: + query: 'true' + data_path: '.data.projects' # Nested data extraction + query_params: + status: "active" + port: + entity: + mappings: + identifier: .project_id + title: .project_name + blueprint: '"project"' + properties: + description: .description + owner: .owner.email + budget: .budget_amount + created: .created_date +``` + +#### What Each Field Does + +- **`kind`**: The API endpoint path (combined with your base URL) +- **`selector.query`**: JQ filter to include/exclude entities (use `'true'` to sync all) +- **`selector.data_path`**: JQ expression pointing to the array of items in the response +- **`selector.query_params`**: (Optional) Query parameters added to the URL +- **`selector.method`**: (Optional) HTTP method, defaults to `GET` +- **`port.entity.mappings`**: How to map API fields to Port entity properties + +--- + +## Advanced Configurations + +Once you have the basics working, these features handle more complex scenarios. + +### Nested Endpoints + +Fetch data from dynamic endpoints that depend on other resources. + +**Use case:** Get all tickets, then fetch comments for each ticket. + +#### How It Works + +**Step 1 - Define parent endpoint:** +```yaml +resources: + - kind: /api/tickets + port: + entity: + mappings: + identifier: .id | tostring + blueprint: '"ticket"' +``` + +**Step 2 - Define nested endpoint:** +```yaml +resources: + - kind: /api/tickets/{ticket_id}/comments + selector: + path_parameters: + ticket_id: + endpoint: /api/tickets + field: .id + filter: 'true' + port: + entity: + mappings: + identifier: .id | tostring + blueprint: '"comment"' + relations: + ticket: .ticket_id | tostring +``` + +The integration will: +1. Call `/api/tickets` โ†’ Get ticket IDs [101, 102, 103] +2. Call `/api/tickets/101/comments`, `/api/tickets/102/comments`, `/api/tickets/103/comments` +3. Sync all comments with relations to their parent tickets + +**Real-world examples:** +- `/projects/{project_id}/tasks` - Tasks within projects +- `/repositories/{repo_id}/pull-requests` - PRs in repositories +- `/customers/{customer_id}/orders` - Orders for customers + +### Pagination + +For APIs that split data across multiple pages, configure how the integration fetches all pages. + +#### Pagination Types + +**Offset-based** (like SQL): +``` +GET /api/users?offset=0&limit=100 +GET /api/users?offset=100&limit=100 +``` + +**Page-based** (traditional): +``` +GET /api/users?page=1&size=100 +GET /api/users?page=2&size=100 +``` + +**Cursor-based** (for large datasets): +``` +GET /api/users?cursor=abc123&limit=100 +GET /api/users?cursor=xyz789&limit=100 +``` + +#### Custom Parameter Names + +APIs often use different parameter names. You can configure: + +- **Pagination parameter:** Use `skip` instead of `offset`, or `after` instead of `cursor` +- **Size parameter:** Use `per_page` instead of `limit`, or `page_size` instead of `size` +- **Start page:** Specify if pages start at 0 or 1 + +**Example:** +```yaml +# GitHub uses page/per_page +paginationType: page +paginationParam: page +sizeParam: per_page +startPage: 1 + +# Stripe uses limit/starting_after +paginationType: cursor +paginationParam: starting_after +sizeParam: limit +``` + +#### Cursor Path Configuration + +For cursor-based pagination, tell the integration where to find the next cursor in responses: + +**Example API response:** +```json +{ + "data": [...], + "meta": { + "after_cursor": "xyz123", + "has_more": true + } +} +``` + +**Configuration:** +```yaml +cursorPath: meta.after_cursor +hasMorePath: meta.has_more +``` + +### Rate Limiting + +Control how the integration interacts with your API to prevent overwhelming it or hitting rate limits. + +#### Request Timeout + +How long to wait for each API call to complete. + +```yaml +timeout: 30 # seconds (default: 30) +``` + +**When to adjust:** +- Increase for slow APIs or large responses (e.g., 60 seconds) +- Decrease for fast, local APIs (e.g., 10 seconds) + +--- + +## Ready to Build? + +Head to [Build Your Integration](./build-your-integration) for a step-by-step guide with an interactive configuration builder. + +--- + +## More Resources + +For all configuration options, code examples, and advanced use cases, check out the [Generic HTTP integration repository on GitHub](https://github.com/port-labs/ocean/tree/main/integrations/generic-http). + diff --git a/package-lock.json b/package-lock.json index 654531a6e0..58e4f042c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "docusaurus-plugin-image-zoom": "^3.0.1", "docusaurus-plugin-openapi-docs": "^4.5.1", "docusaurus-theme-openapi-docs": "^4.5.1", + "pluralize": "^8.0.0", "prettier": "^3.6.2", "prism-react-renderer": "^2.4.1", "react": "^19.2.0", diff --git a/package.json b/package.json index f54d9fa53e..8434f6b9a9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "docusaurus-plugin-image-zoom": "^3.0.1", "docusaurus-plugin-openapi-docs": "^4.5.1", "docusaurus-theme-openapi-docs": "^4.5.1", + "pluralize": "^8.0.0", "prettier": "^3.6.2", "prism-react-renderer": "^2.4.1", "react": "^19.2.0", diff --git a/src/components/GenericHttp/ApiConfigBuilder.jsx b/src/components/GenericHttp/ApiConfigBuilder.jsx new file mode 100644 index 0000000000..af839796a5 --- /dev/null +++ b/src/components/GenericHttp/ApiConfigBuilder.jsx @@ -0,0 +1,210 @@ +import React, { useState } from 'react'; +import styles from './styles.module.css'; + +export function ApiConfigBuilder() { + const [baseUrl, setBaseUrl] = useState(''); + const [authType, setAuthType] = useState('bearer_token'); + const [apiToken, setApiToken] = useState(''); + const [apiKey, setApiKey] = useState(''); + const [apiKeyHeader, setApiKeyHeader] = useState('X-API-Key'); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [paginationType, setPaginationType] = useState('none'); + const [pageSize, setPageSize] = useState('100'); + + const generateEnvVars = () => { + const vars = [`OCEAN__INTEGRATION__CONFIG__BASE_URL=${baseUrl}`]; + + // Auth config + vars.push(`OCEAN__INTEGRATION__CONFIG__AUTH_TYPE=${authType}`); + + if (authType === 'bearer_token' && apiToken) { + vars.push(`OCEAN__INTEGRATION__CONFIG__API_TOKEN=${apiToken}`); + } else if (authType === 'api_key' && apiKey) { + vars.push(`OCEAN__INTEGRATION__CONFIG__API_KEY=${apiKey}`); + vars.push(`OCEAN__INTEGRATION__CONFIG__API_KEY_HEADER=${apiKeyHeader}`); + } else if (authType === 'basic' && username && password) { + vars.push(`OCEAN__INTEGRATION__CONFIG__USERNAME=${username}`); + vars.push(`OCEAN__INTEGRATION__CONFIG__PASSWORD=${password}`); + } + + // Pagination config + if (paginationType !== 'none') { + vars.push(`OCEAN__INTEGRATION__CONFIG__PAGINATION_TYPE=${paginationType}`); + vars.push(`OCEAN__INTEGRATION__CONFIG__PAGE_SIZE=${pageSize}`); + } + + return vars.join('\n'); + }; + + const copyToClipboard = () => { + navigator.clipboard.writeText(generateEnvVars()); + }; + + return ( +
+
+

๐ŸŒ API Base URL

+ setBaseUrl(e.target.value)} + className={styles.input} + /> + +

๐Ÿ” Authentication

+
+ + + + +
+ + {authType === 'bearer_token' && ( + setApiToken(e.target.value)} + className={styles.input} + /> + )} + + {authType === 'api_key' && ( + <> + setApiKey(e.target.value)} + className={styles.input} + /> + setApiKeyHeader(e.target.value)} + className={styles.input} + /> + + )} + + {authType === 'basic' && ( + <> + setUsername(e.target.value)} + className={styles.input} + /> + setPassword(e.target.value)} + className={styles.input} + /> + + )} + +

๐Ÿ“„ Pagination

+
+ + + + +
+ + {paginationType !== 'none' && ( + setPageSize(e.target.value)} + className={styles.input} + /> + )} +
+ +
+
+

โœจ Generated Configuration

+ +
+
+          {generateEnvVars() || '# Fill in the form to generate configuration'}
+        
+
+
+ ); +} + + diff --git a/src/components/GenericHttp/BlueprintGenerator.jsx b/src/components/GenericHttp/BlueprintGenerator.jsx new file mode 100644 index 0000000000..7a6570cf6b --- /dev/null +++ b/src/components/GenericHttp/BlueprintGenerator.jsx @@ -0,0 +1,158 @@ +import React, { useState } from 'react'; +import styles from './styles.module.css'; + +export function BlueprintGenerator() { + const [sampleData, setSampleData] = useState(''); + const [blueprintId, setBlueprintId] = useState(''); + const [blueprintTitle, setBlueprintTitle] = useState(''); + const [selectedFields, setSelectedFields] = useState({}); + + const parseFields = (jsonString) => { + try { + const json = JSON.parse(jsonString); + // Get first object if array + const sample = Array.isArray(json) ? json[0] : json; + + if (!sample || typeof sample !== 'object') { + return []; + } + + return Object.entries(sample).map(([key, value]) => { + let type = 'string'; + let format = null; + + if (typeof value === 'number') { + type = 'number'; + } else if (typeof value === 'boolean') { + type = 'boolean'; + } else if (typeof value === 'string') { + // Try to detect special formats + if (value.includes('@')) { + format = 'email'; + } else if (value.match(/^\d{4}-\d{2}-\d{2}/)) { + format = 'date-time'; + } else if (value.match(/^https?:\/\//)) { + format = 'url'; + } + } + + return { key, type, format, value }; + }); + } catch { + return []; + } + }; + + const fields = parseFields(sampleData); + + const toggleField = (key) => { + setSelectedFields(prev => ({ + ...prev, + [key]: !prev[key] + })); + }; + + const generateBlueprint = () => { + const properties = {}; + + fields.forEach(field => { + if (selectedFields[field.key]) { + properties[field.key] = { + type: field.type, + title: field.key.charAt(0).toUpperCase() + field.key.slice(1).replace(/_/g, ' ') + }; + + if (field.format) { + properties[field.key].format = field.format; + } + } + }); + + return JSON.stringify({ + identifier: blueprintId || 'my_blueprint', + title: blueprintTitle || 'My Blueprint', + icon: 'BlankPage', + schema: { + properties, + required: [] + } + }, null, 2); + }; + + const copyBlueprint = () => { + navigator.clipboard.writeText(generateBlueprint()); + }; + + return ( +
+
+

๐Ÿ“‹ Sample Data

+

Paste a sample object from your API response:

+