Merge branch 'main' into DEVREL-1689

This commit is contained in:
christina-gagnon-sp
2024-07-23 10:36:28 -07:00
committed by GitHub
524 changed files with 25452 additions and 5919 deletions

View File

@@ -79,7 +79,7 @@ jobs:
run: |
echo "CMS_APP_API_ENDPOINT=$API_GATEWAY_URL" >> .env
echo ${{ secrets.NPM_FONTAWESOME_CONFIG }} | base64 -d >> .npmrc
export NODE_OPTIONS="--max_old_space_size=4096"
export NODE_OPTIONS="--max_old_space_size=8192"
npm ci --legacy-peer-deps
npm run gen-api-docs-all
npm run build

View File

@@ -68,8 +68,8 @@ To authenticate to the ISC APIs, you must be able to connect to your tenant to s
Your tenant's OAuth details refer to the details you need to know to connect it to the APIs. You need to know your tenant's name, its `authorizeEndpoint` URL, and its `tokenEndpoint` URL.
Your ISC instance is likely using the domain name supplied by SailPoint (`{tenant}.api.identitynow.com`), in which case, the tenant name is in the URL. This is assumed to be the case in this guide.
However, if your ISC instance is using a vanity URL, you must enter this URL into your browser to get your OAuth info: `https://{tenant}.api.identitynow.com/oauth/info`
Your ISC instance is likely using the domain name supplied by SailPoint (`[tenant].api.identitynow.com`), in which case, the tenant name is in the URL. This is assumed to be the case in this guide.
However, if your ISC instance is using a vanity URL, you must enter this URL into your browser to get your OAuth info: `https://[tenant].api.identitynow.com/oauth/info`
If you have admin access but don't know your tenant name, you can learn it by following these steps:
@@ -100,7 +100,7 @@ A personal access token (PAT) is a method of authenticating to an API as a user
Any ISC user can generate a PAT. To do so, follow these steps:
1. Select **Preferences** from the drop-down menu under your username, then **Personal Access Tokens** on the left. You can also go directly to the page by using this URL (replace `{tenant}` with your Identity Security Cloud tenant): `https://{tenant}.identitynow.com/ui/d/user-preferences/personal-access-tokens`
1. Select **Preferences** from the drop-down menu under your username, then **Personal Access Tokens** on the left. You can also go directly to the page by using this URL (replace `[tenant]` with your Identity Security Cloud tenant): `https://[tenant].identitynow.com/ui/d/user-preferences/personal-access-tokens`
2. Click **New Token** and enter a meaningful description to help differentiate the token from others.
@@ -130,7 +130,7 @@ There are several different authorization flows that OAuth 2.0 supports, and eac
1. [**Client Credentials**](https://oauth.net/2/grant-types/client-credentials/) - Clients use this grant type to obtain a JWT `access_token` without user involvement such as scripts, programs or system to system integration.
2. [**Authorization Code**](https://oauth.net/2/grant-types/authorization-code/) - Clients use this grant type to exchange an authorization code for an `access_token`. Authorization codes are mainly used by web applications because there is a login into ISC with a subsequent redirect back to the web application/client.
3. [**Refresh Token**](https://oauth.net/2/grant-types/refresh-token/) - Clients use this grant type to exchange a refresh token for a new `access_token` when the existing `access_token` has expired. This allows clients to continue using the APIs without having to re-authenticate as frequently. This grant type is commonly used together with `Authorization Code` to prevent a user from having to log in several times per day.
3. [**Refresh Token**](https://oauth.net/2/grant-types/refresh-token/) - Clients use this grant type to exchange a refresh token for a new `access_token` when the existing `access_token` has expired. This allows clients to continue using the APIs without having to re-authenticate as frequently. This grant type can only be used together with `Authorization Code` to prevent a user from having to log in several times per day.
One way to determine which authorization flow you need to use is to look at the specification for the endpoint you want to use. The endpoint will have the supported OAuth flows listed under the 'Authorization' dropdown, like the [List Access Profiles endpoint](https://developer.sailpoint.com/docs/api/beta/list-access-profiles):
@@ -170,7 +170,7 @@ This is the overall authorization flow:
1. The client first submits an OAuth 2.0 token request to ISC in this form:
```text
POST https://{tenant}.api.identitynow.com/oauth/token
POST https://[tenant].api.identitynow.com/oauth/token
```
The request includes the client credential information passed in the request body, as shown in this example using [Postman](https://www.getpostman.com):
@@ -182,22 +182,22 @@ This example shows how to pass the information with form-data in the request bod
- Use x-www-form-urlencoded data to pass in the client credential information in the request body.
- Use query parameters to pass the information in the request URL. The request URL will look like this:
```text
https://{tenant}.api.identitynow.com/oauth/token?grant_type=client_credentials&client_id={{clientId}}&client_secret={{clientSecret}}
https://[tenant].api.identitynow.com/oauth/token?grant_type=client_credentials&client_id={{clientId}}&client_secret={{clientSecret}}
```
- If you are using Postman, you can use the 'Authorization' tab to pass in the client credentials. If you use this option, you must also specify the access token URL: https://{tenant}.api.identitynow.com/oauth/token
- If you are using Postman, you can use the 'Authorization' tab to pass in the client credentials. If you use this option, you must also specify the access token URL: https://[tenant].api.identitynow.com/oauth/token
The OAuth 2.0 token request must include this information:
| Key | Description |
| --- | --- |
| `grant_type` | This is set to `CLIENT_CREDENTIALS` for the authorization code grant type. |
| `client_id` | This is the API client's ID (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). You can generate this ID at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`, or you can generate it when you create a PAT. |
| `client_secret` | This is the API client's secret describing (e.g. `c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5`). You can generate this secret at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`, or you can generate it when you create a PAT. |
| `client_id` | This is the API client's ID (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). You can generate this ID at `https://[tenant].identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`, or you can generate it when you create a PAT. |
| `client_secret` | This is the API client's secret describing (e.g. `c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5`). You can generate this secret at `https://[tenant].identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`, or you can generate it when you create a PAT. |
This example cURL command passes client credentials in the body as form-data to generate an access token:
```bash
curl --location 'https://{tenant}.api.identitynow.com/oauth/token' \
curl --location 'https://[tenant].api.identitynow.com/oauth/token' \
--header 'scope: sp:scope:all' \
--form 'grant_type="client_credentials"' \
--form 'client_id="{clientId}"' \
@@ -251,11 +251,11 @@ sequenceDiagram
participant I as Identity Security Cloud
U->>W: Click login link
W->>I: Authorization request to https://{tenant}.login.sailpoint.com/oauth/authorize
W->>I: Authorization request to https://[tenant].login.sailpoint.com/oauth/authorize
I->>U: Redirect to login prompt
U->>I: Authentication
I->>W: Authorization code granted
W->>I: Authorization code to https://{tenant}.api.identitynow.com/oauth/token
W->>I: Authorization code to https://[tenant].api.identitynow.com/oauth/token
I->>W: JWT access token granted
```
@@ -268,7 +268,7 @@ This is the overall authorization flow:
2. The web app sends an authorization request to ISC in this form:
```Text
GET https://{tenant}.login.sailpoint.com/oauth/authorize?client_id={client-id}&response_type=code&redirect_uri={redirect-url}
GET https://[tenant].login.sailpoint.com/oauth/authorize?client_id={client-id}&response_type=code&redirect_uri={redirect-url}
```
3. ISC redirects the user to a login prompt to authenticate to Identity Security Cloud.
@@ -280,12 +280,12 @@ GET https://{tenant}.login.sailpoint.com/oauth/authorize?client_id={client-id}&r
6. The web app submits an OAuth 2.0 token request to ISC in this form:
```text
POST https://{tenant}.api.identitynow.com/oauth/token?grant_type=authorization_code&client_id={client-id}&code={code}&redirect_uri={redirect-url}
POST https://[tenant].api.identitynow.com/oauth/token?grant_type=authorization_code&client_id={client-id}&code={code}&redirect_uri={redirect-url}
```
:::info
The token endpoint URL is `{tenant}.api.identitynow.com`, and the authorize URL is `{tenant}.login.sailpoint.com`. Please be sure to use the correct URL when you're setting up your webapp to use this flow. You can read more about [finding your tenant OAuth details here](https://developer.sailpoint.com/docs/api/authentication/#find-your-tenants-oauth-details).
The token endpoint URL is `[tenant].api.identitynow.com`, and the authorize URL is `[tenant].login.sailpoint.com`. Please be sure to use the correct URL when you're setting up your webapp to use this flow. You can read more about [finding your tenant OAuth details here](https://developer.sailpoint.com/docs/api/authentication/#find-your-tenants-oauth-details).
:::
@@ -296,7 +296,7 @@ These are the query parameters in the OAuth 2.0 token request for the authorizat
| Key | Description |
| --- | --- |
| `grant_type` | Set this to `authorization_code` for the authorization code grant type. |
| `client_id` | This is the client ID for the API client (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel` |
| `client_id` | This is the client ID for the API client (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). This can be generated at `https://[tenant].identitynow.com/ui/admin/#admin:global:security:apimanagementpanel` |
| `code` | This is a code returned by `/oauth/authorize`. |
| `redirect_uri` | This is the application URL to redirect to once the token has been granted. |
@@ -317,7 +317,7 @@ For more information about the OAuth authorization code grant flow, refer [here]
Clients use this grant type in order to exchange a refresh token for a new `access_token` once the existing `access_token` has expired. This allows clients to continue to have a valid `access_token` without the need for the user to login as frequently.
The OAuth 2.0 client you are using must have `REFRESH_TOKEN` as one of its grant types, and you would typically use this type in conjunction with another grant type, like `CLIENT_CREDENTIALS` or `AUTHORIZATION_CODE`:
The OAuth 2.0 client you are using must have `REFRESH_TOKEN` and `AUTHORIZATION_CODE` as its grant types:
```json
{
@@ -337,12 +337,12 @@ The OAuth 2.0 client you are using must have `REFRESH_TOKEN` as one of its grant
This is the overall authorization flow:
1. The client application receives an `access_token` and a `refresh_token` from one of the other OAuth grant flows, like `AUTHORIZATION_CODE`.
1. The client application receives an `access_token` and a `refresh_token` when using the `AUTHORIZATION_CODE` grant flow.
2. The client application detects that the `access_token` is about to expire, based on the `expires_in` attribute contained within the JWT token.
3. The client submits an OAuth 2.0 token request to ISC in this form:
```text
POST https://{tenant}.api.identitynow.com/oauth/token?grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token={refresh_token}
POST https://[tenant].api.identitynow.com/oauth/token?grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token={refresh_token}
```
4. ISC validates the token request and submits a response. If the request is successful, the response contains a new `access_token` and `refresh_token`.
@@ -352,8 +352,8 @@ These are the query parameters in the OAuth 2.0 token request for the refresh to
| Key | Description |
| --- | --- |
| `grant_type` | Set to `refresh_token` for the authorization code grant type. |
| `client_id` | This is the client ID for the API client (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`. |
| `client_secret` | This is the client secret for the API client (e.g. `c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`. |
| `client_id` | This is the client ID for the API client (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). This can be generated at `https://[tenant].identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`. |
| `client_secret` | This is the client secret for the API client (e.g. `c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5`). This can be generated at `https://[tenant].identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`. |
| `refresh_token` | This is the `refresh_token` that was provided along with the now expired `access_token`. |
Here is an example call OAuth 2.0 Token Request for the Refresh Token grant.
@@ -370,7 +370,7 @@ For more information about the OAuth refresh token grant flow, refer [here](http
### OAuth token response
A successful request using any of the grant flows to `https://{tenant}.api.identitynow.com/oauth/token` will contain a response body like this:
A successful request using any of the grant flows to `https://[tenant].api.identitynow.com/oauth/token` will contain a response body like this:
```json
{
@@ -397,7 +397,7 @@ You can use the JWT `access_token` to authorize REST API calls through the ISC A
```bash
curl -X GET \
'https://{tenant}.api.identitynow.com/v3/account-activities' \
'https://[tenant].api.identitynow.com/v3/account-activities' \
-H 'Authorization: Bearer {access_token}' \
-H 'cache-control: no-cache'
```
@@ -521,13 +521,13 @@ Having issues? Follow these steps:
### Verify API endpoint calls
1. Verify the structure of the API call:
2. Verify that the API calls are going through the API gateway: `https://{tenant}.api.identitynow.com`
2. Verify that the API calls are going through the API gateway: `https://[tenant].api.identitynow.com`
3. Verify you are calling their version correctly:
- Private APIs: `https://{tenant}.api.identitynow.com/cc/api/{endpoint}`
- V2 APIs: `https://{tenant}.api.identitynow.com/v2/{endpoint}`
- V3 APIs: `https://{tenant}.api.identitynow.com/v3/{endpoint}`
- Beta APIs: `https://{tenant}.api.identitynow.com/beta/{endpoint}`
- Private APIs: `https://[tenant].api.identitynow.com/cc/api/{endpoint}`
- V2 APIs: `https://[tenant].api.identitynow.com/v2/{endpoint}`
- V3 APIs: `https://[tenant].api.identitynow.com/v3/{endpoint}`
- Beta APIs: `https://[tenant].api.identitynow.com/beta/{endpoint}`
4. Verify that the API calls have the correct headers (e.g., `content-type`), query parameters, and body data.
5. If the HTTP response is **401 Unauthorized** , this is an indication either that there is no `Authorization` header or that the `access_token` is invalid. Verify that the API calls are providing the `access_token` in the `Authorization` header correctly (ex. `Authorization: Bearer {access_token}`) and that the `access_token` has not expired.
@@ -559,7 +559,7 @@ or
GET /beta/oauth-clients/
```
You can also view all of the active clients in the UI by going to `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`.
You can also view all of the active clients in the UI by going to `https://[tenant].identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`.
3. Verify that the OAuth 2.0 client grant types match the OAuth 2.0 grant type flow you're trying to use. For example, this client will work with [Authorization Code](#authorization-code-grant-flow) and [Client Credentials](#client-Credentials-grant-flow) grant flows, but not [Refresh Token](#refresh-token-grant-flow) flows:
@@ -583,4 +583,4 @@ You can also view all of the active clients in the UI by going to `https://{tena
### Verify OAuth calls
Verify that the OAuth call flow is going to the right URLs, with the correct query parameters and data values. A common source of errors is using the wrong host for authorization and token API calls. The token endpoint URL is `{tenant}.api.identitynow.com`, while the authorize URL is `{tenant}.identitynow.com`.
Verify that the OAuth call flow is going to the right URLs, with the correct query parameters and data values. A common source of errors is using the wrong host for authorization and token API calls. The token endpoint URL is `[tenant].api.identitynow.com`, while the authorize URL is `[tenant].identitynow.com`.

View File

@@ -43,13 +43,9 @@ sequenceDiagram
When managing a user's access to the API, you must first assign the target user an appropriate [user level](https://documentation.sailpoint.com/saas/help/common/users/user_level_matrix.html). It is important to choose the correct user level as it will place a boundary on which APIs a user can call, which also affects the areas and functions of the UI they have access to. For example, if a user is in charge of creating reports for auditing requirements, consider granting them the "Report Admin" user level.
User levels are typically granted through the UI, [following the procedures from this document](https://documentation.sailpoint.com/saas/help/accounts/identities.html#setting-user-level-permissions).
:::caution
User levels are typically granted through the UI, [following the procedures from this document](https://documentation.sailpoint.com/saas/help/accounts/identities.html#setting-user-level-permissions). You can also set user levels via API using the [auth user update](https://developer.sailpoint.com/docs/api/v3/patch-auth-user) endpoint.
There is an [API that can set an identity's user level](https://developer.sailpoint.com/discuss/t/assign-identitynow-admin-roles-via-api/1874/4), but it is a V1 API with no guaranteed support. Use it at your own risk!
:::
User levels act as the first line of defense by applying a rigid boundary around the APIs that a user can call. The next section introduces scopes, which allow users to apply granular controls on the APIs an access token can call.
@@ -100,7 +96,7 @@ When you create a PAT in the UI, you can apply scopes to the token. More informa
You can [create PATs](https://developer.sailpoint.com/docs/api/v3/create-personal-access-token) programmatically with the API. The request body for the endpoint allows the caller to specify a list of scopes to be applied to the PAT. If the `scope` property is omitted from the request body, then `sp:scopes:all` is granted to the credentials. The following example shows how to generate a PAT with the `idn:access-request:manage` and `idn:nelm:manage` scopes.
POST <https://{tenant}.api.identitynow.com/v3/personal-access-tokens>
POST `https://{tenant}.api.identitynow.com/v3/personal-access-tokens`
Request Body

View File

@@ -69,7 +69,7 @@ There is a rate limit of 100 requests per `access_token` per 10 seconds for V3 A
**Headers**:
- **Retry-After**: {seconds to wait before rate limit resets}
- **Retry-After**: [seconds to wait before rate limit resets]
## API Tools

View File

@@ -179,7 +179,7 @@ This rule searches for profiles based on an attribute that profile has.
| object_type | string **required** | The values must equal 'NeAttribute' |
| condition_object_id | string **required** | this is the id of the attribute you are searching against |
| comparison_operator | string **required** | This is how the comparison is made for the attribute values. <br></br>Available basic operators: <ul><li>== (equals)</li><li>!= (not equal)</li><li>> (greater than)</li><li>< (less than)</li><li>start_with? (starts with)</li><li>end_with? (ends with)</li><li>include? (includes)</li></ul> Available date operators: <ul><li>before (before specific date)</li><li>after (after specific date)</li><li>> (more than X days before/after today)</li><li>< (less than X days before/after today)</li><li>== (equal to X days before/after today)</li></ul> |
| value | string **required** | This is the value used for comparison. <br></br>Value formatting: <ul><li>profile select attribute: ID of profile</li><li>profile search attribute: ID of profile</li><li>user select attribute: ID of user</li><li>user search attribute: ID of user</li><li>date attribute (before, after): correct date format for attribute</li><li>date attribute (>, <, ==): "X before" or "X after" where X is the number of days</li></ul> |
| value | string **required** | This is the value used for comparison. <br></br>Value formatting: <ul><li>profile select attribute: ID of profile</li><li>profile search attribute: ID of profile</li><li>user select attribute: ID of user</li><li>user search attribute: ID of user</li><li>date attribute (before, after): correct date format for attribute</li><li>date attribute (\>, \<, ==): "X before" or "X after" where X is the number of days</li></ul> |
| \_destroy | boolean | Supplying this option with "true" will cause the condition to be destroyed |
Example:

View File

@@ -51,7 +51,7 @@ To send API requests in Postman, you must authenticate to the APIs. To authentic
:::caution
Don't specify your baseUrl in your environment variables. When you fork an API collection, the baseUrl is automatically set as <https://{{tenant}}.api.{{domain}}.com>. Setting your baseURl in your environment variables may interfere with this process.
Don't specify your baseUrl in your environment variables. When you fork an API collection, the baseUrl is automatically set as `https://{{tenant}}.api.{{domain}}.com`. Setting your baseURl in your environment variables may interfere with this process.
:::

View File

@@ -18,4 +18,4 @@ There is a rate limit of 100 requests per `access_token` per 10 seconds for V3 A
**Headers**:
- **Retry-After**: {seconds to wait before rate limit resets}
- **Retry-After**: [seconds to wait before rate limit resets]

View File

@@ -5,9 +5,9 @@ pagination_label: Standard Collection Parameters
sidebar_label: Standard Collection Parameters
sidebar_position: 5
sidebar_class_name: standardCollectionParameters
keywords: ['standard collection parameters']
keywords: ['standard collection parameters','filter','pagination','paginate','sort']
description: ISC API pagination, filtering, and sorting.
tags: ['Standard Collection Parameters']
tags: ['Standard Collection Parameters','Filter','Sort','Pagination']
---
Many endpoints in the Identity Security Cloud API support a generic syntax for paginating, filtering and sorting the results. A collection endpoint has the following characteristics:
@@ -50,7 +50,7 @@ The `searchAfter` capability provides the ability to page on sorted field values
Here is an example of a search API call with `searchAfter` paging. The first query will get the first set of results. The default limit for search is 10,000, which is different from other collection endpoints. For this example, the query is set to page 100 records at a time. Paginating search queries also requires the `sort` property to be set to `id`.
**POST** <https://{tenant}.api.identitynow.com/v3/search?limit=100&count=true>
**POST** `https://{tenant}.api.identitynow.com/v3/search?limit=100&count=true`
```json
{
@@ -64,7 +64,7 @@ Here is an example of a search API call with `searchAfter` paging. The first que
This query will return 100 records. To get the next 100 records, find the last record's `id` and use it in the next query's `searchAfter` property.
**POST** <https://{tenant}.api.identitynow.com/v3/search?limit=100&count=true>
**POST** `https://{tenant}.api.identitynow.com/v3/search?limit=100&count=true`
```json
{
@@ -108,11 +108,11 @@ These filter operators apply directly to fields and their values:
| `ca` | True if the collection-valued field contains all the listed values. | groups ca ("Venezia","Firenze") |
| `co` | True if the value of the field contains the specified value as a substring.(Applicable to string-valued fields only.) | name co "Rajesh" |
| `eq` | True if the value of the field indicated by the first operand is equal to the value specified by the second operand. | identitySummary.id eq "2c9180846e85e4b8016eafeba20c1314" |
| `ge` | True if the value of the field indicated by the first operand is greater or equal to the value specified by the second operand. | daysUntilEscalation ge 7 name ge "Genaro" |
| `gt` | True if the value of the field indicated by the first operand is greater than the value specified by the second operand. | daysUntilEscalation gt 7 name gt "Genaro" created gt 2018-12-18T23:05:55Z |
| `ge` | True if the value of the field indicated by the first operand is greater or equal to the value specified by the second operand. | daysUntilEscalation ge 7<br></br><br></br>name ge "Genaro" |
| `gt` | True if the value of the field indicated by the first operand is greater than the value specified by the second operand. | daysUntilEscalation gt 7<br></br><br></br>name gt "Genaro"<br></br><br></br>created gt 2018-12-18T23:05:55Z |
| `in` | True if the field value is in the list of values. | accountActivityItemId in ("2c9180846b0a0583016b299f210c1314","2c9180846b0a0581016b299e82560c1314") |
| `le` | True if the value of the field indicated by the first operand is less or equal to the value specified by the second operand. | daysUntilEscalation le 7 name le "Genaro" |
| `lt` | True if the value of the field indicated by the first operand is less than the value specified by the second operand. | daysUntilEscalation lt 7 name lt "Genaro" created lt 2018-12-18T23:05:55Z |
| `le` | True if the value of the field indicated by the first operand is less or equal to the value specified by the second operand. | daysUntilEscalation le 7<br></br><br></br>name le "Genaro" |
| `lt` | True if the value of the field indicated by the first operand is less than the value specified by the second operand. | daysUntilEscalation lt 7<br></br><br></br>name lt "Genaro"<br></br><br></br>created lt 2018-12-18T23:05:55Z |
| `ne` | True if the value of the field indicated by the first operand is not equal to the value specified by the second operand. | type ne "ROLE" |
| `pr` | True if the field is present, that is, not null. | pr accountRequestInfo |
| `isnull` | True if the field is null. | lastUsed isnull |

View File

@@ -59,6 +59,8 @@ tags: ['Connectivity', 'Connector Command']
The account create command triggers whenever ISC is told to provision entitlements for an identity on the target source, but no account for the identity on the target source exists yet. For example, if you create an access profile that grants a group on the target source and then add that access profile to a role, any identity matching that roles membership criteria will be granted to the group. ISC determines which identities do not have accounts on the target source and triggers the account create command for each identity. If an identity already has an account, then it invokes the account update command.
To use this command, you must specify this value in the `commands` array: `std:account:create`
## The Provisioning Plan
The account create command accepts a provisioning plan from ISC and creates the corresponding account(s) in the target source. When you configure your source in ISC, you must set up Create Profile to tell ISC how to provision new accounts for your source.

View File

@@ -40,6 +40,8 @@ The account delete command sends one attribute from ISC, the identity to delete.
Enable account delete in ISC through a BeforeProvisioning rule. The connector honors whichever operation the provisioning plan sends. For more information, see the [documentation](https://community.sailpoint.com/t5/Identity Security Cloud-Articles/Identity Security Cloud-Rule-Guide/ta-p/76665) and an [example implementation](https://community.sailpoint.com/t5/Identity Security Cloud-Wiki/Identity Security Cloud-Rule-Guide-Before-Provisioning-Rule/ta-p/77415).
To use this command, you must specify this value in the `commands` array: `std:account:delete`
The following snippet shows an example of account delete command implementation:
[index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts)

View File

@@ -56,6 +56,8 @@ Disabling accounts is generally preferred if the source supports account disabli
> 🚧 It is important to note that although SaaS Connectivity supports the account delete command, ISC never sends the account delete command, only the account disable command. The connectors developer determines the appropriate action for account disable on the source.
To use this command, you must specify this value in the `commands` array: `std:account:disable`
Account disable is similar to implementing the account update command. If you have implemented your source call to modify any of the values on your source, then you can use the same method to implement the command. The following code implements disable:
```javascript

View File

@@ -53,6 +53,8 @@ tags: ['Connectivity', 'Connector Command']
The account discover schema command tells ISC to dynamically create the account schema for the source rather than use the account schema provided by the connector in connector-spec.json. It is often ideal to statically define the account schema because it is generally more performant and easier to develop and reason about the code. However, some sources have schemas that can be different for each customer deployment. It can also be difficult to determine which account attributes to statically expose, which requires the schema to be dynamically generated. SalesForce is an example of a source that can have thousands of account attributes, which makes it impractical to statically define a set of attributes that satisfies all connector users. Although the SalesForce connector defines a standard set of account attributes out of the box, it also allows schema discovery for users looking for more attributes.
To use this command, you must specify this value in the `commands` array: `std:account:discover-schema`
## Implementation
If your connector requires dynamic schema discovery, you must add std:account:discover-schema to the list of commands in connector-spec.json. Because the account schema is dynamic, you do not need to specify an accountSchema or an accountCreateTemplate object in connector-spec.json. Your connector-spec.json file will look similar to this example from the [Airtable connector](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/connector-spec.json).

View File

@@ -52,6 +52,8 @@ tags: ['Connectivity', 'Connector Command']
You typically invoke the `account enable` command during the joiner, mover, leaver (JML) lifecycle. An identitys rejoining the organization or move to a role that grants access to a previously disabled account triggers the account enable command.
To use this command, you must specify this value in the `commands` array: `std:account:enable`
Implementing `account enable` is similar to implementing the `account update` command. If you have implemented your source call to modify any of the values on your source, then you can use the same method to implement the command. The following code implements enable:
```javascript

View File

@@ -49,6 +49,8 @@ tags: ['Connectivity', 'Connector Command']
The account list command aggregates all accounts from the target source into Identity Security Cloud. ISC calls this command during a manual or scheduled account aggregation.
To use this command, you must specify this value in the `commands` array: `std:account:list`
![Account List](./img/account_list_idn.png)
## Implementation

View File

@@ -52,6 +52,8 @@ tags: ['Connectivity', 'Connector Command']
The account read command aggregates a single account from the target source into Identity Security Cloud. ISC can call this command during a “one-off” account refresh, which you can trigger by aggregating an individual account in ISC.
To use this command, you must specify this value in the `commands` array: `std:account:read`
![Account Read](./img/account_read_idn.png)
## Implementation

View File

@@ -52,6 +52,8 @@ tags: ['Connectivity', 'Connector Command']
The account lock and account unlock commands provide ways to temporarily prevent access to an account. ISC only supports the unlock command, so accounts must be locked on the source level, but they can be unlocked through ISC, and ISC can store the account's status.
To use this command, you must specify this value in the `commands` array: `std:account:unlock`
Implementing account unlock is similar to the other commands that update attributes on an account. The following code unlocks an account:
```javascript

View File

@@ -61,6 +61,8 @@ tags: ['Connectivity', 'Connector Command']
The account update command triggers whenever ISC is told to modify an identity's attributes or entitlements on the target source. For example, granting an identity a new entitlement through a role, changing an identitys lifecycle state, or modifying an identity attribute tied to an account attribute all trigger the account update command.
To use this command, you must specify this value in the `commands` array: `std:account:update`
## Input Schema
The payload from ISC contains the ID of the identity to modify, the configuration items the connector needs to call the source API, and one or more change operations to apply to the identity. Each operation has the following special considerations:

View File

@@ -37,6 +37,8 @@ tags: ['Connectivity', 'Connector Command']
The change password command is triggered in ISC when a user changes their password through ISC. When this occurs, if your source has change password enabled, then you can change the user password on the source system through ISC.
To use this command, you must specify this value in the `commands` array: `std:change-password`
## The Provisioning Plan
The change password command sends the password change event to your connector whenever a user changes their password through the Password Manager. Handling this even is as simple as implementing a method on the source system that updates a users password

View File

@@ -44,6 +44,8 @@ tags: ['Connectivity', 'Connector Command']
The entitlement list command triggers during a manual or scheduled entitlement aggregation operation within ISC. This operation gathers a list of all entitlements available on the target source, usually multi-valued entitlements like groups or roles. This operation provides ISC administrators with a list of entitlements available on the source so they can create access profiles and roles accordingly, and it provides ISC with more details about the entitlements. The entitlement schemas minimum requirements are name and ID, but you can add other values, such as created date, updated date, status, etc.
To use this command, you must specify this value in the `commands` array: `std:entitlement:list`
![Discover Schema 4](./img/entitlement_list_idn.png)
## Defining the Schema

View File

@@ -15,6 +15,8 @@ At this time Entitlement Read is not triggered from ISC for any specific workflo
:::
To use this command, you must specify this value in the `commands` array: `std:entitlement:list`
| Input/Output | Data Type |
| :----------- | :----------------------: |
| Input | StdEntitlementReadInput |

View File

@@ -48,6 +48,8 @@ Use the source data discover command to identify the types of data your source c
One typical use for the source data discover command is found in Identity Security Cloud customer forms for dropdown menus: they use the command to identify the additional source types their sources can provide to Identity Security Cloud and use that information to populate the dropdown menus.
To use this command, you must specify this value in the `commands` array: `std:source-data:discover`
This is a simple example of the source data discover command. It has been implemented to list two types of queries that the Airtable source can supply.
```javascript

View File

@@ -47,6 +47,8 @@ tags: ['Connectivity', 'Connector Command']
Use the source data read command to query a source in Identity Security Cloud and return a set of data. This data is typically used to populate a dropdown menu for selection purposes. This functionality is typically useful for Identity Security Cloud forms, but it can be used for any type of implementation that requires you to get other information from a source, information that is not normally retrieved from identites or entitlements.
To use this command, you must specify this value in the `commands` array: `std:source-data:read`
This is a simple example of the source data read command. It is implemented to retrieve the base ID name. The `sourceDataKey` is required, the `source data read` command should return it.
```javascript

View File

@@ -25,6 +25,8 @@ tags: ['Connectivity', 'Connector Command']
The test connection command ensures the connector can communicate with the target web service. It validates API credentials, host names, ports, and other configuration items. To implement this command, look for either a health endpoint or a simple GET endpoint. Some web services implement a health endpoint that returns status information about the service, which can be useful to test a connection. If no health endpoint exists, use a simple GET endpoint that takes few to no parameters to ensure the connector can make a successful call to the web service.
To use this command, you must specify this value in the `commands` array: `std:test-connection`
Use Test Connection in the ISC UI after an admin has finished entering configuration information for a new instance of the connector.
![Test Connection](./img/test_command_idn.png)

View File

@@ -13,7 +13,10 @@ tags: ['SaaS Configuration']
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
This is a guide about using the SailPoint SaaS Configuration APIs to import configurations into and export configurations from the SailPoint SaaS system. Use these APIs to get configurations in bulk in support of environmental promotion, go-live, or tenant-to-tenant configuration management processes and pipelines.
You can use the SailPoint SaaS Configuration APIs to export snapshots of your tenant configurations and import them to restore those configurations in other SaaS systems, such as new or existing tenants.
Use these APIs to get configurations in bulk in support of environmental promotion, go-live, or tenant-to-tenant configuration management processes and pipelines.
Read this document to learn about the different SaaS Configuration APIs and what you can do with them.
For more details around how to manage configurations, refer to [SailPoint SaaS Change Management and Deployment Best Practices](https://community.sailpoint.com/t5/IdentityNow-Articles/SailPoint-SaaS-Change-Management-and-Deployment-Best-Practices/ta-p/189871).
@@ -23,6 +26,16 @@ This document is intended for technically proficient administrators, implementer
## Supported Objects
A SailPoint tenant configuration comprises various objects and their details, such as an organization's different identity profiles, roles, certification campaigns, and more.
You can use the SaaS Configuration APIs to exclude objects from the imports and exports.
This table lists the objects supported for import and export with the SaaS Configuration APIs:
:::note
This table also lists the objects supported for backup and deploy, the terms used in Configuration Hub for export and import functionality.
To learn more about Configuration Hub, refer to [Configuration Hub](https://documentation.sailpoint.com/saas/help/confighub/config_hub.html).
:::
| **Object** | **Object Type** | **Export** | **Import** | **Backup** | **Deploy** |
| :-- | :-- | :-- | :-- | :-- | :-- |
| Access Profiles | `ACCESS_PROFILE` | ✅ | ❌ | ✅ | ✅ |

View File

@@ -77,6 +77,11 @@ This is an example input from this trigger:
}
}
```
:::info
`clientMetadata` is determined by the user that invoked `create-access-request` and can contain any value at runtime that was specified in the access request.
:::
## Additional Information and Links

View File

@@ -30,6 +30,8 @@ You can use this trigger to develop logic outside of Identity Security Clouds
- A security officer
- A high-risk governance group for highly sensitive roles
If there is an active subscription to the [Access Request Submitted trigger](./access-request-submitted.md), this trigger is invoked **after** a response is submitted to the Access Request Submitted trigger, and only if that response is to approve the access request.
## Configuration
This is a `REQUEST_RESPONSE` trigger type. For more information about how to respond to a `REQUEST_RESPONSE` type trigger, see [responding to a request response type trigger](../responding-to-a-request-response-trigger.mdx) . This trigger intercepts newly submitted access requests and allows the subscribing service to add one additional identity or governance group as the last step in the approver list for the access request.

View File

@@ -21,7 +21,7 @@ tags: ['Event Triggers', 'Available Event Triggers', 'Request Response']
## Event Context
The platform now includes event triggers within the access request approval workflow. The 'Access Request Submitted' event trigger provides more proactive governance, ensures users can quickly obtain needed access, and helps with more preventative measures towards unintended access.
The platform now includes event triggers within the access request approval workflow. The 'Access Request Submitted' event trigger provides more proactive governance, ensures users can quickly obtain needed access, and helps with more preventative measures towards unintended access. When an access request is submitted, this trigger is invoked before the [Access Request Dynamic Approval trigger](./access-request-dynamic-approval.md).
![Flow](./img/access-request-preapproval-path.png)
@@ -96,10 +96,20 @@ To deny an access request, the subscribing service responds to the event trigger
}
```
This event trigger interrupts the normal workflow for access requests. Access requests can only proceed if the subscribing service responds within the alotted time by approving the request. If the subscribing service is non-responsive or it is responding with an incorrect payload, access requests will fail after the **Separation of Duties** check. If you see numerous access requests failing at this stage, verify that your subscribing service itself is operating correctly.
:::warning
The `approver` does not have to be the name of an existing identity in your ISC tenant. It can be anything you want it to be. However, if you have an active subscription to the [Access Request Decision](./access-request-decision.md) trigger, you **MUST** provide the **username** of an existing identity in your tenant in the `approver` field. If you do not provide the **username** of an existing identity, then your Access Request Decision subscriptions will never be triggered.
:::
:::warning
This event trigger interrupts the normal workflow for access requests. Access requests can only proceed if the subscribing service responds within the alotted time by approving the request. If the subscribing service is non-responsive or it is responding with an incorrect payload, access requests will fail after the **Separation of Duties** check.
![AR failed](./img/access-request-preapproval-failure.png)
If you see numerous access requests failing at this stage, verify that your subscribing service is operating correctly.
:::
## Additional Information and Links
- **Trigger Type**: [REQUEST_RESPONSE](../trigger-types.md#request-response)

View File

@@ -20,7 +20,6 @@ The platform has introduced an event trigger within the Source Aggregation workf
After the initial collection of accounts in the source system during aggregation completes, some uses cases for this trigger include the following:
- Notify an administrator that Identity Security Cloud was able to successfully connect to the source system and collect source accounts.
- Notify an administrator when the aggregation is terminated manually during the account collection phase.
- Notify an administrator or system (e.g. PagerDuty) that Identity Security Cloud failed to collect accounts during aggregation and indicate required remediation for the source system.
:::info
@@ -69,13 +68,10 @@ In this example, there are 10 changed accounts (`scanned` (200) - `unchanged` -
> This event trigger fires even without changed accounts. The unchanged count will match the scanned accounts in the response.
The status of the aggregation can be one of three possible values:
The status of the aggregation can be one of two possible values:
- **Success**: Account collection was successful and aggregation can move to the next step.
- **Error**: There is a failure in account collection or an issue connecting to the source. The `errors` vary by source.
- **Termination**: The aggregation was terminated during the account collection phase. Aggregation can be terminated when the account deletion threshold is exceeded. For example, an account delete threshold of 10% is set by default for the source, and if the number of `removed` accounts for the above example is 21 (more than 10% of `scanned` accounts (200)), the aggregation is cancelled.
![Account_Delete_Threshold](./img/aggregation-delete-threshold.png)
## Additional Information and Links

View File

@@ -18,6 +18,13 @@ Identity Attribute Changed events occur when any attributes aggegrated from an a
This event trigger provides a flexible way to extend Joiner-Mover-Leaver processes. This provides more proactive governance and ensures users can quickly get necessary access when they enter your organization.
:::info
This event trigger doesn't detect an identity's change in lifecycle state from 'null' to 'active', so it's recommended that you set an identity's lifecycle state when it's created. You can then use the [Identity Created](./identity-created.md) trigger to detect that change to 'active' for Joiners.
:::
Some uses cases for this trigger include the following:
- Notify an administrator or system to take the appropriate provisioning actions as part of the Mover workflow.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -61,8 +61,8 @@ Operators provide more options to filter JSON structures.
| != | **Not equal to** - Evaluates to `true` if operands do not match. | $[?($.identity.name != "george.washington")] |
| > | **Greater than** - Evaluates to `true` if the left operand is greater than the right operand. It works on strings and numbers. | $[?($.attributes.created > '2020-04-27T16:48:33.200Z')] |
| >= | **Greater than or equal to** - Evaluates to `true` if the left operand is greater than or equal to the right operand. | $[?($.attributes.created >= '2020-04-27T16:48:33.597Z')] |
| < | **Less than** - Evaluates to `true` if the left operand is less than the right operand. | $[?($.attributes.created < '2020-04-27T16:48:33.200Z')] |
| <= | **Less than or equal to** - Evaluates to `true` if the left operand is less than or equal to the right operand. | $[?($.attributes.created <= '2020-04-27T16:48:33.200Z')] |
| \< | **Less than** - Evaluates to `true` if the left operand is less than the right operand. | $[?($.attributes.created < '2020-04-27T16:48:33.200Z')] |
| \<= | **Less than or equal to** - Evaluates to `true` if the left operand is less than or equal to the right operand. | $[?($.attributes.created \<= '2020-04-27T16:48:33.200Z')] |
| =~ | **Regular expression** - Evaluates to `true` if the left operand matches the regular expression. | $.changes[?(@.attribute == "department" && @.newValue =~ /US.*Support/i)] |
| in | **In** - Evaluates to `true` if the left operand exists in the list of values on the right. | $.changes[?(@.attribute == 'department' && @.newValue in ['sales','engineering'])] |
| nin | **Not in** - Evaluates to `true` if the left operand **does not** exist in the list of values on the right. | $.changes[?(@.attribute == 'department' && @.newValue nin ['sales','engineering'])] |
@@ -79,7 +79,7 @@ Operators provide more options to filter JSON structures.
Developing a filter can be faster when you use a tool like an online [JSONpath editor](https://www.javainuse.com/jsonpath). These tools can provide quick feedback on your filter, allowing you to focus on the exact filter expression you want before testing it on a trigger. Just paste an example of your event trigger input and start crafting an expression to see its result.
:::Warning
:::warning
Third party websites like the one mentioned earlier must be treated with caution. Do not use real data from your tenant when you're interacting with these tools.
:::
@@ -182,7 +182,7 @@ POST https://{tenant}.api.identitynow.com/beta/trigger-subscriptions/validate-fi
If SailPoint accepts your trigger filter, you must test whether it actually works. You must configure your trigger subscription to point to the URL of your testing service. [webhook.site](https://webhook.site) is an easy to use testing service. Just copy the unique URL it generates and paste it into your subscription's integration URL field. The easiest way to test a trigger subscription is to use the UI to fire off a test event.
:::Warning
:::warning
Third party websites like the one mentioned earlier must be treated with caution. Do not use real data from your tenant when you're interacting with these tools.
:::

View File

@@ -5,7 +5,7 @@ pagination_label: Account Profile Attribute Generator (from Template)
sidebar_label: Account Profile Attribute Generator (from Template)
sidebar_class_name: accountProfileAttributeGeneratorTemplate
keywords: ['cloud', 'rules', 'account profile', 'attribute generator']
description: This rule generates complex account attribute values during provisioning, e.g. when creating an account. The rule's configuration comes from a template of values.
description: This rule generates complex account attribute values during provisioning, e.g. when creating an account.
slug: /extensibility/rules/cloud-rules/account-profile-attribute-generator-template
tags: ['Rules']
---

View File

@@ -5,7 +5,7 @@ pagination_label: BuildMap Rule
sidebar_label: BuildMap Rule
sidebar_class_name: buildMapRule
keywords: ['cloud', 'rules']
description: This rule manipulates raw input data provided by the rows and columns in a file and builds a map from the incoming data.
description: This rule manipulates raw input data provided by the rows and columns in a file.
slug: /extensibility/rules/cloud-rules/buildmap-rule
tags: ['Rules']
---

View File

@@ -34,7 +34,7 @@ In this process, SailPoint does _not check_ whether the rule executes correctly
## Submit for Rule Review
To submit your Cloud Rule for review, approval, and inclusion in the SailPoint platform, submit a [SailPoint support portal request](https://support.sailpoint.com/csm) or send an email to <support@sailpoint.com>. Attach the rule, validator output, tenant name (e.g., acme-sb.identitynow.com for sandbox or acme.identitynow.com for production) and approval for expert services to proceed. If you need assistance writing and testing rules, Expert Services can assist in that process as well. Make sure your contact information is up to date so the review team can contact you if they need to.
To submit your Cloud Rule for review, approval, and inclusion in the SailPoint platform, submit a [SailPoint support portal request](https://support.sailpoint.com/csm) or send an email to `support@sailpoint.com`. Attach the rule, validator output, tenant name (e.g., acme-sb.identitynow.com for sandbox or acme.identitynow.com for production) and approval for expert services to proceed. If you need assistance writing and testing rules, Expert Services can assist in that process as well. Make sure your contact information is up to date so the review team can contact you if they need to.
## Review Guidelines

View File

@@ -40,10 +40,10 @@ Connector Rules are directly editable with the [Connector Rule REST APIs](https:
| Name | Path |
| --- | --- |
| [List Connector Rules](/docs/api/beta/get-connector-rule-list) | `GET /beta/connector-rules/` |
| [Get Connector Rule](/docs/api/beta/get-connector-rule) | `GET /beta/connector-rules/{id}` |
| [Get Connector Rule](/docs/api/beta/get-connector-rule) | `GET /beta/connector-rules/[id]` |
| [Create Connector Rule](/docs/api/beta/create-connector-rule) | `POST /beta/connector-rules/` |
| [Update Connector Rule](/docs/api/beta/update-connector-rule) | `PUT /beta/connector-rules/{id}` |
| [Delete Connector Rule](/docs/api/beta/delete-connector-rule) | `DELETE /beta/connector-rules/{id}` |
| [Update Connector Rule](/docs/api/beta/update-connector-rule) | `PUT /beta/connector-rules/[id]` |
| [Delete Connector Rule](/docs/api/beta/delete-connector-rule) | `DELETE /beta/connector-rules/[id]` |
| [Validate Connector Rule](/docs/api/beta/validate-connector-rule) | `POST /beta/connector-rules/validate` |
SailPoint architectural optimizations have added resiliency and protections against malformed or long-running rules. These APIs also offer built-in protection and checking against potentially harmful code. For more information, see [Rule Code Restrictions](../../rules/index.md#rule-code-restrictions).
@@ -99,7 +99,7 @@ For the `PATCH` operations, you must provide an `op` key. For new configurations
### BeforeProvisioning Rule
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -119,7 +119,7 @@ Content-Type: `application/json-patch+json`
### AfterCreate, AfterModify, AfterDelete, BeforeCreate, BeforeModify, BeforeDelete Rules
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -141,7 +141,7 @@ The value key is a list. All available AfterCreate, AfterModify, BeforeCreate, a
### Correlation Rule
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -161,7 +161,7 @@ Content-Type: `application/json-patch+json`
### ManagerCorrelation Rule
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -181,7 +181,7 @@ Content-Type: `application/json-patch+json`
### BuildMap Rule
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -197,7 +197,7 @@ Content-Type: `application/json-patch+json`
### JDBCBuildMap Rule
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -213,7 +213,7 @@ Content-Type: `application/json-patch+json`
### JDBCProvision Rule
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -229,7 +229,7 @@ Content-Type: `application/json-patch+json`
### SAP HR Provisioning Modify Rule
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -245,7 +245,7 @@ Content-Type: `application/json-patch+json`
### WebServiceBeforeOperation Rule
`PATCH` /v3/sources/{id}
`PATCH` /v3/sources/[id]
Content-Type: `application/json-patch+json`
@@ -263,7 +263,7 @@ _Note: Replace `_`with the index location of operation the way it is configured
### WebServiceAfterOperation Rule
`PATCH` /v3/sources/{id} Content-Type: `application/json-patch+json`
`PATCH` /v3/sources/[id] Content-Type: `application/json-patch+json`
_Note: Replace \[\*\] with the index location of the operation the way it is configured on the source. For example, 0, 1, 2, etc. You can use a `GET` call on the source first to verify the index location prior to executing the `PATCH` call to attach the rule._

View File

@@ -26,7 +26,7 @@ This rule calculates attributes after a web-service operation call.
| Argument | Type | Purpose |
| --- | --- | --- |
| application | sailpoint.object.Application | Application whose data file is being processed. |
| processedResponseObject | List<Map<String, Object>> | List of map (account/group). The map contains a key, the identityAttribute of the application schema, and a value, all the account/group attributes (schema) passed by the connector after parsing the respective API response. |
| processedResponseObject | List\<Map\<String, Object\>\> | List of map (account/group). The map contains a key, the identityAttribute of the application schema, and a value, all the account/group attributes (schema) passed by the connector after parsing the respective API response. |
| requestEndPoint | sailpoint.connector.webservices.EndPoint | Current request information. It contains the header, body, context url, method type, response attribute map, successful response code. |
| restClient | sailpoint.connector.webservices.WebServicesClient | WebServicesClient (HttpClient) object that enables the user to call the Web Services API target system. |
| rawResponseObject | String | String object that holds the raw response returned from the target system, which can be in JSON or XML form. |

View File

@@ -483,11 +483,11 @@ ________________________________________________________________________________
## Submit for Rule Review
To submit your Cloud Rule for review, approval, and inclusion in the SailPoint platform, submit a [SailPoint support portal request](https://support.sailpoint.com/csm) or send an email to <support@sailpoint.com>. Attach the rule, validator output, tenant name (e.g., acme-sb.identitynow.com for sandbox or acme.identitynow.com for production) and approval for expert services to proceed. If you need assistance writing and testing rules, Expert Services can assist in that process as well. Make sure your contact information is up to date so the review team can contact you if they need to.
To submit your Cloud Rule for review, approval, and inclusion in the SailPoint platform, submit a [SailPoint support portal request](https://support.sailpoint.com/csm) or send an email to `support@sailpoint.com`. Attach the rule, validator output, tenant name (e.g., acme-sb.identitynow.com for sandbox or acme.identitynow.com for production) and approval for expert services to proceed. If you need assistance writing and testing rules, Expert Services can assist in that process as well. Make sure your contact information is up to date so the review team can contact you if they need to.
## Add Rule To Account Creation
Log into your ISC tenant and navigate to **Admin** -> **Connections** -> **Sources** -> **{Source Name}** -> **Accounts** -> **Create Account**. Scroll to the attribute you wish to use the rule for generating the username. Check the generator radio button and pick your new rule from the drop down.
Log into your ISC tenant and navigate to **Admin** -\> **Connections** -\> **Sources** -\> **[Source Name]** -\> **Accounts** -\> **Create Account**. Scroll to the attribute you wish to use the rule for generating the username. Check the generator radio button and pick your new rule from the drop down.
![Account Create](./img/account-create.png)

View File

@@ -54,7 +54,7 @@ The date compare transform takes as an input the two dates to compare, denoted a
- **secondDate** - This is the second date to consider (i.e., the date that would be on the right hand side of the comparison operation).
- **operator** - This is the comparison to perform. The following values are valid:
- **LT**: Strictly less than: firstDate < secondDate
- **LTE**: Less than or equal to: firstDate <= secondDate
- **LTE**: Less than or equal to: firstDate \<= secondDate
- **GT**: Strictly greater than: firstDate > secondDate
- **GTE**: Greater than or equal to: firstDate >= secondDate
- **positiveCondition** - This is the value to return if the comparison is true.

View File

@@ -57,7 +57,7 @@ The structure of a generate random string transform requires the `name` of the r
- (
- )
- \+
- <
- \<
- \>
- ?
- **length** - This is the required length ofthe randomly generated string.

View File

@@ -40,7 +40,7 @@ The object model for plugins has also changed somewhat. This table maps the old
| certificationLevel | certificationLevel |
| pluginAccessRight | rightRequired |
Gone in 7.1 is the idea of a plugin configuration model, and a snippet model. Instead, these elements have been rolled into the 'PluginAttributes' map that appears in the 'manifest.xml' file required by each plugin. The 'fullPage' object is now a single entry in the attributes mapping, which only holds the title of the 'fullPage'. Snippets move into a 'List' entry key in the attributes map. For each snippet entry in the 'List', implementers can define a regular expression 'regexPattern' to match against, the 'rightRequired' to see the snippet, and then a list of <Scripts/> and <StyleSheets/> that determine the look and action of the snippet.
Gone in 7.1 is the idea of a plugin configuration model, and a snippet model. Instead, these elements have been rolled into the 'PluginAttributes' map that appears in the 'manifest.xml' file required by each plugin. The 'fullPage' object is now a single entry in the attributes mapping, which only holds the title of the 'fullPage'. Snippets move into a 'List' entry key in the attributes map. For each snippet entry in the 'List', implementers can define a regular expression 'regexPattern' to match against, the 'rightRequired' to see the snippet, and then a list of \<Scripts/> and \<StyleSheets/> that determine the look and action of the snippet.
The most readily apparent change in plugin definition going from 7.0 to 7.1 is the location of each setting's plugins. Previously, developers could define a URL to a settings page ('settingsPageTemplateURL') that they could completely customize. In 7.1, in order to support future portability and support, the settings page has been removed, and individual plugin settings have been internalized to the 'manifest.xm'l file. These settings are now defined in a 'Settings' list in the 'PluginAttributes' map. Each element of the list is a 'Setting', which can have the following defined:

View File

@@ -15,7 +15,7 @@ tags: ['plugin', 'guide', 'identityiq']
Most plugins will have some additional UI component that will display in IdentityIQ. You can use images, CSS files, HTML templates, and JavaScript to provide the interactions and views required by the plugin. Plugins using a `fullPage` element will look for a file called 'page.xhtml' in the build.
:::Note
:::info
Any css installed with the plugin will apply to all elements in IdentityIQ. For this reason it is recommended that developers keep their css classes specific to their plugin.

View File

@@ -49,7 +49,7 @@ An annotation should have at least three parts
- **Line 2** - The path or endpoint - this can be parameterized, which is useful for pulling back a single record. The earlier example uses parameterization by adding the variable within {} tags to the end of the URL and also declaring the @PathParam "appName" in the input arguments of the method signature.
- **Line 3** - The authorization of the method. The following values are allowed:
- **@AllowAll** - Allows anyone to interrogate the endpoint.
- **@RequiredRight("<SPRight/>")** - Allows users with the named SPRight to access the endpoint.
- **@RequiredRight("\<SPRight/>")** - Allows users with the named SPRight to access the endpoint.
- **@SystemAdmin** - System administrator access only.
- **@Deferred** - Authorization is deferred to the method. When this option is selected, the implementer must also create an `Authorizer` class that implements the `sailpoint.authorization.Authorize`r interface. The `Authorizer` class should override the `authorize(UserContext)` method of the base `Authorizer` interface. Inside the REST resource method, the author would then call `authorize()`. Here is a simple example:

View File

@@ -15,7 +15,7 @@ tags: ['CLI']
Learn how to use the CLI to search your ISC tenant in this guide.
In Identity Security Cloud (ISC), you can search across all the sources connected to your tenant and return virtually any information you have access to. The `search` command allows you to access ISC search functionality within the CLI. For more information about the `search` command, refer to the CLI [Search guide](docs/tools/cli/search). For more information about search in ISC, refer to [Search](docs/api/v3/search).
In Identity Security Cloud (ISC), you can search across all the sources connected to your tenant and return virtually any information you have access to. The `search` command allows you to access ISC search functionality within the CLI. For more information about the `search` command, refer to the CLI [Search guide](./search.md). For more information about search in ISC, refer to [Search](../../api/v3/search).
In Identity Security Cloud, you can search all the sources connected to your tenant and return virtually any information you have access to. To learn more about search in Identity Security Cloud, refer to [Search](https://documentation.sailpoint.com/saas/help/search/index.html).

View File

@@ -100,7 +100,7 @@ When you launch the Community Toolbox, it prompts you to provide your tenant inf
You must provide this tenant information to be able to log in:
- **Tenant**: This is your organization's tenant name.
- **Domain**: This is usually 'identitynow'.
- **Base URL**: This is the API URL - it is 'https://{tenant}.{domain}.com', like 'https:acme.identitynow.com', for example.
- **Base URL**: This is the API URL - it is 'https://[tenant].[domain].com', like 'https:acme.identitynow.com', for example.
- **Tenant URL**: This is the tenant URL - it is often the same as the API URL.
Once you have provided your tenant information, the Community Toolbox prompts you to log in to your tenant the same way you would from the browser. You must provide a password and possibly some additional form of authentication.

View File

@@ -68,7 +68,7 @@ src/
- **`src/test/java/sailpoint/`** Use this folder for test classes to test your rules.
## Install depedencies
## Install dependencies
Install all the required dependencies by running Maven install in the root of the project:

View File

@@ -15,9 +15,9 @@ Once your SDK is installed and configured, you can start accessing the SDK's dif
## List Transforms
One of the most useful functionalities of the Python SDK is the ability to easily access all the [V3 APIs](/idn/api/v3) and [Beta APIs](/idn/api/beta) and implement them in your project.
One of the most useful functionalities of the Python SDK is the ability to easily access all the [V3 APIs](/docs/api/v3) and [Beta APIs](/docs/api/beta) and implement them in your project.
Here is an example of how to use the SDK to get a list of available [transforms](/idn/docs/transforms). This example leverages the [List Transforms endpoint](/idn/api/v3/list-transforms).
Here is an example of how to use the SDK to get a list of available [transforms](/docs/extensibility/transforms). This example leverages the [List Transforms endpoint](/docs/api/v3/list-transforms).
Create a file in your project called "sdk.py" and copy this content into it:
@@ -57,7 +57,7 @@ You can use this example as a guide for how to access all the V3 and Beta APIs (
With the same SDK function, you can use query parameters to limit the results of your transforms list to only the results you want.
Refer to the [List Transforms endpoint specification](/idn/api/v3/list-transforms) to view all its query parameters.
Refer to the [List Transforms endpoint specification](/docs/api/v3/list-transforms) to view all its query parameters.
Here is an example that uses query parameters to limit the list to no more than 10 transforms that all start with the name "Test":

View File

@@ -19,7 +19,7 @@ You need the following to use the Python SDK:
- Python version 3.7 or above. You can download it [here](https://www.python.org/downloads/). You can use `python --version` to check your version.
- Your tenant name in Identity Security Cloud. To learn how to find it, refer to [Getting Started](/idn/api/getting-started#find-your-tenant-name). The SDK will use this tenant name to connect to your Identity Security Cloud instance.
- Your tenant name in Identity Security Cloud. To learn how to find it, refer to [Getting Started](/docs/api/getting-started.md#find-your-tenant-name). The SDK will use this tenant name to connect to your Identity Security Cloud instance.
- A PAT with a client secret and ID. To learn how to create one in Identity Security Cloud, refer to [Personal Access Tokens](https://documentation.sailpoint.com/saas/help/common/api_keys.html#generating-a-personal-access-token). The SDK will use this PAT to authenticate with the SailPoint APIs.

View File

@@ -11,7 +11,7 @@ slug: /tools/sdk/python/paginate
tags: ['SDK']
---
By default, your requests will return a maximum of 250 records. To return more, you must implement pagination. To learn more about pagination, refer to [Paginating Results](/idn/api/standard-collection-parameters/#paginating-results).
By default, your requests will return a maximum of 250 records. To return more, you must implement pagination. To learn more about pagination, refer to [Paginating Results](/docs/api/standard-collection-parameters/#paginating-results).
Here is an example of how to implement pagination with the SDK on line 10:
@@ -44,4 +44,4 @@ The `result_limit` specifies the total number of results you can return, 1000. T
You can also provide an `offset` value to specify which record number to start the request on. For example, you can add `offset=11` to start getting accounts from the 12th record, 11, instead of the first, 0.
To find out whether an endpoint supports pagination, refer to its documentation. Any API supporting pagination lists the optional query parameters detailed in [Paginating Results](/idn/api/standard-collection-parameters/#paginating-results).
To find out whether an endpoint supports pagination, refer to its documentation. Any API supporting pagination lists the optional query parameters detailed in [Paginating Results](/docs/api/standard-collection-parameters/#paginating-results).

View File

@@ -11,7 +11,7 @@ slug: /tools/sdk/python/search
tags: ['SDK']
---
One of the most useful functionalities you can access with the Python SDK is Identity Security Cloud's [search functionality](/idn/api/v3/search-post).
One of the most useful functionalities you can access with the Python SDK is Identity Security Cloud's [search functionality](/docs/api/v3/search-post).
Here is an example of how you can implement Search, along with pagination. Copy this code into your "sdk.py" file to try it out:
@@ -46,9 +46,9 @@ This example returns 1000 identities, 100 per page, and sorts them in descending
There are two main ways you can manipulate this example to search for the results you want:
The first way is to change the `indices`, the document types you want to limit your search to. For example, if you add `"access profiles"` to the indices, the SDK will search access profiles too. To see all the indices you can search, refer to the [Search endpoint specification](/idn/api/v3/search-post).
The first way is to change the `indices`, the document types you want to limit your search to. For example, if you add `"access profiles"` to the indices, the SDK will search access profiles too. To see all the indices you can search, refer to the [Search endpoint specification](/docs/api/v3/search-post).
The second way is to change the `query`, the value you're searching for. For example, if you change the query to "a*", the search will return all records starting with the letter "a". To learn more about how to build search queries, refer to [Building a Search Query](https://documentation.sailpoint.com/saas/help/search/building-query.html).
You can also change the sorting logic in the brackets next to `sort`. For more information about sorting results, refer to [Sorting Results](/idn/api/standard-collection-parameters/#sorting-results).
You can also change the sorting logic in the brackets next to `sort`. For more information about sorting results, refer to [Sorting Results](/docs/api/standard-collection-parameters/#sorting-results).

View File

@@ -1,8 +1,9 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
const {themes} = require('prism-react-renderer');
const lightCodeTheme = themes.github;
const darkCodeTheme = themes.dracula;
const footer = require('./footer');
const navbar = require('./navbar');
@@ -37,7 +38,6 @@ const config = {
showLastUpdateTime: true,
sidebarCollapsible: true,
sidebarPath: require.resolve('./sidebars.js'),
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/ApiItem', // Derived from docusaurus-theme-openapi
},
theme: {
@@ -98,6 +98,7 @@ const config = {
'bash',
'go',
'python',
'json'
],
},
mermaid: {

9
output.txt Normal file
View File

@@ -0,0 +1,9 @@
> sailpoint-developer-portal@0.0.0 start
> docusaurus start --port=4200
------------------------------------------------------------------------------ Update available 3.0.1 → 3.4.0 To upgrade Docusaurus packages with the latest version, run the following command: `npm i @docusaurus/plugin-client-redirects@latest @docusaurus/plugin-content-docs@latest @docusaurus/plugin-google-tag-manager@latest @docusaurus/theme-mermaid@latest @docusaurus/core@latest @docusaurus/types@latest @docusaurus/module-type-aliases@latest @docusaurus/preset-classic@latest` ------------------------------------------------------------------------------
[INFO] Starting the development server...
[ERROR] Something is already running on port 4200.

15291
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,38 +21,40 @@
"rebuild-docs": "npm run clean-api-docs-all && npm run gen-api-docs-all"
},
"dependencies": {
"@docusaurus/plugin-client-redirects": "2.4.3",
"@docusaurus/plugin-content-docs": "^2.4.3",
"@docusaurus/plugin-google-tag-manager": "^2.4.3",
"@docusaurus/theme-mermaid": "2.4.3",
"@docusaurus/plugin-client-redirects": "3.4.0",
"@docusaurus/plugin-google-tag-manager": "3.4.0",
"@docusaurus/theme-mermaid": "3.4.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mdx-js/react": "^1.6.22",
"@gracefullight/docusaurus-plugin-microsoft-clarity": "^1.0.0",
"@mdx-js/react": "^3.0.0",
"@typeform/embed-react": "^1.21.0",
"autoprefixer": "^10.4.13",
"classnames": "^2.3.2",
"clsx": "^1.1.1",
"docusaurus-plugin-openapi-docs": "^2.0.2",
"docusaurus-theme-openapi-docs": "^2.0.2",
"clsx": "^2.0.0",
"docusaurus-plugin-openapi-docs": "^3.0.1",
"docusaurus-theme-openapi-docs": "^3.0.1",
"docusaurus2-dotenv": "^1.4.0",
"esbuild-loader": "^2.20.0",
"ldrs": "^1.0.1",
"prism-react-renderer": "^1.3.1",
"prism-react-renderer": "^2.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-live": "^4.0.0",
"react-markdown": "^8.0.7",
"react-spinners": "^0.13.8",
"react-tabs": "^4.3.0",
"esbuild-loader": "^2.20.0"
"react-tabs": "^4.3.0"
},
"overrides": {
"mermaid": "9.1.7"
"mermaid": "10.4.0"
},
"devDependencies": {
"@docusaurus/core": "2.4.3",
"@docusaurus/module-type-aliases": "2.4.3",
"@docusaurus/preset-classic": "2.4.3",
"@docusaurus/core": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/preset-classic": "3.4.0",
"@docusaurus/types": "3.4.0",
"husky": "^8.0.2",
"prettier": "2.8.0",
"pretty-quick": "^3.1.3"
@@ -70,7 +72,7 @@
]
},
"engines": {
"node": ">=16.14"
"node": ">=18.0"
},
"husky": {
"hooks": {

View File

@@ -1659,4 +1659,8 @@ module.exports = [
},
},
],
[
"@gracefullight/docusaurus-plugin-microsoft-clarity",
{ projectId: "naher5vlxx" },
],
];

View File

@@ -69,7 +69,7 @@ const sidebars = {
slug: '/api/v3',
},
// @ts-ignore
items: require('./docs/api/v3/sidebar.js'),
items: require('./docs/api/v3/sidebar.ts'),
},
{
type: 'category',
@@ -82,7 +82,7 @@ const sidebars = {
slug: '/api/beta',
},
// @ts-ignore
items: require('./docs/api/beta/sidebar.js'),
items: require('./docs/api/beta/sidebar.ts'),
},
],
},
@@ -121,7 +121,7 @@ const sidebars = {
slug: '/api/nerm/v1',
},
// @ts-ignore
items: require('./docs/api/nerm/v1/sidebar.js'),
items: require('./docs/api/nerm/v1/sidebar.ts'),
},
],
},
@@ -227,7 +227,7 @@ const sidebars = {
slug: '/api/iiq',
},
// @ts-ignore
items: require('./docs/api/iiq/sidebar.js'),
items: require('./docs/api/iiq/sidebar.ts'),
},
{
type: 'category',

View File

@@ -547,3 +547,7 @@ div[id^='discourse-comments'] {
display: flex;
padding-bottom: 2%;
}
.openapi-security__summary-container {
background: var(--ifm-pre-background);
}

View File

@@ -0,0 +1,29 @@
import React from "react";
import FormItem from "@theme/ApiExplorer/FormItem";
import FormSelect from "@theme/ApiExplorer/FormSelect";
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
import { setAccept } from "./slice";
function Accept() {
const value = useTypedSelector((state: any) => state.accept.value);
const options = useTypedSelector((state: any) => state.accept.options);
const dispatch = useTypedDispatch();
if (options.length <= 1) {
return null;
}
return (
<FormItem label="Accept">
<FormSelect
value={value}
options={options}
onChange={(e: any) => dispatch(setAccept(e.target.value))}
/>
</FormItem>
);
}
export default Accept;

View File

@@ -0,0 +1,22 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface State {
value: string;
options: string[];
}
const initialState: State = {} as any;
export const slice = createSlice({
name: "accept",
initialState,
reducers: {
setAccept: (state, action: PayloadAction<string>) => {
state.value = action.payload;
},
},
});
export const { setAccept } = slice.actions;
export default slice.reducer;

View File

@@ -0,0 +1,7 @@
.openapi-explorer__code-block-container {
height: 100%;
background: var(--prism-background-color);
color: var(--prism-color);
margin-bottom: unset;
box-shadow: var(--ifm-global-shadow-lw);
}

View File

@@ -0,0 +1,25 @@
import React, { ComponentProps } from "react";
import { ThemeClassNames, usePrismTheme } from "@docusaurus/theme-common";
import { getPrismCssVariables } from "@docusaurus/theme-common/internal";
import clsx from "clsx";
export default function CodeBlockContainer<T extends "div" | "pre">({
as: As,
...props
}: { as: T } & ComponentProps<T>): React.JSX.Element {
const prismTheme = usePrismTheme();
const prismCssVariables = getPrismCssVariables(prismTheme);
return (
<As
// Polymorphic components are hard to type, without `oneOf` generics
{...(props as any)}
style={prismCssVariables}
className={clsx(
"openapi-explorer__code-block-container",
props.className,
ThemeClassNames.common.codeBlock
)}
/>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import Container from "@theme/ApiExplorer/ApiCodeBlock/Container";
import type { Props } from "@theme/CodeBlock/Content/Element";
import clsx from "clsx";
// <pre> tags in markdown map to CodeBlocks. They may contain JSX children. When
// the children is not a simple string, we just return a styled block without
// actually highlighting.
export default function CodeBlockJSX({
children,
className,
}: Props): React.JSX.Element {
return (
<Container
as="pre"
tabIndex={0}
className={clsx(
"openapi-explorer__code-block-standalone",
"thin-scrollbar",
className
)}
>
<code className="openapi-explorer__code-block-lines">{children}</code>
</Container>
);
}

View File

@@ -0,0 +1,127 @@
import React from "react";
import { useThemeConfig, usePrismTheme } from "@docusaurus/theme-common";
import {
parseCodeBlockTitle,
parseLanguage,
parseLines,
containsLineNumbers,
useCodeWordWrap,
} from "@docusaurus/theme-common/internal";
import Container from "@theme/ApiExplorer/ApiCodeBlock/Container";
import CopyButton from "@theme/ApiExplorer/ApiCodeBlock/CopyButton";
import ExpandButton from "@theme/ApiExplorer/ApiCodeBlock/ExpandButton";
import Line from "@theme/ApiExplorer/ApiCodeBlock/Line";
import WordWrapButton from "@theme/ApiExplorer/ApiCodeBlock/WordWrapButton";
import type { Props } from "@theme/CodeBlock/Content/String";
import clsx from "clsx";
import { Highlight, Language } from "prism-react-renderer";
export default function CodeBlockString({
children,
className: blockClassName = "",
metastring,
title: titleProp,
showLineNumbers: showLineNumbersProp,
language: languageProp,
}: Props): React.JSX.Element {
const {
prism: { defaultLanguage, magicComments },
} = useThemeConfig();
const language =
languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage;
const prismTheme = usePrismTheme();
const wordWrap = useCodeWordWrap();
// We still parse the metastring in case we want to support more syntax in the
// future. Note that MDX doesn't strip quotes when parsing metastring:
// "title=\"xyz\"" => title: "\"xyz\""
const title = parseCodeBlockTitle(metastring) || titleProp;
const { lineClassNames, code } = parseLines(children, {
metastring,
language,
magicComments,
});
const showLineNumbers =
showLineNumbersProp ?? containsLineNumbers(metastring);
return (
<Container
as="div"
className={clsx(
blockClassName,
language &&
!blockClassName.includes(`language-${language}`) &&
`language-${language}`
)}
>
{title && (
<div className="openapi-explorer__code-block-title">{title}</div>
)}
<div className="openapi-explorer__code-block-content">
<Highlight
// {...defaultProps}
theme={prismTheme}
code={code}
language={language ?? "text"}
>
{({ className, tokens, getLineProps, getTokenProps }) => (
<pre
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
tabIndex={0}
ref={wordWrap.codeBlockRef}
className={clsx(
className,
"openapi-explorer__code-block",
"thin-scrollbar"
)}
>
<code
className={clsx(
"openapi-explorer__code-block-lines",
showLineNumbers &&
"openapi-explorer__code-block-lines-numbering"
)}
>
{tokens.map((line, i) => (
<Line
key={i}
line={line}
getLineProps={getLineProps}
getTokenProps={getTokenProps}
classNames={lineClassNames[i]}
showLineNumbers={showLineNumbers}
/>
))}
</code>
</pre>
)}
</Highlight>
<div className="openapi-explorer__code-block-btn-group">
{(wordWrap.isEnabled || wordWrap.isCodeScrollable) && (
<WordWrapButton
className="openapi-explorer__code-block-code-btn"
onClick={() => wordWrap.toggle()}
isEnabled={wordWrap.isEnabled}
/>
)}
<CopyButton
className="openapi-explorer__code-block-code-btn"
code={code}
/>
<ExpandButton
className={clsx(
"openapi-explorer__code-block-code-btn",
"openapi-explorer__expand-btn"
)}
code={code}
language={(language ?? "text") as Language}
showLineNumbers={showLineNumbers}
blockClassName={blockClassName}
title={title}
lineClassNames={lineClassNames}
/>
</div>
</div>
</Container>
);
}

View File

@@ -0,0 +1,91 @@
.openapi-explorer__code-block-content {
height: 100%;
position: relative;
/* rtl:ignore */
direction: ltr;
border-radius: inherit;
}
.openapi-explorer__code-block-title {
border-bottom: 1px solid var(--ifm-color-emphasis-300);
font-size: var(--ifm-code-font-size);
font-weight: 500;
padding: 0.75rem var(--ifm-pre-padding);
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
.openapi-explorer__code-block {
height: 100%;
border-radius: var(--ifm-global-radius);
--ifm-pre-background: var(--prism-background-color);
margin: 0;
padding: 0;
}
.openapi-explorer__code-block-title
+ .openapi-explorer__code-block-content
.openapi-explorer__code-block {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.openapi-explorer__code-block-standalone {
padding: 0;
}
.openapi-explorer__code-block-lines {
font: inherit;
/* rtl:ignore */
float: left;
min-width: 100%;
padding: var(--ifm-pre-padding);
}
.openapi-explorer__code-block-lines-numbering {
// This causes max-height to unset
// display: table;
padding: var(--ifm-pre-padding) 0;
}
@media print {
.openapi-explorer__code-block-lines {
white-space: pre-wrap;
}
}
.openapi-explorer__code-block-btn-group {
display: flex;
column-gap: 0.2rem;
position: absolute;
right: calc(var(--ifm-pre-padding) / 2);
top: calc(var(--ifm-pre-padding) / 2);
}
.openapi-explorer__code-block-btn-group button {
display: flex;
align-items: center;
background: var(--prism-background-color);
color: var(--prism-color);
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: var(--ifm-global-radius);
padding: 0.4rem;
line-height: 0;
transition: opacity 200ms ease-in-out;
opacity: 0;
}
.openapi-explorer__code-block-btn-group button:focus-visible,
.openapi-explorer__code-block-btn-group button:hover {
opacity: 1 !important;
}
.theme-code-block:hover .openapi-explorer__code-block-btn-group button {
opacity: 0.4;
}
@media screen and (max-width: 996px) {
.openapi-explorer__expand-btn {
display: none !important;
}
}

View File

@@ -0,0 +1,44 @@
.theme-code-block:hover {
.openapi-explorer__code-block-copy-btn--copied {
opacity: 1 !important;
}
}
.openapi-explorer__code-block-copy-btn-icons {
position: relative;
width: 1.125rem;
height: 1.125rem;
}
.openapi-explorer__code-block-copy-btn-icon,
.openapi-explorer__code-block-copy-btn-icon--success {
position: absolute;
top: 0;
left: 0;
fill: currentColor;
opacity: inherit;
width: inherit;
height: inherit;
transition: all 0.15s ease;
}
.openapi-explorer__code-block-copy-btn-icon--success {
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.33);
opacity: 0;
color: #00d600;
}
.openapi-explorer__code-block-copy-btn--copied {
.openapi-explorer__code-block-copy-btn-icon {
transform: scale(0.33);
opacity: 0;
}
.openapi-explorer__code-block-copy-btn-icon--success {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
transition-delay: 0.075s;
}
}

View File

@@ -0,0 +1,72 @@
import React, { useCallback, useState, useRef, useEffect } from "react";
import { CopyButtonProps } from "@docusaurus/theme-common/internal";
import { translate } from "@docusaurus/Translate";
import clsx from "clsx";
import copy from "copy-text-to-clipboard";
export default function CopyButton({
code,
className,
}: CopyButtonProps): React.JSX.Element {
const [isCopied, setIsCopied] = useState(false);
const copyTimeout = useRef<number | undefined>(undefined);
const handleCopyCode = useCallback(() => {
copy(code);
setIsCopied(true);
copyTimeout.current = window.setTimeout(() => {
setIsCopied(false);
}, 1000);
}, [code]);
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
return (
<button
type="button"
aria-label={
isCopied
? translate({
id: "theme.CodeBlock.copied",
message: "Copied",
description: "The copied button label on code blocks",
})
: translate({
id: "theme.CodeBlock.copyButtonAriaLabel",
message: "Copy code to clipboard",
description: "The ARIA label for copy code blocks button",
})
}
title={translate({
id: "theme.CodeBlock.copy",
message: "Copy",
description: "The copy button label on code blocks",
})}
className={clsx(
"clean-btn",
className,
"openapi-explorer__code-block-copy-btn",
isCopied && "openapi-explorer__code-block-copy-btn--copied"
)}
onClick={handleCopyCode}
>
<span
className="openapi-explorer__code-block-copy-btn-icons"
aria-hidden="true"
>
<svg
className="openapi-explorer__code-block-copy-btn-icon"
viewBox="0 0 24 24"
>
<path d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" />
</svg>
<svg
className="openapi-explorer__code-block-copy-btn-icon--success"
viewBox="0 0 24 24"
>
<path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>
</span>
</button>
);
}

View File

@@ -0,0 +1,16 @@
.openapi-explorer__code-block-exit-btn-icons {
position: relative;
width: 1.125rem;
height: 1.125rem;
}
.openapi-explorer__code-block-exit-btn-icon {
position: absolute;
top: 0;
left: 0;
fill: currentColor;
opacity: inherit;
width: inherit;
height: inherit;
transition: all 0.15s ease;
}

View File

@@ -0,0 +1,48 @@
import React from "react";
import { translate } from "@docusaurus/Translate";
import clsx from "clsx";
export interface Props {
readonly className: string;
readonly handler: () => void;
}
export default function ExitButton({
className,
handler,
}: Props): React.JSX.Element {
return (
<button
type="button"
aria-label={translate({
id: "theme.CodeBlock.exitButtonAriaLabel",
message: "Exit expanded view",
description: "The ARIA label for exit expanded view button",
})}
title={translate({
id: "theme.CodeBlock.copy",
message: "Copy",
description: "The exit button label on code blocks",
})}
className={clsx(
"clean-btn",
"openapi-explorer__code-block-exit-btn",
className
)}
onClick={handler}
>
<span
className="openapi-explorer__code-block-exit-btn-icons"
aria-hidden="true"
>
<svg
className="openapi-explorer__code-block-exit-btn-icon"
viewBox="0 0 384 512"
>
<path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z" />
</svg>
</span>
</button>
);
}

View File

@@ -0,0 +1,62 @@
.openapi-explorer__expand-modal-content {
padding: none;
border: thin solid var(--ifm-toc-border-color);
border-radius: var(--ifm-global-radius);
max-width: 95%;
width: 65vw;
height: 65vh;
overflow: auto;
}
.openapi-explorer__expand-modal-overlay {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
inset: 0px;
background-color: rgba(0, 0, 0, 0.9);
z-index: 201;
}
.theme-code-block:hover .openapi-explorer__code-block-expand-btn--copied {
opacity: 1 !important;
}
.openapi-explorer__code-block-expand-btn-icons {
position: relative;
width: 1.125rem;
height: 1.125rem;
}
.openapi-explorer__code-block-expand-btn-icon,
.openapi-explorer__code-block-expand-btn-icon--success {
position: absolute;
top: 0;
left: 0;
fill: currentColor;
opacity: inherit;
width: inherit;
height: inherit;
transition: all 0.15s ease;
}
.openapi-explorer__code-block-expand-btn-icon--success {
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.33);
opacity: 0;
color: #00d600;
}
.openapi-explorer__code-block-expand-btn--copied
.openapi-explorer__code-block-expand-btn-icon {
transform: scale(0.33);
opacity: 0;
}
.openapi-explorer__code-block-expand-btn--copied
.openapi-explorer__code-block-expand-btn-icon--success {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
transition-delay: 0.075s;
}

View File

@@ -0,0 +1,159 @@
import React, { useEffect, useState } from "react";
import { usePrismTheme } from "@docusaurus/theme-common";
import { translate } from "@docusaurus/Translate";
import Container from "@theme/ApiExplorer/ApiCodeBlock/Container";
import CopyButton from "@theme/ApiExplorer/ApiCodeBlock/CopyButton";
import ExitButton from "@theme/ApiExplorer/ApiCodeBlock/ExitButton";
import Line from "@theme/ApiExplorer/ApiCodeBlock/Line";
import clsx from "clsx";
import { Highlight, Language } from "prism-react-renderer";
import Modal from "react-modal";
export interface Props {
readonly code: string;
readonly className: string;
readonly language: Language;
readonly showLineNumbers: boolean;
readonly blockClassName: string;
readonly title: string | undefined;
readonly lineClassNames: { [lineIndex: number]: string[] };
}
export default function ExpandButton({
code,
className,
language,
showLineNumbers,
blockClassName,
title,
lineClassNames,
}: Props): React.JSX.Element {
const [isModalOpen, setIsModalOpen] = useState(false);
const prismTheme = usePrismTheme();
useEffect(() => {
Modal.setAppElement("body");
}, []);
return (
<>
<button
type="button"
aria-label={
isModalOpen
? translate({
id: "theme.CodeBlock.expanded",
message: "Expanded",
description: "The expanded button label on code blocks",
})
: translate({
id: "theme.CodeBlock.expandButtonAriaLabel",
message: "Expand code to fullscreen",
description: "The ARIA label for expand code blocks button",
})
}
title={translate({
id: "theme.CodeBlock.expand",
message: "Expand",
description: "The expand button label on code blocks",
})}
className={clsx(
"clean-btn",
className,
"openapi-explorer__code-block-expand-btn",
isModalOpen && "openapi-explorer__code-block-expand-btn--copied"
)}
onClick={() => setIsModalOpen(true)}
>
<span
className="openapi-explorer__code-block-expand-btn-icons"
aria-hidden="true"
>
<svg
className="openapi-explorer__code-block-expand-btn-icon"
viewBox="0 0 448 512"
>
<path d="M32 32C14.3 32 0 46.3 0 64v96c0 17.7 14.3 32 32 32s32-14.3 32-32V96h64c17.7 0 32-14.3 32-32s-14.3-32-32-32H32zM64 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v96c0 17.7 14.3 32 32 32h96c17.7 0 32-14.3 32-32s-14.3-32-32-32H64V352zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32h64v64c0 17.7 14.3 32 32 32s32-14.3 32-32V64c0-17.7-14.3-32-32-32H320zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32v64H320c-17.7 0-32 14.3-32 32s14.3 32 32 32h96c17.7 0 32-14.3 32-32V352z" />
</svg>
<svg
className="openapi-explorer__code-block-expand-btn-icon--success"
viewBox="0 0 24 24"
>
<path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>
</span>
</button>
<Modal
className="openapi-explorer__expand-modal-content"
overlayClassName="openapi-explorer__expand-modal-overlay"
isOpen={isModalOpen}
onRequestClose={() => setIsModalOpen(false)}
contentLabel="Code Snippet"
>
<Container
as="div"
className={clsx(
"openapi-explorer__code-block-container",
language &&
!blockClassName.includes(`language-${language}`) &&
`language-${language}`
)}
>
{title && (
<div className="openapi-explorer__code-block-title">{title}</div>
)}
<div className="openapi-explorer__code-block-content">
<Highlight
// {...defaultProps}
theme={prismTheme}
code={code}
language={language ?? "text"}
>
{({ className, tokens, getLineProps, getTokenProps }) => (
<pre
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
tabIndex={0}
className={clsx(
className,
"openapi-explorer__code-block",
"thin-scrollbar"
)}
>
<code
className={clsx(
"openapi-explorer__code-block-lines",
showLineNumbers &&
"openapi-explorer__code-block-lines-numbers"
)}
>
{tokens.map((line, i) => (
<Line
key={i}
line={line}
getLineProps={getLineProps}
getTokenProps={getTokenProps}
classNames={lineClassNames[i]}
showLineNumbers={showLineNumbers}
/>
))}
</code>
</pre>
)}
</Highlight>
<div className="openapi-explorer__code-block-btn-group">
<CopyButton
className="openapi-explorer__code-block-code-btn"
code={code}
/>
<ExitButton
className="openapi-explorer__code-block-code-btn"
handler={() => setIsModalOpen(false)}
/>
</div>
</div>
</Container>
</Modal>
</>
);
}

View File

@@ -0,0 +1,44 @@
:where(:root) {
--docusaurus-highlighted-code-line-bg: rgb(72 77 91);
}
:where([data-theme="dark"]) {
--docusaurus-highlighted-code-line-bg: rgb(100 100 100);
}
.theme-code-block-highlighted-line {
background-color: var(--docusaurus-highlighted-code-line-bg);
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);
}
.openapi-explorer__code-block-code-line {
display: table-row;
counter-increment: line-count;
}
.openapi-explorer__code-block-code-line-number {
display: table-cell;
text-align: right;
width: 1%;
position: sticky;
left: 0;
padding: 0 var(--ifm-pre-padding);
background: var(--ifm-pre-background);
overflow-wrap: normal;
}
.openapi-explorer__code-block-code-line-number::before {
content: counter(line-count);
opacity: 0.4;
}
:global(.theme-code-block-highlighted-line)
.openapi-explorer__code-block-code-line-number::before {
opacity: 0.8;
}
.openapi-explorer__code-block-code-line-number {
padding-right: var(--ifm-pre-padding);
}

View File

@@ -0,0 +1,41 @@
import React from "react";
import { LineProps } from "@docusaurus/theme-common/internal";
import clsx from "clsx";
export default function CodeBlockLine({
line,
classNames,
showLineNumbers,
getLineProps,
getTokenProps,
}: LineProps): React.JSX.Element {
if (line.length === 1 && line[0].content === "\n") {
line[0].content = "";
}
const lineProps = getLineProps({
line,
className: clsx(
classNames,
showLineNumbers && "openapi-explorer__code-block-code-line"
),
});
const lineTokens = line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
));
return (
<span {...lineProps}>
{showLineNumbers ? (
<>
<span className="openapi-explorer__code-block-code-line-number" />
<span className="openapi-explorer__code-block-code-line-content">
{lineTokens}
</span>
</>
) : (
lineTokens
)}
<br />
</span>
);
}

View File

@@ -0,0 +1,10 @@
.openapi-explorer__code-block-word-wrap-btn-icon {
width: 1.2rem;
height: 1.2rem;
}
.openapi-explorer__code-block-word-wrap-btn--enabled {
.openapi-explorer__code-block-word-wrap-btn-icon {
color: var(--ifm-color-primary);
}
}

View File

@@ -0,0 +1,47 @@
import React from "react";
import { translate } from "@docusaurus/Translate";
import clsx from "clsx";
export interface Props {
readonly className?: string;
readonly onClick: React.MouseEventHandler;
readonly isEnabled: boolean;
}
export default function WordWrapButton({
className,
onClick,
isEnabled,
}: Props): React.JSX.Element | null {
const title = translate({
id: "theme.CodeBlock.wordWrapToggle",
message: "Toggle word wrap",
description:
"The title attribute for toggle word wrapping button of code block lines",
});
return (
<button
type="button"
onClick={onClick}
className={clsx(
"clean-btn",
className,
isEnabled && "openapi-explorer__code-block-word-wrap-btn--enabled"
)}
aria-label={title}
title={title}
>
<svg
className="openapi-explorer__code-block-word-wrap-btn-icon"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill="currentColor"
d="M4 19h6v-2H4v2zM20 5H4v2h16V5zm-3 6H4v2h13.25c1.1 0 2 .9 2 2s-.9 2-2 2H15v-2l-3 3l3 3v-2h2c2.21 0 4-1.79 4-4s-1.79-4-4-4z"
/>
</svg>
</button>
);
}

View File

@@ -0,0 +1,38 @@
import React, { isValidElement, ReactNode } from "react";
import { CodeBlockProps } from "@docusaurus/theme-common/internal";
import useIsBrowser from "@docusaurus/useIsBrowser";
import ElementContent from "@theme/ApiExplorer/ApiCodeBlock/Content/Element";
import StringContent from "@theme/ApiExplorer/ApiCodeBlock/Content/String";
/**
* Best attempt to make the children a plain string so it is copyable. If there
* are react elements, we will not be able to copy the content, and it will
* return `children` as-is; otherwise, it concatenates the string children
* together.
*/
function maybeStringifyChildren(children: ReactNode): ReactNode {
if (React.Children.toArray(children).some((el) => isValidElement(el))) {
return children;
}
// The children is now guaranteed to be one/more plain strings
return Array.isArray(children) ? children.join("") : (children as string);
}
export default function ApiCodeBlock({
children: rawChildren,
...props
}: CodeBlockProps) {
// The Prism theme on SSR is always the default theme but the site theme can
// be in a different mode. React hydration doesn't update DOM styles that come
// from SSR. Hence force a re-render after mounting to apply the current
// relevant styles.
const isBrowser = useIsBrowser();
const children = maybeStringifyChildren(rawChildren);
const CodeBlockComp =
typeof children === "string" ? StringContent : ElementContent;
return (
<CodeBlockComp key={String(isBrowser)} {...props}>
{children as string}
</CodeBlockComp>
);
}

View File

@@ -0,0 +1,23 @@
export function getAuthDataKeys(security: { [key: string]: any }) {
// Bearer Auth
if (security.type === "http" && security.scheme === "bearer") {
return ["token"];
}
if (security.type === "oauth2") {
return ["token"];
}
// Basic Auth
if (security.type === "http" && security.scheme === "basic") {
return ["username", "password"];
}
// API Auth
if (security.type === "apiKey") {
return ["apiKey"];
}
// none
return [];
}

View File

@@ -0,0 +1,148 @@
import React from "react";
import FormItem from "@theme/ApiExplorer/FormItem";
import FormSelect from "@theme/ApiExplorer/FormSelect";
import FormTextInput from "@theme/ApiExplorer/FormTextInput";
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
import { setAuthData, setSelectedAuth } from "./slice";
function Authorization() {
const data = useTypedSelector((state: any) => state.auth.data);
const options = useTypedSelector((state: any) => state.auth.options);
const selected = useTypedSelector((state: any) => state.auth.selected);
const dispatch = useTypedDispatch();
if (selected === undefined) {
return null;
}
const selectedAuth = options[selected];
const optionKeys = Object.keys(options);
return (
<div>
{optionKeys.length > 1 && (
<FormItem label="Security Scheme">
<FormSelect
options={optionKeys}
value={selected}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setSelectedAuth(e.target.value));
}}
/>
</FormItem>
)}
{selectedAuth.map((a: any) => {
if (a.type === "http" && a.scheme === "bearer") {
return (
<FormItem label="Bearer Token" key={a.key + "-bearer"}>
<FormTextInput
placeholder="Bearer Token"
value={data[a.key].token ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
dispatch(
setAuthData({
scheme: a.key,
key: "token",
value: value ? value : undefined,
})
);
}}
/>
</FormItem>
);
}
if (a.type === "oauth2") {
return (
<FormItem label="Bearer Token" key={a.key + "-oauth2"}>
<FormTextInput
placeholder="Bearer Token"
value={data[a.key].token ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
dispatch(
setAuthData({
scheme: a.key,
key: "token",
value: value ? value : undefined,
})
);
}}
/>
</FormItem>
);
}
if (a.type === "http" && a.scheme === "basic") {
return (
<React.Fragment key={a.key + "-basic"}>
<FormItem label="Username">
<FormTextInput
placeholder="Username"
value={data[a.key].username ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
dispatch(
setAuthData({
scheme: a.key,
key: "username",
value: value ? value : undefined,
})
);
}}
/>
</FormItem>
<FormItem label="Password">
<FormTextInput
placeholder="Password"
password
value={data[a.key].password ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
dispatch(
setAuthData({
scheme: a.key,
key: "password",
value: value ? value : undefined,
})
);
}}
/>
</FormItem>
</React.Fragment>
);
}
if (a.type === "apiKey") {
return (
<FormItem label={`${a.key}`} key={a.key + "-apikey"}>
<FormTextInput
placeholder={`${a.key}`}
value={data[a.key].apiKey ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
dispatch(
setAuthData({
scheme: a.key,
key: "apiKey",
value: value ? value : undefined,
})
);
}}
/>
</FormItem>
);
}
return null;
})}
</div>
);
}
export default Authorization;

View File

@@ -0,0 +1,139 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { createStorage, hashArray } from "@theme/ApiExplorer/storage-utils";
import {
SecurityRequirementObject,
SecuritySchemeObject,
} from "docusaurus-plugin-openapi-docs/src/openapi/types";
/* eslint-disable import/no-extraneous-dependencies*/
import { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types";
import { getAuthDataKeys } from "./auth-types";
// The global definitions
// "securitySchemes": {
// "BearerAuth": { "type": "http", "scheme": "BeAreR" },
// "BasicAuth": { "type": "http", "scheme": "basic" }
// },
// The operation level requirements
// "security": [
// { "BearerAuth": [] },
// { "BearerAuth": [], "BasicAuth": [] }
// ],
// SLICE_STATE
// data:
// BearerAuth:
// token=xxx
// BasicAuth:
// username=xxx
// password=xxx
//
// options:
// "BearerAuth": [{ key: "BearerAuth", scopes: [], ...rest }]
// "BearerAuth and BasicAuth": [{ key: "BearerAuth", scopes: [], ...rest }, { key: "BasicAuth", scopes: [], ...rest }]
//
// selected: "BearerAuth and BasicAuth"
// LOCAL_STORAGE
// hash(SLICE_STATE.options) -> "BearerAuth and BasicAuth"
// BearerAuth -> { token: xxx }
// BasicAuth -> { username: xxx, password: xxx }
export function createAuth({
security,
securitySchemes,
options: opts,
}: {
security?: SecurityRequirementObject[];
securitySchemes?: {
[key: string]: SecuritySchemeObject;
};
options?: ThemeConfig["api"];
}): AuthState {
const storage = createStorage("sessionStorage");
let data: AuthState["data"] = {};
let options: AuthState["options"] = {};
for (const option of security ?? []) {
const id = Object.keys(option).join(" and ");
for (const [schemeID, scopes] of Object.entries(option)) {
const scheme = securitySchemes?.[schemeID];
if (scheme) {
if (options[id] === undefined) {
options[id] = [];
}
const dataKeys = getAuthDataKeys(scheme);
for (const key of dataKeys) {
if (data[schemeID] === undefined) {
data[schemeID] = {};
}
let persisted = undefined;
try {
persisted = JSON.parse(storage.getItem(schemeID) ?? "")[key];
} catch {}
data[schemeID][key] = persisted;
}
options[id].push({
...scheme,
key: schemeID,
scopes,
});
}
}
}
let persisted = undefined;
try {
persisted = storage.getItem(hashArray(Object.keys(options))) ?? undefined;
} catch {}
return {
data,
options,
selected: persisted ?? Object.keys(options)[0],
};
}
export type Scheme = {
key: string;
scopes: string[];
} & SecuritySchemeObject;
export interface AuthState {
data: {
[scheme: string]: {
[key: string]: string | undefined;
};
};
options: {
[key: string]: Scheme[];
};
selected?: string;
}
const initialState: AuthState = {} as any;
export const slice = createSlice({
name: "auth",
initialState,
reducers: {
setAuthData: (
state,
action: PayloadAction<{ scheme: string; key: string; value?: string }>
) => {
const { scheme, key, value } = action.payload;
state.data[scheme][key] = value;
},
setSelectedAuth: (state, action: PayloadAction<string>) => {
state.selected = action.payload;
},
},
});
export const { setAuthData, setSelectedAuth } = slice.actions;
export default slice.reducer;

View File

@@ -0,0 +1,361 @@
import React from "react";
import json2xml from "@theme/ApiExplorer/Body/json2xml";
import FormFileUpload from "@theme/ApiExplorer/FormFileUpload";
import FormItem from "@theme/ApiExplorer/FormItem";
import FormSelect from "@theme/ApiExplorer/FormSelect";
import FormTextInput from "@theme/ApiExplorer/FormTextInput";
import LiveApp from "@theme/ApiExplorer/LiveEditor";
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
import Markdown from "@theme/Markdown";
import SchemaTabs from "@theme/SchemaTabs";
import TabItem from "@theme/TabItem";
import { RequestBodyObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
import format from "xml-formatter";
import {
clearFormBodyKey,
clearRawBody,
setFileFormBody,
setFileRawBody,
setStringFormBody,
} from "./slice";
export interface Props {
jsonRequestBodyExample: string;
requestBodyMetadata?: RequestBodyObject;
methods?: any;
required?: boolean;
}
function BodyWrap({
requestBodyMetadata,
jsonRequestBodyExample,
methods,
required,
}: Props) {
const contentType = useTypedSelector((state: any) => state.contentType.value);
// NOTE: We used to check if body was required, but opted to always show the request body
// to reduce confusion, see: https://github.com/cloud-annotations/docusaurus-openapi/issues/145
// No body
if (contentType === undefined) {
return null;
}
return (
<Body
requestBodyMetadata={requestBodyMetadata}
jsonRequestBodyExample={jsonRequestBodyExample}
required={required}
/>
);
}
function Body({
requestBodyMetadata,
jsonRequestBodyExample,
methods,
required,
}: Props) {
const contentType = useTypedSelector((state: any) => state.contentType.value);
const dispatch = useTypedDispatch();
// Lot's of possible content-types:
// - application/json
// - application/xml
// - text/plain
// - text/css
// - text/html
// - text/javascript
// - application/javascript
// - multipart/form-data
// - application/x-www-form-urlencoded
// - image/svg+xml;charset=US-ASCII
// Show editor:
// - application/json
// - application/xml
// - */*
// Show form:
// - multipart/form-data
// - application/x-www-form-urlencoded
const schema = requestBodyMetadata?.content?.[contentType]?.schema;
const example = requestBodyMetadata?.content?.[contentType]?.example;
const examples = requestBodyMetadata?.content?.[contentType]?.examples;
if (schema?.format === "binary") {
return (
<FormItem>
<FormFileUpload
placeholder={schema.description || "Body"}
onChange={(file: any) => {
if (file === undefined) {
dispatch(clearRawBody());
return;
}
dispatch(
setFileRawBody({
src: `/path/to/${file.name}`,
content: file,
})
);
}}
/>
</FormItem>
);
}
if (
(contentType === "multipart/form-data" ||
contentType === "application/x-www-form-urlencoded") &&
schema?.type === "object"
) {
return (
<FormItem className="openapi-explorer__form-item-body-container">
<div>
{Object.entries(schema.properties ?? {}).map(([key, val]: any) => {
if (val.format === "binary") {
return (
<FormItem
key={key}
label={key}
required={
Array.isArray(schema.required) &&
schema.required.includes(key)
}
>
<FormFileUpload
placeholder={val.description || key}
onChange={(file: any) => {
if (file === undefined) {
dispatch(clearFormBodyKey(key));
return;
}
dispatch(
setFileFormBody({
key: key,
value: {
src: `/path/to/${file.name}`,
content: file,
},
})
);
}}
/>
</FormItem>
);
}
if (val.enum) {
return (
<FormItem
key={key}
label={key}
required={
Array.isArray(schema.required) &&
schema.required.includes(key)
}
>
<FormSelect
options={["---", ...val.enum]}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
if (val === "---") {
dispatch(clearFormBodyKey(key));
} else {
dispatch(
setStringFormBody({
key: key,
value: val,
})
);
}
}}
/>
</FormItem>
);
}
// TODO: support all the other types.
return (
<FormItem
key={key}
label={key}
required={
Array.isArray(schema.required) &&
schema.required.includes(key)
}
>
<FormTextInput
paramName={key}
isRequired={
Array.isArray(schema.required) &&
schema.required.includes(key)
}
placeholder={val.description || key}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(
setStringFormBody({ key: key, value: e.target.value })
);
}}
/>
</FormItem>
);
})}
</div>
</FormItem>
);
}
let language = "plaintext";
let defaultBody = ""; //"body content";
let exampleBody;
let examplesBodies = [] as any;
if (
contentType.includes("application/json") ||
contentType.endsWith("+json")
) {
if (jsonRequestBodyExample) {
defaultBody = JSON.stringify(jsonRequestBodyExample, null, 2);
}
if (example) {
exampleBody = JSON.stringify(example, null, 2);
}
if (examples) {
for (const [key, example] of Object.entries(examples)) {
examplesBodies.push({
label: key,
body: JSON.stringify(example.value, null, 2),
summary: example.summary,
});
}
}
language = "json";
}
if (contentType === "application/xml" || contentType.endsWith("+xml")) {
if (jsonRequestBodyExample) {
try {
defaultBody = format(json2xml(jsonRequestBodyExample, ""), {
indentation: " ",
lineSeparator: "\n",
collapseContent: true,
});
} catch {
defaultBody = json2xml(jsonRequestBodyExample);
}
}
if (example) {
try {
exampleBody = format(json2xml(example, ""), {
indentation: " ",
lineSeparator: "\n",
collapseContent: true,
});
} catch {
exampleBody = json2xml(example);
}
}
if (examples) {
for (const [key, example] of Object.entries(examples)) {
let formattedXmlBody;
try {
formattedXmlBody = format(example.value, {
indentation: " ",
lineSeparator: "\n",
collapseContent: true,
});
} catch {
formattedXmlBody = example.value;
}
examplesBodies.push({
label: key,
body: formattedXmlBody,
summary: example.summary,
});
}
}
language = "xml";
}
if (exampleBody) {
return (
<FormItem>
<SchemaTabs className="openapi-tabs__schema" lazy>
{/* @ts-ignore */}
<TabItem
label="Example (from schema)"
value="Example (from schema)"
default
>
<LiveApp action={dispatch} language={language} required={required}>
{defaultBody}
</LiveApp>
</TabItem>
{/* @ts-ignore */}
<TabItem label="Example" value="example">
{example.summary && <Markdown children={example.summary} />}
{exampleBody && (
<LiveApp
action={dispatch}
language={language}
required={required}
>
{exampleBody}
</LiveApp>
)}
</TabItem>
</SchemaTabs>
</FormItem>
);
}
if (examplesBodies && examplesBodies.length > 0) {
return (
<FormItem className="openapi-explorer__form-item-body-container">
<SchemaTabs className="openapi-tabs__schema" lazy>
{/* @ts-ignore */}
<TabItem
label="Example (from schema)"
value="Example (from schema)"
default
>
<LiveApp action={dispatch} language={language} required={required}>
{defaultBody}
</LiveApp>
</TabItem>
{examplesBodies.map((example: any) => {
return (
// @ts-ignore
<TabItem
label={example.label}
value={example.label}
key={example.label}
>
{example.summary && <Markdown children={example.summary} />}
{example.body && (
<LiveApp action={dispatch} language={language}>
{example.body}
</LiveApp>
)}
</TabItem>
);
})}
</SchemaTabs>
</FormItem>
);
}
return (
<FormItem>
<LiveApp action={dispatch} language={language} required={required}>
{defaultBody}
</LiveApp>
</FormItem>
);
}
export default BodyWrap;

View File

@@ -0,0 +1,36 @@
export default function json2xml(o, tab) {
var toXml = function (v, name, ind) {
var xml = "";
if (v instanceof Array) {
for (var i = 0, n = v.length; i < n; i++)
xml += ind + toXml(v[i], name, ind + "\t") + "\n";
} else if (typeof v == "object") {
var hasChild = false;
xml += ind + "<" + name;
for (var m in v) {
if (m.charAt(0) === "@")
xml += " " + m.substr(1) + '="' + v[m].toString() + '"';
else hasChild = true;
}
xml += hasChild ? ">" : "/>";
if (hasChild) {
for (var m2 in v) {
if (m2 === "#text") xml += v[m2];
else if (m2 === "#cdata") xml += "<![CDATA[" + v[m2] + "]]>";
else if (m2.charAt(0) !== "@") xml += toXml(v[m2], m2, ind + "\t");
}
xml +=
(xml.charAt(xml.length - 1) === "\n" ? ind : "") +
"</" +
name +
">";
}
} else {
xml += ind + "<" + name + ">" + v.toString() + "</" + name + ">";
}
return xml;
},
xml = "";
for (var m3 in o) xml += toXml(o[m3], m3, "");
return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, "");
}

View File

@@ -0,0 +1,126 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface FileContent {
type: "file";
value: {
src: string;
content: Blob;
};
}
export interface StringContent {
type: "string";
value?: string;
}
export type Content = FileContent | StringContent | undefined;
export interface FormBody {
type: "form";
content: {
[key: string]: Content;
};
}
export interface RawBody {
type: "raw";
content: Content;
}
export interface EmptyBody {
type: "empty";
}
export type Body = EmptyBody | FormBody | RawBody;
export type State = Body;
const initialState: State = {} as any;
export const slice = createSlice({
name: "body",
initialState,
reducers: {
clearRawBody: (_state) => {
return {
type: "empty",
};
},
setStringRawBody: (_state, action: PayloadAction<string>) => {
return {
type: "raw",
content: {
type: "string",
value: action.payload,
},
};
},
setFileRawBody: (_state, action: PayloadAction<FileContent["value"]>) => {
return {
type: "raw",
content: {
type: "file",
value: action.payload,
},
};
},
clearFormBodyKey: (state, action: PayloadAction<string>) => {
if (state?.type === "form") {
delete state.content[action.payload];
}
},
setStringFormBody: (
state,
action: PayloadAction<{ key: string; value: string }>
) => {
if (state?.type !== "form") {
return {
type: "form",
content: {
[action.payload.key]: {
type: "string",
value: action.payload.value,
},
},
};
}
state.content[action.payload.key] = {
type: "string",
value: action.payload.value,
};
return state;
},
setFileFormBody: (
state,
action: PayloadAction<{ key: string; value: FileContent["value"] }>
) => {
if (state?.type !== "form") {
return {
type: "form",
content: {
[action.payload.key]: {
type: "file",
value: action.payload.value,
},
},
};
}
state.content[action.payload.key] = {
type: "file",
value: action.payload.value,
};
return state;
},
},
});
export const {
clearRawBody,
setStringRawBody,
setFileRawBody,
clearFormBodyKey,
setStringFormBody,
setFileFormBody,
} = slice.actions;
export default slice.reducer;

View File

@@ -0,0 +1,48 @@
// https://github.com/github-linguist/linguist/blob/master/lib/linguist/popular.yml
export type CodeSampleLanguage =
| "C"
| "C#"
| "C++"
| "CoffeeScript"
| "CSS"
| "Dart"
| "DM"
| "Elixir"
| "Go"
| "Groovy"
| "HTML"
| "Java"
| "JavaScript"
| "Kotlin"
| "Objective-C"
| "Perl"
| "PHP"
| "PowerShell"
| "Python"
| "Ruby"
| "Rust"
| "Scala"
| "Shell"
| "Swift"
| "TypeScript";
export interface Language {
highlight: string;
language: string;
codeSampleLanguage: CodeSampleLanguage;
logoClass: string;
variant: string;
variants: string[];
options?: { [key: string]: boolean };
sample?: string;
samples?: string[];
samplesSources?: string[];
samplesLabels?: string[];
}
// https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples
export interface CodeSample {
source: string;
lang: CodeSampleLanguage;
label?: string;
}

View File

@@ -0,0 +1,438 @@
/* ============================================================================
* Copyright (c) Palo Alto Networks
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* ========================================================================== */
import React, { useState, useEffect } from "react";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import ApiCodeBlock from "@theme/ApiExplorer/ApiCodeBlock";
import buildPostmanRequest from "@theme/ApiExplorer/buildPostmanRequest";
import CodeTabs from "@theme/ApiExplorer/CodeTabs";
import { useTypedSelector } from "@theme/ApiItem/hooks";
import codegen from "postman-code-generators";
import sdk from "postman-collection";
import { CodeSample, Language } from "./code-snippets-types";
import {
getCodeSampleSourceFromLanguage,
mergeArraysbyLanguage,
mergeCodeSampleLanguage,
} from "./languages";
export const languageSet: Language[] = [
{
highlight: "bash",
language: "curl",
codeSampleLanguage: "Shell",
logoClass: "bash",
options: {
longFormat: false,
followRedirect: true,
trimRequestBody: true,
},
variant: "cURL",
variants: ["curl"],
},
{
highlight: "python",
language: "python",
codeSampleLanguage: "Python",
logoClass: "python",
options: {
followRedirect: true,
trimRequestBody: true,
},
variant: "requests",
variants: ["requests", "http.client"],
},
{
highlight: "go",
language: "go",
codeSampleLanguage: "Go",
logoClass: "go",
options: {
followRedirect: true,
trimRequestBody: true,
},
variant: "native",
variants: ["native"],
},
{
highlight: "javascript",
language: "nodejs",
codeSampleLanguage: "JavaScript",
logoClass: "nodejs",
options: {
ES6_enabled: true,
followRedirect: true,
trimRequestBody: true,
},
variant: "axios",
variants: ["axios", "native"],
},
{
highlight: "ruby",
language: "ruby",
codeSampleLanguage: "Ruby",
logoClass: "ruby",
options: {
followRedirect: true,
trimRequestBody: true,
},
variant: "Net::HTTP",
variants: ["net::http"],
},
{
highlight: "csharp",
language: "csharp",
codeSampleLanguage: "C#",
logoClass: "csharp",
options: {
followRedirect: true,
trimRequestBody: true,
},
variant: "RestSharp",
variants: ["restsharp", "httpclient"],
},
{
highlight: "php",
language: "php",
codeSampleLanguage: "PHP",
logoClass: "php",
options: {
followRedirect: true,
trimRequestBody: true,
},
variant: "cURL",
variants: ["curl", "guzzle", "pecl_http", "http_request2"],
},
{
highlight: "java",
language: "java",
codeSampleLanguage: "Java",
logoClass: "java",
options: {
followRedirect: true,
trimRequestBody: true,
},
variant: "OkHttp",
variants: ["okhttp", "unirest"],
},
{
highlight: "powershell",
language: "powershell",
codeSampleLanguage: "PowerShell",
logoClass: "powershell",
options: {
followRedirect: true,
trimRequestBody: true,
},
variant: "RestMethod",
variants: ["restmethod"],
},
];
export interface Props {
postman: sdk.Request;
codeSamples: CodeSample[];
}
function CodeTab({ children, hidden, className }: any): JSX.Element {
return (
<div role="tabpanel" className={className} {...{ hidden }}>
{children}
</div>
);
}
function CodeSnippets({ postman, codeSamples }: Props) {
const { siteConfig } = useDocusaurusContext();
const contentType = useTypedSelector((state: any) => state.contentType.value);
const accept = useTypedSelector((state: any) => state.accept.value);
const server = useTypedSelector((state: any) => state.server.value);
const body = useTypedSelector((state: any) => state.body);
const pathParams = useTypedSelector((state: any) => state.params.path);
const queryParams = useTypedSelector((state: any) => state.params.query);
const cookieParams = useTypedSelector((state: any) => state.params.cookie);
const headerParams = useTypedSelector((state: any) => state.params.header);
const auth = useTypedSelector((state: any) => state.auth);
// User-defined languages array
// Can override languageSet, change order of langs, override options and variants
const userDefinedLanguageSet =
(siteConfig?.themeConfig?.languageTabs as Language[] | undefined) ??
languageSet;
// Filter languageSet by user-defined langs
const filteredLanguageSet = languageSet.filter((ls) => {
return userDefinedLanguageSet?.some((lang) => {
return lang.language === ls.language;
});
});
// Merge user-defined langs into languageSet
const mergedLangs = mergeCodeSampleLanguage(
mergeArraysbyLanguage(userDefinedLanguageSet, filteredLanguageSet),
codeSamples
);
// Read defaultLang from localStorage
const defaultLang: Language[] = mergedLangs.filter(
(lang) =>
lang.language === localStorage.getItem("docusaurus.tab.code-samples")
);
const [selectedVariant, setSelectedVariant] = useState<string | undefined>();
const [selectedSample, setSelectedSample] = useState<string | undefined>();
const [language, setLanguage] = useState(() => {
// Return first index if only 1 user-defined language exists
if (mergedLangs.length === 1) {
return mergedLangs[0];
}
// Fall back to language in localStorage or first user-defined language
return defaultLang[0] ?? mergedLangs[0];
});
const [codeText, setCodeText] = useState<string>("");
const [codeSampleCodeText, setCodeSampleCodeText] = useState<
string | (() => string)
>(() => getCodeSampleSourceFromLanguage(language));
useEffect(() => {
if (language && !!language.sample) {
setCodeSampleCodeText(getCodeSampleSourceFromLanguage(language));
}
if (language && !!language.options) {
const postmanRequest = buildPostmanRequest(postman, {
queryParams,
pathParams,
cookieParams,
contentType,
accept,
headerParams,
body,
server,
auth,
});
codegen.convert(
language.language,
language.variant,
postmanRequest,
language.options,
(error: any, snippet: string) => {
if (error) {
return;
}
setCodeText(snippet);
}
);
} else if (language && !language.options) {
const langSource = mergedLangs.filter(
(lang) => lang.language === language.language
);
// Merges user-defined language with default languageSet
// This allows users to define only the minimal properties necessary in languageTabs
// User-defined properties should override languageSet properties
const mergedLanguage = { ...langSource[0], ...language };
const postmanRequest = buildPostmanRequest(postman, {
queryParams,
pathParams,
cookieParams,
contentType,
accept,
headerParams,
body,
server,
auth,
});
codegen.convert(
mergedLanguage.language,
mergedLanguage.variant,
postmanRequest,
mergedLanguage.options,
(error: any, snippet: string) => {
if (error) {
return;
}
setCodeText(snippet);
}
);
} else {
setCodeText("");
}
}, [
accept,
body,
contentType,
cookieParams,
headerParams,
language,
pathParams,
postman,
queryParams,
server,
auth,
mergedLangs,
]);
// no dependencies was intentionlly set for this particular hook. it's safe as long as if conditions are set
useEffect(function onSelectedVariantUpdate() {
if (selectedVariant && selectedVariant !== language.variant) {
const postmanRequest = buildPostmanRequest(postman, {
queryParams,
pathParams,
cookieParams,
contentType,
accept,
headerParams,
body,
server,
auth,
});
codegen.convert(
language.language,
selectedVariant,
postmanRequest,
language.options,
(error: any, snippet: string) => {
if (error) {
return;
}
setCodeText(snippet);
}
);
}
});
// no dependencies was intentionlly set for this particular hook. it's safe as long as if conditions are set
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(function onSelectedSampleUpdate() {
if (
language.samples &&
language.samplesSources &&
selectedSample &&
selectedSample !== language.sample
) {
const sampleIndex = language.samples.findIndex(
(smp) => smp === selectedSample
);
setCodeSampleCodeText(language.samplesSources[sampleIndex]);
}
});
if (language === undefined) {
return null;
}
return (
<>
<CodeTabs
groupId="code-samples"
action={{
setLanguage: setLanguage,
setSelectedVariant: setSelectedVariant,
setSelectedSample: setSelectedSample,
}}
languageSet={mergedLangs}
lazy
>
{mergedLangs.map((lang) => {
return (
<CodeTab
value={lang.language}
label={lang.language}
key={lang.language}
attributes={{
className: `openapi-tabs__code-item--${lang.logoClass}`,
}}
>
{lang.samples && (
<CodeTabs
className="openapi-tabs__code-container-inner"
action={{
setLanguage: setLanguage,
setSelectedSample: setSelectedSample,
}}
includeSample={true}
currentLanguage={lang.language}
defaultValue={selectedSample}
languageSet={mergedLangs}
lazy
>
{lang.samples.map((sample, index) => {
return (
<CodeTab
value={sample}
label={
lang.samplesLabels
? lang.samplesLabels[index]
: sample
}
key={`${lang.language}-${lang.sample}`}
attributes={{
className: `openapi-tabs__code-item--sample`,
}}
>
{/* @ts-ignore */}
<ApiCodeBlock
language={lang.highlight}
className="openapi-explorer__code-block"
showLineNumbers={true}
>
{codeSampleCodeText}
</ApiCodeBlock>
</CodeTab>
);
})}
</CodeTabs>
)}
<CodeTabs
className="openapi-tabs__code-container-inner"
action={{
setLanguage: setLanguage,
setSelectedVariant: setSelectedVariant,
}}
includeVariant={true}
currentLanguage={lang.language}
defaultValue={selectedVariant}
languageSet={mergedLangs}
lazy
>
{lang.variants.map((variant, index) => {
return (
<CodeTab
value={variant.toLowerCase()}
label={variant.toUpperCase()}
key={`${lang.language}-${lang.variant}`}
attributes={{
className: `openapi-tabs__code-item--variant`,
}}
>
{/* @ts-ignore */}
<ApiCodeBlock
language={lang.highlight}
className="openapi-explorer__code-block"
showLineNumbers={true}
>
{codeText}
</ApiCodeBlock>
</CodeTab>
);
})}
</CodeTabs>
</CodeTab>
);
})}
</CodeTabs>
</>
);
}
export default CodeSnippets;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
import find from "lodash/find";
import isArray from "lodash/isArray";
import mergeWith from "lodash/mergeWith";
import unionBy from "lodash/unionBy";
import { CodeSample, Language } from "./code-snippets-types";
export function mergeCodeSampleLanguage(
languages: Language[],
codeSamples: CodeSample[]
): Language[] {
return languages.map((language) => {
const languageCodeSamples = codeSamples.filter(
({ lang }) => lang === language.codeSampleLanguage
);
if (languageCodeSamples.length) {
const samples = languageCodeSamples.map(({ lang }) => lang);
const samplesLabels = languageCodeSamples.map(
({ label, lang }) => label || lang
);
const samplesSources = languageCodeSamples.map(({ source }) => source);
return {
...language,
sample: samples[0],
samples,
samplesSources,
samplesLabels,
};
}
return language;
});
}
export const mergeArraysbyLanguage = (arr1: any, arr2: any) => {
const mergedArray = unionBy(arr1, arr2, "language");
return mergedArray.map((item: any) => {
const matchingItems = [
find(arr1, ["language", item["language"]]),
find(arr2, ["language", item["language"]]),
];
return mergeWith({}, ...matchingItems, (objValue: any) => {
if (isArray(objValue)) {
return objValue;
}
return undefined;
});
});
};
export function getCodeSampleSourceFromLanguage(language: Language) {
if (
language &&
language.sample &&
language.samples &&
language.samplesSources
) {
const sampleIndex = language.samples.findIndex(
(smp) => smp === language.sample
);
return language.samplesSources[sampleIndex];
}
return "";
}

View File

@@ -0,0 +1,307 @@
:root {
--bash-background-color: transparent;
--bash-border-radius: none;
--code-tab-logo-width: 24px;
--code-tab-logo-height: 24px;
}
[data-theme="dark"] {
--bash-background-color: lightgrey;
--bash-border-radius: 20px;
}
.openapi-tabs__code-container {
margin-bottom: 1rem;
&:not(.openapi-tabs__code-container-inner) {
padding: 1rem;
background-color: var(--ifm-pre-background);
border-radius: var(--ifm-global-radius);
border: 1px solid var(--openapi-explorer-border-color);
box-shadow:
0 2px 3px hsla(222, 8%, 43%, 0.1),
0 8px 16px -10px hsla(222, 8%, 43%, 0.2);
transition: 300ms;
&:hover {
box-shadow:
0 0 0 2px rgba(38, 53, 61, 0.15),
0 2px 3px hsla(222, 8%, 43%, 0.15),
0 16px 16px -10px hsla(222, 8%, 43%, 0.2);
}
}
.openapi-tabs__code-item {
display: flex;
flex-direction: column-reverse;
flex: 0 0 80px;
align-items: center;
padding: 0.5rem 0 !important;
margin-top: 0 !important;
margin-right: 0.5rem;
border: 1px solid transparent;
transition: 300ms;
&:not(.active):hover {
border: 1px solid var(--openapi-code-tab-border-color);
}
&:hover {
background-color: transparent;
}
span {
padding-top: 0.5rem;
color: var(--ifm-font-color-secondary);
font-size: 10px;
text-transform: uppercase;
}
}
}
.openapi-tabs__code-list-container {
display: flex;
justify-content: flex-start;
padding: 0.25rem;
padding-bottom: 0.6rem;
}
.openapi-tabs__code-content {
margin-top: unset !important;
}
.openapi-explorer__code-block code {
max-height: 200px;
font-size: var(--openapi-explorer-font-size-code);
padding-top: var(--ifm-pre-padding);
}
body[class="ReactModal__Body--open"] {
.openapi-explorer__code-block code {
max-height: 600px;
}
}
.openapi-tabs__code-item--variant {
color: var(--ifm-color-secondary);
&.active {
border-color: var(--ifm-toc-border-color);
}
}
.openapi-tabs__code-item--variant > span {
padding-top: unset !important;
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
}
.openapi-tabs__code-item--sample {
color: var(--ifm-color-secondary);
&.active {
border-color: var(--ifm-toc-border-color);
}
}
.openapi-tabs__code-item--sample > span {
padding-top: unset !important;
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
}
.openapi-tabs__code-item--python {
color: var(--ifm-color-success);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/python/python-original.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-python);
border-color: var(--openapi-code-tab-border-color-python);
}
}
.openapi-tabs__code-item--go {
color: var(--ifm-color-info);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/go/go-original-wordmark.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-go);
border-color: var(--openapi-code-tab-border-color-go);
}
}
.openapi-tabs__code-item--javascript {
color: var(--ifm-color-warning);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/javascript/javascript-original.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-js);
border-color: var(--openapi-code-tab-border-color-js);
}
}
.openapi-tabs__code-item--bash {
color: var(--ifm-color-danger);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/bash/bash-plain.svg")
no-repeat;
margin-block: auto;
background-color: var(--bash-background-color);
border-radius: var(--bash-border-radius);
}
&.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-bash);
border-color: var(--ifm-color-danger);
}
}
.openapi-tabs__code-item--ruby {
color: var(--ifm-color-danger);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/ruby/ruby-plain.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-ruby);
border-color: var(--openapi-code-tab-border-color-ruby);
}
}
.openapi-tabs__code-item--csharp {
color: var(--ifm-color-gray-500);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/csharp/csharp-original.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-csharp);
border-color: var(--openapi-code-tab-border-color-csharp);
}
}
.openapi-tabs__code-item--nodejs {
color: var(--ifm-color-success);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/nodejs/nodejs-original.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-nodejs);
border-color: var(--openapi-code-tab-border-color-nodejs);
}
}
.openapi-tabs__code-item--php {
color: var(--ifm-color-gray-500);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/php/php-original.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-php);
border-color: var(--openapi-code-tab-border-color-php);
}
}
.openapi-tabs__code-item--java {
color: var(--ifm-color-warning);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/java/java-original.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-java);
border-color: var(--openapi-code-tab-border-color-java);
}
}
.openapi-tabs__code-item--powershell {
color: var(--ifm-color-info);
&::after {
content: "";
width: var(--code-tab-logo-width);
height: var(--code-tab-logo-height);
background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/windows8/windows8-original.svg")
no-repeat;
margin-block: auto;
}
&.active {
box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-powershell);
border-color: var(--openapi-code-tab-border-color-powershell);
}
}
@media only screen and (min-width: 768px) and (max-width: 996px) {
.openapi-tabs__code-list {
justify-content: space-around;
}
}
.ReactModal__Body--open {
overflow: hidden !important;
}
.openapi-modal--open {
background-color: rgba(0, 0, 0, 0.7) !important;
}

View File

@@ -0,0 +1,206 @@
import React, { cloneElement, ReactElement } from "react";
import {
sanitizeTabsChildren,
type TabProps,
useScrollPositionBlocker,
useTabs,
} from "@docusaurus/theme-common/internal";
import { TabItemProps } from "@docusaurus/theme-common/lib/utils/tabsUtils";
import useIsBrowser from "@docusaurus/useIsBrowser";
import { Language } from "@theme/ApiExplorer/CodeSnippets";
import clsx from "clsx";
export interface Props {
action: {
[key: string]: React.Dispatch<any>;
};
currentLanguage: Language;
languageSet: Language[];
includeVariant: boolean;
}
export interface CodeTabsProps extends Props, TabProps {
includeSample?: boolean;
}
function TabList({
action,
currentLanguage,
languageSet,
includeVariant,
includeSample,
className,
block,
selectedValue,
selectValue,
tabValues,
}: CodeTabsProps & ReturnType<typeof useTabs>) {
const tabRefs: (HTMLLIElement | null)[] = [];
const { blockElementScrollPositionUntilNextRender } =
useScrollPositionBlocker();
const handleTabChange = (
event:
| React.FocusEvent<HTMLLIElement>
| React.MouseEvent<HTMLLIElement>
| React.KeyboardEvent<HTMLLIElement>
) => {
const newTab = event.currentTarget;
const newTabIndex = tabRefs.indexOf(newTab);
const newTabValue = tabValues[newTabIndex]!.value;
if (newTabValue !== selectedValue) {
blockElementScrollPositionUntilNextRender(newTab);
selectValue(newTabValue);
}
if (action) {
let newLanguage: Language;
if (currentLanguage && includeVariant) {
newLanguage = languageSet.filter(
(lang: Language) => lang.language === currentLanguage
)[0];
newLanguage.variant = newTabValue;
action.setSelectedVariant(newTabValue.toLowerCase());
} else if (currentLanguage && includeSample) {
newLanguage = languageSet.filter(
(lang: Language) => lang.language === currentLanguage
)[0];
newLanguage.sample = newTabValue;
action.setSelectedSample(newTabValue);
} else {
newLanguage = languageSet.filter(
(lang: Language) => lang.language === newTabValue
)[0];
action.setSelectedVariant(newLanguage.variant.toLowerCase());
action.setSelectedSample(newLanguage.sample);
}
action.setLanguage(newLanguage);
}
};
const handleKeydown = (event: React.KeyboardEvent<HTMLLIElement>) => {
let focusElement: HTMLLIElement | null = null;
switch (event.key) {
case "Enter": {
handleTabChange(event);
break;
}
case "ArrowRight": {
const nextTab = tabRefs.indexOf(event.currentTarget) + 1;
focusElement = tabRefs[nextTab] ?? tabRefs[0]!;
break;
}
case "ArrowLeft": {
const prevTab = tabRefs.indexOf(event.currentTarget) - 1;
focusElement = tabRefs[prevTab] ?? tabRefs[tabRefs.length - 1]!;
break;
}
default:
break;
}
focusElement?.focus();
};
return (
<ul
role="tablist"
aria-orientation="horizontal"
className={clsx(
"tabs",
"openapi-tabs__code-list-container",
{
"tabs--block": block,
},
className
)}
>
{tabValues.map(({ value, label, attributes }) => (
<li
// TODO extract TabListItem
role="tab"
tabIndex={selectedValue === value ? 0 : -1}
aria-selected={selectedValue === value}
key={value}
ref={(tabControl) => tabRefs.push(tabControl)}
onKeyDown={handleKeydown}
onClick={handleTabChange}
{...attributes}
className={clsx(
"tabs__item",
"openapi-tabs__code-item",
attributes?.className as string,
{
active: selectedValue === value,
}
)}
>
<span>{label ?? value}</span>
</li>
))}
</ul>
);
}
function TabContent({
lazy,
children,
selectedValue,
}: CodeTabsProps & ReturnType<typeof useTabs>): React.JSX.Element | null {
const childTabs = (Array.isArray(children) ? children : [children]).filter(
Boolean
) as ReactElement<TabItemProps>[];
if (lazy) {
const selectedTabItem = childTabs.find(
(tabItem) => tabItem.props.value === selectedValue
);
if (!selectedTabItem) {
// fail-safe or fail-fast? not sure what's best here
return null;
}
return cloneElement(selectedTabItem, { className: "margin-top--md" });
}
return (
<div className="margin-top--md openapi-tabs__code-content">
{childTabs.map((tabItem, i) =>
cloneElement(tabItem, {
key: i,
hidden: tabItem.props.value !== selectedValue,
})
)}
</div>
);
}
function TabsComponent(props: CodeTabsProps & Props): React.JSX.Element {
const tabs = useTabs(props);
const { className } = props;
return (
<div
className={clsx("tabs-container openapi-tabs__code-container", className)}
>
<TabList {...props} {...tabs} />
<TabContent {...props} {...tabs} />
</div>
);
}
export default function CodeTabs(
props: CodeTabsProps & Props
): React.JSX.Element {
const isBrowser = useIsBrowser();
return (
<TabsComponent
// Remount tabs after hydration
// Temporary fix for https://github.com/facebook/docusaurus/issues/5653
key={String(isBrowser)}
{...props}
>
{sanitizeTabsChildren(props.children)}
</TabsComponent>
);
}

View File

@@ -0,0 +1,31 @@
import React from "react";
import FormItem from "@theme/ApiExplorer/FormItem";
import FormSelect from "@theme/ApiExplorer/FormSelect";
import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
import { setContentType } from "./slice";
function ContentType() {
const value = useTypedSelector((state: any) => state.contentType.value);
const options = useTypedSelector((state: any) => state.contentType.options);
const dispatch = useTypedDispatch();
if (options.length <= 1) {
return null;
}
return (
<FormItem label="Content-Type">
<FormSelect
value={value}
options={options}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setContentType(e.target.value))
}
/>
</FormItem>
);
}
export default ContentType;

View File

@@ -0,0 +1,22 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface State {
value: string;
options: string[];
}
const initialState: State = {} as any;
export const slice = createSlice({
name: "contentType",
initialState,
reducers: {
setContentType: (state, action: PayloadAction<string>) => {
state.value = action.payload;
},
},
});
export const { setContentType } = slice.actions;
export default slice.reducer;

View File

@@ -0,0 +1,40 @@
import React from "react";
import fileSaver from "file-saver";
const saveFile = (url: string) => {
let fileName;
if (url.endsWith("json") || url.endsWith("yaml") || url.endsWith("yml")) {
fileName = url.substring(url.lastIndexOf("/") + 1);
}
fileSaver.saveAs(url, fileName ? fileName : "openapi.txt");
};
function Export({ url, proxy }: any) {
return (
<div
style={{ float: "right" }}
className="dropdown dropdown--hoverable dropdown--right"
>
<button className="export-button button button--sm button--secondary">
Export
</button>
<ul className="export-dropdown dropdown__menu">
<li>
<a
onClick={(e) => {
e.preventDefault();
saveFile(`${url}`);
}}
className="dropdown__link"
href={`${url}`}
>
OpenAPI Spec
</a>
</li>
</ul>
</div>
);
}
export default Export;

View File

@@ -0,0 +1,27 @@
.openapi-explorer__floating-btn {
position: relative;
button {
position: relative;
background: var(--ifm-color-emphasis-900);
border: none;
border-radius: var(--ifm-global-radius);
color: var(--ifm-color-emphasis-100);
cursor: pointer;
padding: 0.4rem 0.5rem;
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s ease-in-out,
visibility 0.2s ease-in-out,
bottom 0.2s ease-in-out;
position: absolute;
right: calc(var(--ifm-pre-padding) / 2);
}
}
.openapi-explorer__floating-btn:hover button,
.openapi-explorer__floating-btn:focus-visible button,
.openapi-explorer__floating-btn button:focus-visible {
visibility: visible;
opacity: 1;
}

View File

@@ -0,0 +1,22 @@
import React from "react";
export interface Props {
label?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
children?: React.ReactNode;
}
function FloatingButton({ label, onClick, children }: Props) {
return (
<div tabIndex={0} className="openapi-explorer__floating-btn">
{label && (
<button tabIndex={0} onClick={onClick}>
{label}
</button>
)}
{children}
</div>
);
}
export default FloatingButton;

View File

@@ -0,0 +1,72 @@
.openapi-explorer__dropzone {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 2px dashed var(--openapi-monaco-border-color);
background-color: var(--openapi-input-background);
width: 100%;
border-radius: 4px;
padding: var(--ifm-pre-padding);
font-size: var(--ifm-code-font-size);
&:hover {
border: 2px dashed var(--ifm-color-primary);
background: linear-gradient(
var(--openapi-dropzone-hover-shim),
var(--openapi-dropzone-hover-shim)
),
linear-gradient(var(--ifm-color-primary), var(--ifm-color-primary));
.openapi-explorer__dropzone-content {
color: var(--ifm-pre-color);
}
}
}
.openapi-explorer__dropzone-hover {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 2px dashed var(--openapi-monaco-border-color);
background-color: var(--openapi-input-background);
width: 100%;
border-radius: 4px;
padding: var(--ifm-pre-padding);
font-size: var(--ifm-code-font-size);
border: 2px dashed var(--ifm-color-primary);
background: linear-gradient(
var(--openapi-dropzone-hover-shim),
var(--openapi-dropzone-hover-shim)
),
linear-gradient(var(--ifm-color-primary), var(--ifm-color-primary));
.openapi-explorer__dropzone-content {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
margin: var(--ifm-pre-padding) 0;
color: var(--ifm-pre-color);
}
.openapi-explorer__file-name {
margin: 0 calc(var(--ifm-pre-padding) * 1.5);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
}
.openapi-explorer__dropzone-content {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
margin: var(--ifm-pre-padding) 0;
color: var(--openapi-dropzone-color);
}

View File

@@ -0,0 +1,112 @@
import React, { useState } from "react";
import FloatingButton from "@theme/ApiExplorer/FloatingButton";
import MagicDropzone from "react-magic-dropzone";
type PreviewFile = { preview: string } & File;
interface RenderPreviewProps {
file: PreviewFile;
}
function RenderPreview({ file }: RenderPreviewProps) {
switch (file.type) {
case "image/png":
case "image/jpeg":
case "image/jpg":
case "image/svg+xml":
return (
<img
style={{
borderRadius: "4px",
}}
src={file.preview}
alt=""
/>
);
default:
return (
<div
style={{
display: "flex",
alignItems: "center",
minWidth: 0,
}}
>
<svg viewBox="0 0 100 120" style={{ width: "50px", height: "60px" }}>
<path
fillRule="evenodd"
fill="#b3beca"
d="M100.000,39.790 L100.000,105.000 C100.000,113.284 93.284,120.000 85.000,120.000 L15.000,120.000 C6.716,120.000 -0.000,113.284 -0.000,105.000 L-0.000,15.000 C-0.000,6.716 6.716,-0.000 15.000,-0.000 L60.210,-0.000 L100.000,39.790 Z"
/>
<path
fillRule="evenodd"
fill="#90a1b1"
transform="translate(60, 0)"
d="M0.210,-0.000 L40.000,39.790 L40.000,40.000 L15.000,40.000 C6.716,40.000 0.000,33.284 0.000,25.000 L0.000,-0.000 L0.210,-0.000 Z"
/>
</svg>
<div className="openapi-explorer__file-name">{file.name}</div>
</div>
);
}
}
export interface Props {
placeholder: string;
onChange?(file?: File): any;
}
function FormFileUpload({ placeholder, onChange }: Props) {
const [hover, setHover] = useState(false);
const [file, setFile] = useState<PreviewFile>();
function setAndNotifyFile(file?: PreviewFile) {
setFile(file);
onChange?.(file);
}
function handleDrop(accepted: PreviewFile[]) {
const [file] = accepted;
setAndNotifyFile(file);
setHover(false);
}
return (
<FloatingButton>
<MagicDropzone
className={
hover
? "openapi-explorer__dropzone-hover"
: "openapi-explorer__dropzone"
}
onDrop={handleDrop}
onDragEnter={() => setHover(true)}
onDragLeave={() => setHover(false)}
multiple={false}
style={{ marginTop: "calc(var(--ifm-pre-padding) / 2)" }}
>
{file ? (
<>
<button
style={{ marginTop: "calc(var(--ifm-pre-padding) / 2)" }}
onClick={(e) => {
e.stopPropagation();
setAndNotifyFile(undefined);
}}
>
Clear
</button>
<RenderPreview file={file} />
</>
) : (
<div className="openapi-explorer__dropzone-content">
{placeholder}
</div>
)}
</MagicDropzone>
</FloatingButton>
);
}
export default FormFileUpload;

View File

@@ -0,0 +1,21 @@
.openapi-explorer__form-item {
padding: var(--openapi-explorer-padding-input);
font-size: var(--openapi-explorer-font-size-input);
&:first-child {
margin-top: 0;
}
.required {
color: var(--openapi-required);
}
}
.openapi-explorer__form-item-body-container {
padding: 0;
}
.openapi-explorer__form-item-label {
font-family: var(--ifm-font-family-monospace);
font-weight: bold;
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import clsx from "clsx";
export interface Props {
label?: string;
type?: string;
required?: boolean | undefined;
children?: React.ReactNode;
className?: string;
}
function FormItem({ label, type, required, children, className }: Props) {
return (
<div className={clsx("openapi-explorer__form-item", className)}>
{label && (
<label className="openapi-explorer__form-item-label">{label}</label>
)}
{type && <span style={{ opacity: 0.6 }}> {type}</span>}
{required && <span className="openapi-schema__required">required</span>}
<div>{children}</div>
</div>
);
}
export default FormItem;

View File

@@ -0,0 +1,30 @@
.openapi-explorer__multi-select-input {
width: 100%;
margin-top: calc(var(--ifm-pre-padding) / 2);
padding: 1rem;
border-radius: 4px;
border: 1px solid transparent;
background-color: var(--openapi-input-background);
outline: none;
font-size: var(--openapi-explorer-font-size-input);
color: var(--ifm-pre-color);
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
&:focus {
border: 1px solid var(--openapi-input-border);
}
&.error {
border: 1px solid var(--ifm-color-danger);
}
option {
border-radius: 0.25rem;
color: var(--ifm-menu-color);
margin: 0.25rem 0;
padding: var(--ifm-menu-link-padding-vertical)
var(--ifm-menu-link-padding-horizontal);
}
}

View File

@@ -0,0 +1,50 @@
import React from "react";
import clsx from "clsx";
export interface Props {
value?: string;
options: string[];
onChange?: React.ChangeEventHandler<HTMLSelectElement>;
showErrors?: boolean;
}
function FormMultiSelect({ value, options, onChange, showErrors }: Props) {
if (options.length === 0) {
return null;
}
let height;
if (options.length < 6) {
const selectPadding = 12 * 2;
const rawHeight = options.length * 29;
const innerMargins = 4 * options.length - 1;
const outerMargins = 4 * 2;
const mysteryScroll = 1;
height =
rawHeight + innerMargins + outerMargins + selectPadding + mysteryScroll;
}
return (
<select
style={{ height: height }}
className={clsx("openapi-explorer__multi-select-input", {
error: showErrors,
})}
value={value}
onChange={onChange}
size={Math.min(6, options.length + 1)}
multiple
>
{options.map((option) => {
return (
<option key={option} value={option}>
{option}
</option>
);
})}
</select>
);
}
export default FormMultiSelect;

View File

@@ -0,0 +1,43 @@
html[data-theme="dark"] .openapi-explorer__select-input {
margin-top: calc(var(--ifm-pre-padding) / 2);
background-color: var(--openapi-input-background);
border: none;
outline: none;
width: 100%;
color: var(--ifm-pre-color);
border-radius: 4px;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg focusable="false" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" fill="white"><path d="M8 11L3 6 3.7 5.3 8 9.6 12.3 5.3 13 6z"></path></svg>');
background-repeat: no-repeat;
background-position: right var(--ifm-pre-padding) top 50%;
background-size: auto auto;
}
.openapi-explorer__select-input {
width: 100%;
margin-top: calc(var(--ifm-pre-padding) / 2);
padding: var(--openapi-explorer-padding-input);
border: none;
outline: none;
border-radius: 4px;
background-color: var(--openapi-input-background);
font-size: var(--openapi-explorer-font-size-input);
font-family: var(--ifm-font-family-monospace);
color: var(--ifm-pre-color);
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,<svg focusable="false" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 11L3 6 3.7 5.3 8 9.6 12.3 5.3 13 6z"></path></svg>');
background-repeat: no-repeat;
background-position: right var(--ifm-pre-padding) top 50%;
background-size: auto auto;
&:focus {
box-shadow: inset 0px 0px 0px 2px var(--openapi-input-border);
}
}

View File

@@ -0,0 +1,31 @@
import React from "react";
export interface Props {
value?: string;
options?: string[];
onChange?: React.ChangeEventHandler<HTMLSelectElement>;
}
function FormSelect({ value, options, onChange }: Props) {
if (!Array.isArray(options) || options.length === 0) {
return null;
}
return (
<select
className="openapi-explorer__select-input"
value={value}
onChange={onChange}
>
{options.map((option) => {
return (
<option key={option} value={option}>
{option}
</option>
);
})}
</select>
);
}
export default FormSelect;

View File

@@ -0,0 +1,34 @@
.openapi-explorer__form-item-input {
margin-top: calc(var(--ifm-pre-padding) / 2);
background-color: var(--openapi-input-background);
border: 1px solid transparent;
outline: none;
width: 100%;
color: var(--ifm-pre-color);
padding: var(--openapi-explorer-padding-input);
border-radius: 4px;
&:hover {
border: 1px solid var(--ifm-toc-border-color);
}
&:focus {
border: 1px solid var(--ifm-color-primary);
box-shadow: none;
}
&.error {
border: 1px solid var(--openapi-required);
}
}
.openapi-explorer__input-error {
font-size: var(--openapi-explorer-font-size-input);
color: var(--openapi-required);
padding-top: var(--openapi-explorer-padding-input);
&::before {
display: inline;
content: "";
}
}

Some files were not shown because too many files have changed in this diff Show More