Merge main

This commit is contained in:
tyler-mairose-sp
2023-06-21 11:06:06 -04:00
268 changed files with 25261 additions and 14675 deletions

View File

@@ -70,7 +70,7 @@ module.exports = {
{
position: 'left',
label: 'Blog',
to: 'https://medium.com/sailpointengineering',
to: '/blog',
},
{
position: 'left',

50
package-lock.json generated
View File

@@ -19,7 +19,10 @@
"docusaurus-theme-openapi-docs": "^1.5.1",
"prism-react-renderer": "^1.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-markdown": "^8.0.7",
"react-spinners": "^0.13.8",
"react-tabs": "^4.3.0"
},
"devDependencies": {
"@docusaurus/core": "2.2.0",
@@ -11024,8 +11027,9 @@
"license": "MIT"
},
"node_modules/react-markdown": {
"version": "8.0.5",
"license": "MIT",
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz",
"integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==",
"dependencies": {
"@types/hast": "^2.0.0",
"@types/prop-types": "^15.0.0",
@@ -11302,6 +11306,27 @@
"react": ">=15"
}
},
"node_modules/react-spinners": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz",
"integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-tabs": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-4.3.0.tgz",
"integrity": "sha512-2GfoG+f41kiBIIyd3gF+/GRCCYtamC8/2zlAcD8cqQmqI9Q+YVz7fJLHMmU9pXDVYYHpJeCgUSBJju85vu5q8Q==",
"dependencies": {
"clsx": "^1.1.0",
"prop-types": "^15.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-0 || ^18.0.0"
}
},
"node_modules/react-textarea-autosize": {
"version": "8.4.0",
"dev": true,
@@ -21693,7 +21718,9 @@
"version": "1.0.1"
},
"react-markdown": {
"version": "8.0.5",
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz",
"integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==",
"requires": {
"@types/hast": "^2.0.0",
"@types/prop-types": "^15.0.0",
@@ -21858,6 +21885,21 @@
"tiny-warning": "^1.0.0"
}
},
"react-spinners": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz",
"integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==",
"requires": {}
},
"react-tabs": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-4.3.0.tgz",
"integrity": "sha512-2GfoG+f41kiBIIyd3gF+/GRCCYtamC8/2zlAcD8cqQmqI9Q+YVz7fJLHMmU9pXDVYYHpJeCgUSBJju85vu5q8Q==",
"requires": {
"clsx": "^1.1.0",
"prop-types": "^15.5.0"
}
},
"react-textarea-autosize": {
"version": "8.4.0",
"dev": true,

View File

@@ -32,7 +32,10 @@
"docusaurus-theme-openapi-docs": "^1.5.1",
"prism-react-renderer": "^1.3.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-markdown": "^8.0.7",
"react-spinners": "^0.13.8",
"react-tabs": "^4.3.0"
},
"overrides": {
"mermaid": "9.1.7"

View File

@@ -153,7 +153,7 @@ This document covers these three common flows:
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.
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 haev the supported OAuth flows listed under the 'Authorization' dropdown, like the [List Access Profiles endpoint](https://developer.sailpoint.com/idn/api/beta/list-access-profiles):
The endpoint will have the supported OAuth flows listed under the 'Authorization' dropdown, like the [List Access Profiles endpoint](https://developer.sailpoint.com/idn/api/beta/list-access-profiles):
![Authorization Dropdown](./img/authorization/authorization-dropdown.png)

View File

@@ -36,7 +36,10 @@ If there are no changes, you're up to date. If there are changes, the screen lis
## Configure your environment
The SailPoint workspace provides an environment, a set of variables you can use in your requests, that you can fork and pull changes from to stay up to date the same way you can with collections.
The SailPoint workspace provides an environment, a set of variables you can use in your requests, that you can fork and pull changes from to stay up to date the same way you can with collections. To import the environment into your workspace, select 'Run in Postman'.
| Environment | [![Run in Postman](./img/button.svg)](https://www.postman.com/sailpoint/workspace/identitynow/environment/23226990-ed571d4f-37a3-4a2c-9105-5d8d8cce1d20/fork) |
|------|----------------------------|
To send API requests in Postman, you must authenticate to the APIs. To authenticate to the APIs, you must specify these variables in your Postman environment:

View File

@@ -93,7 +93,7 @@ if( startDate.before( today ) && endDate.after( today ) ) {
return "inactive";
}
// If we haven't calculated a state already; return null.
// If we haven't calculated a state already, return null.
return null;
]]></Source>

View File

@@ -15,6 +15,12 @@ tags: ['Rules']
In SailPoint solutions, rules serve as a flexible configuration framework implementers can leverage to preform complex or advanced configurations. Though rules allow some advanced flexibility, you must take special considerations when you are deciding to implement rules.
## Java Beanshell
Rules in IdentityNow are written in Java Beanshell, a lightweight scripting language that allows you to define custom logic and behavior within the rules. Java Beanshell provides a familiar syntax similar to Java, making it easier for implementers to create and maintain rule configurations.
For more information about Java Beanshell, you can refer to the [Java Beanshell Documentation](https://github.com/beanshell/beanshell/wiki).
## Rule Execution
IdentityNow (IDN) is a multi-tenant cloud solution, and its architecture varies differently from other SailPoint products like IdentityIQ (IIQ). Therefore, the way rules execute within IDN reflects the architectural design considerations the platform was built on. These considerations determine the rule's limitations.

View File

@@ -11,9 +11,16 @@ tags: ['Connectivity', 'Connector Command']
| Input/Output | Data Type |
| :----------- | :------------------: |
| Input | undefined |
| Input | StdAccountListInput |
| Output | StdAccountListOutput |
### Example StdAccountListInput
```javascript
"state": {"date": "1686341338871"},
"stateful": true
```
### Example StdAccountListOutput
```javascript
@@ -70,6 +77,12 @@ IDN will throw a connection timeout error if your connector doesn't respond with
:::
:::caution Important
IDN supports [delta aggregation](#delta-aggregation-state). If your source has a large number of accounts that will be syncronized with IDN, then it is highly recommended to utilize [delta aggregation](#delta-aggregation-state) for the source.
:::
The following code snippet from [index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts) shows how to register the account list command on the connector object:
```javascript
@@ -162,3 +175,60 @@ The result of the account list command is not an array of objects but several in
}
}
```
## Delta Aggregation (State)
If your source can keep track of changes to the data in some way, then delta aggregation can be performed on a source. In order to implement, there are a few things that need to be configured
1. In your connector-spec.json file, the feature needs to be enabled by adding the following key: ```"supportsStatefulCommands": true,``` and in the sourceConfig section, a checkbox needs to be added to enable state with the key ```spConnEnableStatefulCommands```:
```javascript
"supportsStatefulCommands": true,
...
{
"key": "spConnEnableStatefulCommands",
"label": "Stateful",
"required": true,
"type": "checkbox"
}
```
2. In the ```stdAccountList``` command, when you are done sending accounts, you need to also send the state to IDN so it knows where to start the next time it sends a list request:
```javascript
const state = {"data": Date.now().toString()}
...
res.saveState(state)
```
In the above example, I am capturing the date, but you can use any value you want to store the state
:::caution Important
The state that you send using the ```saveState``` command MUST be a json object, and it is recommend to only save strings to ensure proper serialization/deserialization of the data. You cannot send a simple string or number or it will not properly save the state.
:::
3. In the ```stdAccountList``` command, you need to properly handle the state object. Something like below checks the stateful boolean as well as the state object and fetches accounts accordingly:
```javascript
.stdAccountList(async (context: Context, input: StdAccountListInput, res: Response<StdAccountListOutput>) => {
let accounts = []
const state = {"data": Date.now().toString()}
if (!input.state && input.stateful) {
logger.info(input, "No state provided, fetching all accounts")
accounts = await airtable.getAllAccounts()
} else if (input.state && input.stateful) {
logger.info(input ,"Current state provided, only fetching accounts after that state")
accounts = await airtable.getAllStatefulAccounts(new Date(Number(input.state?.data)))
} else {
logger.info(input.state ,"Source is not stateful, getting all acounts")
accounts = await airtable.getAllAccounts()
}
logger.info(accounts, "fetched the following accounts from Airtable")
for (const account of accounts) {
res.send(account.toStdAccountListOutput())
}
res.saveState(state)
})
```

View File

@@ -108,4 +108,67 @@ private buildStandardObject(): StdEntitlementReadOutput | StdEntitlementListOutp
IDN will throw a connection timeout error if your connector doesn't respond within 3 minutes, and there are memory limitations involved with aggregating data. To prevent large memory utilization or timeout errors, you should set up your connectors to send data to IDN as it's retrieved from your source system. For more details and an example, refer to [Connector Timeouts](../in-depth/connector-timeouts.md).
:::
:::
:::caution Important
IDN supports [delta aggregation](#delta-aggregation-state). If your source has a large number of entitlements that will be syncronized with IDN, then it is highly recommended to utilize [delta aggregation](#delta-aggregation-state) for the source.
:::
## Delta Aggregation (State)
If your source can keep track of changes to the data in some way, then delta aggregation can be performed on a source. In order to implement, there are a few things that need to be configured
1. In your connector-spec.json file, the feature needs to be enabled by adding the following key: ```"supportsStatefulCommands": true,``` and in the sourceConfig section, a checkbox needs to be added to enable state with the key ```spConnEnableStatefulCommands```:
```javascript
"supportsStatefulCommands": true,
...
{
"key": "spConnEnableStatefulCommands",
"label": "Stateful",
"required": true,
"type": "checkbox"
}
```
2. In the ```stdEntitlementList``` command, when you are done sending entitlments, you need to also send the state to IDN so it knows where to start the next time it sends a list request:
```javascript
const state = {"data": Date.now().toString()}
...
res.saveState(state)
```
In the above example, I am capturing the date, but you can use any value you want to store the state
:::caution Important
The state that you send using the ```saveState``` command MUST be a json object, and it is recommend to only save strings to ensure proper serialization/deserialization of the data. You cannot send a simple string or number or it will not properly save the state.
:::
3. In the ```stdEntitlementList``` command, you need to properly handle the state object. Something like below checks the stateful boolean as well as the state object and fetches accounts accordingly:
```javascript
.stdEntitlementList(async (context: Context, input: StdEntitlementListInput, res: Response<StdEntitlementListOutput>) => {
let groups = []
const state = {"data": Date.now().toString()}
if (!input.state && input.stateful) {
logger.info(input, "No state provided, fetching all entitlements")
const groups = await airtable.getAllEntitlements()
} else if (input.state && input.stateful) {
logger.info(input ,"Current state provided, only fetching entitlements after that state")
const groups = await airtable.getAllStatefulEntitlements(new Date(Number(input.state?.data)))
} else {
logger.info(input.state ,"Source is not stateful, getting all entitlements")
const groups = await airtable.getAllEntitlements()
}
logger.info(groups, "fetched the following entitlements in Airtable")
for (const group of groups) {
res.send(group.toStdEntitlementListOutput())
}
res.saveState(state)
})
```

View File

@@ -29,7 +29,7 @@ The following describes in detail the different fields in the connector spec:
- For example, the stdAccountRead command input is the StdAccountReadInput. if you select keyType as “simple,” then the StdAccountReadInput.key will be the type SimpleKey.
- **commands:** The list of commands the connector supports. A full list of available commands can be found here.
- **commands:** The list of commands the connector supports. A full list of available commands can be found [here](../connector-commands/index.md).
- **sourceConfig** A list of configuration items you must provide when you create a source in IDN. The order of these items is preserved in the UI.
- **type:** This is always “menu” - it indicates a new menu for the sidebar. You can have multiple sections defined for complex connector configurations

View File

@@ -20,7 +20,7 @@ To build the CLI, the following packages are required:
To develop a connector, the following packages are required:
- Node >= 14.17.3
- Node >= 16.2.0
## IDE

View File

@@ -156,8 +156,8 @@ The following variables are available to the Apache Velocity template engine whe
| Variable | Type | Description |
| --- | --- | --- |
| identity | sailpoint.object.Identity | This is the identity the attribute promotion is performed on. |
| oldValue | Object | This is the definition of the attribute being promoted. |
| attributeDefinition | sailpoint.object.ObjectAttribute | This is the attribute's previous value. |
| attributeDefinition | sailpoint.object.ObjectAttribute | This is the definition of the attribute being promoted. |
| oldValue | Object | This is the attribute's previous value. |
### Account Profile Context

View File

@@ -0,0 +1,19 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
export default function AmbassadorBanner() {
return (
<div>
<div className={styles.imageContainer}>
<img className={styles.headerImage} src={useBaseUrl('/ambassador/ambassador_banner_template.png')}></img>
<div className={styles.ambassadorHeaderText}>
Ambassadors
</div>
</div >
</div>
);
}

View File

@@ -0,0 +1,28 @@
.ambassadorHeaderText {
position: relative;
color: #ffffff;
font-size: 48px;
max-width: 396px;
font-weight: bold;
line-height: 133%;
top: -84px;
left: 92px;
}
.background {
width: 100%;
object-fit: repeat;
height: 100%;
}
.imageContainer {
width: 100%;
height: 90px;
background: rgb(0,51,161);
background: linear-gradient(90deg, rgba(0,51,161,1) 0%, rgba(84,192,232,1) 100%);
}
.headerImage {
height: 90px;
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import styles from './styles.module.css';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import ThemedImage from '@theme/ThemedImage';
import {addDarkToFileName} from '../../../util/util';
export default function AmbassadorCard({
data
}) {
return (
<Link to={data.link} className={styles.link}>
<div className={styles.card}>
<div className={styles.cardFaceContainer}>
<img className={styles.cardFace} src={useBaseUrl(data.creatorImage)}></img>
<div className={styles.cardNameContainer}>
<div className={styles.name}>{data.name}</div>
</div>
</div>
<div className={styles.bio} dangerouslySetInnerHTML={{__html: data.bio}}></div>
<div className={styles.cardData}>
<img className={styles.cardEye} src={useBaseUrl('/icons/square-check-regular.svg')}></img>
<div className={styles.cardCommentText}>{data.answers} solutions</div>
</div>
<div className={styles.cardDataLower}>
<img className={styles.cardEye} src={useBaseUrl('/icons/calendar-clock-light.svg')}></img>
<div className={styles.cardCommentText}>member since {data.member_since}</div>
</div>
</div>
</Link>
);
}

View File

@@ -0,0 +1,136 @@
/* Getting Started Card */
.link:hover {
text-decoration: none;
}
.card {
position: relative;
margin-top: 20px;
height: 500px;
max-width: 420px;
/* UI Properties */
background: var(--dev-card-background);
box-shadow: var(--dev-card-shadow);
border: 1px solid var(--dev-card-background);
border-radius: 40px;
opacity: 1;
transition: all 0.3s;
opacity: 1;
}
.card:hover {
cursor: pointer;
transform: translate(0px, -5px);
box-shadow: var(--dev-card-selected);
}
.cardFaceContainer {
display: flex;
margin: 30px;
}
.cardFace {
border-radius: 99px;
justify-content: center;
display: flex;
height: 100px;
width: 100px;
}
.cardNameContainer {
margin-left: 15px;
display: flex;
justify-content: center;
align-items: center;
}
.name {
font-size: 25px;
font-weight: 500;
width: 100%;
top: 124px;
left: 0;
color: var(--dev-text-color-normal);
}
.titleText {
font-size: 10px;
font-weight: 400;
width: 100%;
letter-spacing: 2px;
top: 150px;
left: 0;
color: var(--dev-text-color-normal);
}
.location {
font-size: 10px;
font-weight: 500;
width: 100%;
letter-spacing: 2px;
top: 141px;
left: 0;
color: var(--ifm-color-primary);
}
.cardArrow {
position: absolute;
margin: 20px;
bottom: 5px;
right: 0;
width: 20px;
}
.bio {
position: absolute;
margin: 30px auto;
font-size: 16px;
font-weight: 500;
width: 100%;
top: 100px;
left: 0;
color: var(--ifm-color-primary);
padding: 30px;
}
.cardData {
position: absolute;
bottom: 50px;
left: 20px;
display: flex;
align-items: center;
}
.cardEye {
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
margin-left: 5px;
height: 18px;
width: 18px;
}
.cardCommentText {
color: #96a9ba;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
margin-left: 5px;
margin-bottom: 5px;
height: 20px;
font-size: 16px;
}
.cardDataLower {
position: absolute;
bottom: 20px;
left: 22px;
display: flex;
align-items: center;
}

View File

@@ -0,0 +1,109 @@
import React from 'react';
import styles from './styles.module.css';
import AmbassadorCard from '../AmbassadorCard';
import BounceLoader from 'react-spinners/BounceLoader';
import {getAmbassadors, getAmbassadorDetails} from '../../../services/DiscourseService';
export default function AmbassadorCards({
expert
}) {
const [cardData, setCardData] = React.useState();
const [loadingCards, setLoadingCards] = React.useState(true);
const getPosts = async () => {
const data = await getAmbassadors(expert);
const resultset = []
if (data.members) {
const memberDetails = await getAmbassadorDetails(data.members.map(item => item.id))
for (const member of data.members) {
const memberDetail = memberDetails.users.filter(item => item.id === member.id)[0]
if (member.avatar_template.includes("developer.sailpoint.com") && memberDetail.bio_excerpt && memberDetail.bio_excerpt.length > 60 && memberDetail.accepted_answers > 0) {
resultset.push(await getMemberList(member, memberDetail))
}
}
resultset.sort((a, b) => a.date - b.date)
setCardData(resultset);
} else {
setCardData(undefined);
}
setLoadingCards(false);
};
React.useEffect(() => {
getPosts();
setCardData(undefined);
setLoadingCards(true);
}, []);
if (cardData) {
return (
<div className={styles.center}>
<div className={styles.gridContainer}>
{cardData.map(function(a, index){
return <AmbassadorCard
key={a.link}
data={a}
></AmbassadorCard>
})}
</div>
</div>
);
} else if (loadingCards) {
return (
<BounceLoader
className={styles.spinnerCenter}
color={'#0033a1'}
loading={true}
size={150}
aria-label="Loading Spinner"
data-testid="loader"
/>
);
} else {
return (
<div className={styles.noFound}>
{' '}
No Ambassadors Found with the Given Search Criteria
</div>
);
}
}
async function getMemberList(member, details) {
return {
name: member.name,
creatorImage: getavatarURL(member.avatar_template),
title: member.title,
bio: details.bio_excerpt,
member_since: new Date(member.added_at).toLocaleString('default', {month: 'long'}) + ' ' + new Date(member.added_at).toISOString().slice(0, 4),
badge_count: details.badge_count,
answers: details.accepted_answers,
location: details.location,
website: details.website_name,
link:
'https://developer.sailpoint.com/discuss/u/' +
member.username +
'/summary',
};
}
function getavatarURL(avatar) {
if (avatar.includes("developer.sailpoint.com")) {
return "https://developer.sailpoint.com" + avatar.replace("{size}", "120")
} else {
return avatar.replace("{size}", "120")
}
}
function styleExcerpt(excerpt) {
if (excerpt.length > 150) {
return excerpt.slice(0, 150) + "..."
} else {
return excerpt.replace("&hellip;", "")
}
}

View File

@@ -0,0 +1,35 @@
/* Getting Started Card container */
.gridContainer {
display: grid;
place-content: center;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
grid-gap: 40px;
margin-left: 40px;
margin-right: 40px;
}
.center {
margin: 0px auto;
max-width: 1300px;
margin-bottom: 50px;
}
.space {
height: 200px;
}
.noFound {
font-size: 28px;
font-weight: 500;
color: var(--dev-secondary-text);
padding: 8px;
margin: 50px;
}
.spinnerCenter {
margin: 100px auto;
max-width: 1300px;
margin-bottom: 50px;
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
export default function BlogBanner() {
return (
<div>
<div className={styles.imageContainer}>
<img className={styles.headerImage} src={useBaseUrl('/blog/blog_banner_template.png')}></img>
<div className={styles.blogHeaderText}>
Blog
</div>
</div >
</div>
);
}

View File

@@ -0,0 +1,28 @@
.blogHeaderText {
position: relative;
color: #ffffff;
font-size: 48px;
max-width: 396px;
font-weight: bold;
line-height: 133%;
top: -84px;
left: 59px;
}
.background {
width: 100%;
object-fit: repeat;
height: 100%;
}
.imageContainer {
width: 100%;
height: 90px;
background: rgb(0,51,161);
background: linear-gradient(90deg, rgba(0,51,161,1) 0%, rgba(84,192,232,1) 100%);
}
.headerImage {
height: 90px;
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import styles from './styles.module.css';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import ThemedImage from '@theme/ThemedImage';
import {addDarkToFileName} from '../../../util/util';
export default function BlogCard({
link,
title,
tags,
creatorImage,
image,
excerpt,
name,
views,
replies,
readTime
}) {
return (
<Link to={link}>
<div className={styles.card} >
<div className={styles.cardData}>
<img className={styles.cardEye} src={useBaseUrl('/blog/eye-regular.svg')}></img>
<div className={styles.cardCommentText}>{views}</div>
<img className={styles.cardComment} src={useBaseUrl('/blog/comment-light.svg')}></img>
<div className={styles.cardCommentText}>{replies}</div>
<img className={styles.cardComment} src={useBaseUrl('/blog/clock-light.svg')}></img>
<div className={styles.cardCommentText}>{readTime} minute read</div>
</div>
<div className={styles.cardUser}>
<img className={styles.cardFace} src={useBaseUrl(creatorImage)}></img>
<div className={styles.cardName}>{name}</div>
</div>
<div className={styles.cardText}>
<img className={styles.cardImage} src={useBaseUrl(image)}></img>
<div className={styles.cardTitle}>{title}</div>
<div className={styles.tags}>
{tags?.map((tag, index) => {
if (index > 2) {
return '';
}
return <div key={tag} className={styles.tag}>{tag}</div>;
})}
</div>
<div className={styles.cardBody}>{excerpt}</div>
</div>
</div>
</Link>
);
}

View File

@@ -0,0 +1,127 @@
/* Getting Started Card */
.card {
position: relative;
margin-top: 20px;
height: 650px;
/* UI Properties */
background: var(--dev-card-background);
box-shadow: var(--dev-card-shadow);
border: 1px solid var(--dev-card-background);
border-radius: 40px;
opacity: 1;
transition: all 0.3s;
max-width: 600px;
}
.card:hover {
cursor: pointer;
transform: translate(0px, -5px);
box-shadow: var(--dev-card-selected);
}
.cardText {
position: absolute;
margin: 22px;
min-width: 170px;
top: 10px;
left: 0;
}
.cardTitle {
font-size: 22px;
font-weight: 700;
}
.cardBody {
font-size: 16px;
font-weight: 500;
top: 10px;
left: 0;
}
.tag {
font-size: 16px;
font-weight: 500;
color: var(--dev-secondary-text);
background-color: var(--dev-tag-highlight);
padding: 0px 8px;
margin-left: 5px;
margin-top: 5px;
}
.tags {
margin: 0px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.cardUser {
position: absolute;
bottom: 10px;
left: 25px;
display: flex;
}
.cardFace {
border-radius: 9999px;
margin: auto;
height: 40px;
width: 40px;
}
.cardName {
min-width: 170px;
margin-top: 7px;
margin-left: 10px;
}
.cardData {
position: absolute;
bottom: 50px;
left: 20px;
display: flex;
align-items: center;
}
.cardEye {
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
margin-left: 5px;
height: 18px;
width: 18px;
}
.cardComment {
margin-left: 10px;
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
height: 18px;
width: 18px;
margin-bottom: 4px;
}
.cardCommentText {
color: #96a9ba;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
margin-left: 5px;
margin-bottom: 5px;
height: 20px;
font-size: 16px;
}

View File

@@ -0,0 +1,109 @@
import React from 'react';
import styles from './styles.module.css';
import BlogCard from '../BlogCard';
import BounceLoader from 'react-spinners/BounceLoader';
import {getBlogPosts, getTopic} from '../../../services/DiscourseService';
export default function BlogCards({
filterCallback
}) {
const [cardData, setCardData] = React.useState();
const [loadingCards, setLoadingCards] = React.useState(true);
const getPosts = async () => {
const data = await getBlogPosts(filterCallback.join(','));
const resultset = []
if (data.topics) {
for (const topic of data.topics) {
resultset.push(await getPostList(topic))
}
setCardData(resultset);
} else {
setCardData(undefined);
}
setLoadingCards(false);
};
React.useEffect(() => {
getPosts();
setCardData(undefined);
setLoadingCards(true);
}, [filterCallback]);
if (cardData) {
return (
<div className={styles.center}>
<div className={styles.gridContainer}>
{cardData.map(function(a, index){
return <BlogCard
key={a.link}
id={index + a.link}
excerpt={a.excerpt}
name={a.name}
tags={a.tags}
link={a.link}
image={a.image}
title={a.title}
views={a.views}
replies={a.replies}
readTime={a.readTime}
creatorImage={a.creatorImage}></BlogCard>
})}
</div>
</div>
);
} else if (loadingCards) {
return (
<BounceLoader
className={styles.spinnerCenter}
color={'#0033a1'}
loading={true}
size={150}
aria-label="Loading Spinner"
data-testid="loader"
/>
);
} else {
return (
<div className={styles.noFound}>
{' '}
No Blogposts Found with the Given Search Criteria
</div>
);
}
}
async function getPostList(topic) {
const fullTopic = await getTopic(topic.id);
return {
name: fullTopic.details.created_by.name,
excerpt: styleExcerpt(topic.excerpt),
creatorImage: getavatarURL(fullTopic.details.created_by.avatar_template),
tags: topic.tags,
image: fullTopic.image_url,
link:
'https://developer.sailpoint.com/discuss/t/' +
topic.slug +
'/' +
topic.id,
title: topic.title,
views: fullTopic.views,
liked: topic.like_count,
replies: fullTopic.posts_count,
solution: topic.has_accepted_answer,
readTime: parseInt(fullTopic.word_count/100),
};
}
function getavatarURL(avatar) {
return "https://developer.sailpoint.com" + avatar.replace("{size}", "120")
}
function styleExcerpt(excerpt) {
if (excerpt.length > 150) {
return excerpt.slice(0, 150) + "..."
} else {
return excerpt.replace("&hellip;", "")
}
}

View File

@@ -0,0 +1,35 @@
/* Getting Started Card container */
.gridContainer {
display: grid;
place-content: center;
grid-template-columns: repeat(auto-fit, minmax(345px, 1fr));
grid-gap: 40px;
margin-left: 40px;
margin-right: 40px;
}
.center {
margin: 0px auto;
max-width: 1300px;
margin-bottom: 50px;
}
.space {
height: 200px;
}
.noFound {
font-size: 28px;
font-weight: 500;
color: var(--dev-secondary-text);
padding: 8px;
margin: 50px;
}
.spinnerCenter {
margin: 100px auto;
max-width: 1300px;
margin-bottom: 50px;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import styles from './styles.module.css';
export default function BlogSidebarButton({
filterCallback,
text
}) {
const [isActive, setIsActive] = React.useState(false);
const activeClass = isActive ? styles.tagSelected : ''
function setFilters(e, f) {
filterCallback(f)
setIsActive(current => !current);
}
return <div key={text} onClick={(e) => setFilters(e, text)} className={activeClass + ' ' + styles.tag}>{text}</div>
}

View File

@@ -0,0 +1,36 @@
.tag {
font-size: 16px;
font-weight: 500;
color: var(--dev-secondary-text);
padding: 8px;
margin-right: 5px;
margin-top: 5px;
margin-bottom: 5px;
border-style: solid;
border-width: 1px;
border-color: var(--dev-text-color-normal);
transition: background-color 500ms;
}
.tagSelected {
font-size: 16px;
font-weight: 500;
background-color: var(--dev-secondary-text);
color: var(--dev-card-background);
padding: 8px;
margin-right: 5px;
margin-bottom: 5px;
margin-top: 5px;
border-style: solid;
border-width: 1px;
border-color: var(--dev-text-color-normal);
transition: background-color 500ms;
}
.tag:hover {
cursor: pointer;
background-color: var(--dev-text-color-normal);
color: var(--dev-card-background);
}

View File

@@ -0,0 +1,72 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
import { getTags } from '../../../services/DiscourseService';
import BlogSidebarButton from './BlogSidebarButton';
export default function BlogSidebar({
filterCallback
}) {
const [tagProductData, setTagProductData] = React.useState();
const [tagTechnologyData, setTagTechnologyData] = React.useState();
const [filterTags, setFilterTags] = React.useState(true);
const getTagData = async () => {
const data = await getTags();
const tagTechnologyResultset = []
const tagProductResultset = []
for (const tagGroup of data.extras.tag_groups) {
if (tagGroup.id === 20) {
for (const tag of tagGroup.tags) {
tagProductResultset.push(tag.text)
}
}
if (tagGroup.id === 17) {
for (const tag of tagGroup.tags) {
tagTechnologyResultset.push(tag.text)
}
}
}
setTagProductData(tagProductResultset)
setTagTechnologyData(tagTechnologyResultset)
};
function toggleSeeAll() {
filterTags ? setFilterTags(false) : setFilterTags(true)
}
React.useEffect(() => {
getTagData();
}, []);
const filterText = filterTags ? 'See All Tags' : 'See Less Tags'
if (tagProductData && tagTechnologyData) {
return (
<div className={styles.sidebar}>
<div className={styles.tagHeader}>Blogs by Product</div>
<div className={styles.tagContainer}>
{tagProductData.map(function(a, index){
return <BlogSidebarButton key={a} text={a} filterCallback={filterCallback}></BlogSidebarButton>
})}
</div>
<div className={styles.tagHeader}>Blogs by Identity Governance</div>
<div className={styles.tagContainer}>
{tagTechnologyData.map(function(a, index){
return <div key={'div' + a} className={index > 10 && filterTags ? styles.hidden : ''} > <BlogSidebarButton key={a} text={a} filterCallback={filterCallback}></BlogSidebarButton></div>
})}
</div>
<div className={styles.seeAll} onClick={(e) => toggleSeeAll()}>
{filterText}
{/* <img className={styles.caretDown} src={useBaseUrl('/blog/caret-down-thin.svg')}></img> */}
</div>
</div>
);
} else {
return <div></div>;
}
}

View File

@@ -0,0 +1,40 @@
.sidebar {
width: 400px;
height: 100%;
margin-left: 50px;
}
.tagHeader {
margin-top: 30px;
font-size: 22px;
font-weight: 700;
}
.tagContainer {
display: flex;
flex-wrap: wrap;
}
.hidden {
display: none;
}
.seeAll {
margin-top: 5px;
margin-bottom: 10px;
color: var(--dev-secondary-text);
border-style: solid;
border-width: 1px;
width: 100%;
transition: background-color 500ms;
text-align: center;
}
.seeAll:hover {
cursor: pointer;
background-color: var(--dev-text-color-normal);
color: var(--dev-card-background);
}

View File

@@ -64,7 +64,7 @@ export default function DiscussCard({
if (index > 2) {
return '';
}
return <div className={styles.tag}>{tag}</div>;
return <div key={tag} className={styles.tag}>{tag}</div>;
})}
</div>
</div>

View File

@@ -33,8 +33,9 @@
cursor: pointer;
top: -2px;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.2);
background-color: #c552ae10;
background-color: var(--dev-button-hover);
color: #cc27b0;
text-decoration: none;
}
.button {
@@ -56,6 +57,7 @@
box-shadow: 0px 20px 60px #00000015;
border: 2px solid #df61ca;
border-radius: 5px;
transition: all 0.3s;
}
.link {
@@ -63,4 +65,5 @@
}
.link:hover {
color: #df61ca;
text-decoration: none;
}

View File

@@ -73,7 +73,7 @@
grid-template-columns: repeat(auto-fit, minmax(520px, 1fr));
/* UI Properties */
background: var(--main-hero-card-background);
box-shadow: var(--dev-card-shadow);
box-shadow: var(--dev-main-card-shadow);
border: 1px solid var(--dev-card-background);
border-radius: 40px;
opacity: 1;

View File

@@ -2,13 +2,14 @@
.gridContainer {
display: grid;
place-content: center;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(195px, 1fr));
grid-gap: 40px;
margin-left: 40px;
margin-right: 40px;
}
.center {
margin: 50px auto;
max-width: 1000px;
margin: 0px auto;
max-width: 1300px;
margin-bottom: 50px;
}

View File

@@ -0,0 +1,22 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
export default function MarketplaceBanner() {
return (
<div>
<div className={styles.imageContainer}>
<img className={styles.headerImage} src={useBaseUrl('/blog/marketplace_banner_template.png')}></img>
<div className={styles.blogHeaderText}>
Marketplace
</div>
</div >
</div>
);
}

View File

@@ -0,0 +1,28 @@
.blogHeaderText {
position: relative;
color: #ffffff;
font-size: 48px;
max-width: 459px;
font-weight: bold;
line-height: 130%;
top: -84px;
left: 101px;
}
.background {
width: 100%;
object-fit: repeat;
height: 100%;
}
.imageContainer {
width: 100%;
height: 90px;
background: rgb(0,51,161);
background: linear-gradient(90deg, rgba(0,51,161,1) 0%, rgba(84,192,232,1) 100%);
}
.headerImage {
height: 90px;
}

View File

@@ -0,0 +1,68 @@
import React from 'react';
import styles from './styles.module.css';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import ThemedImage from '@theme/ThemedImage';
import {addDarkToFileName} from '../../../util/util';
import ReactMarkdown from 'react-markdown'
export default function MarketplaceCard({
post,
openDialogFunc,
}) {
function setFilters(e) {
openDialogFunc({"title": post.title, "image": post.image, "link": post.link, "id": post.id});
}
let badge = (
<div></div>
);
if (post.tags.includes("sailpoint-authored")) {
badge = (
<img
className={styles.cardBadge}
src={useBaseUrl('/icons/SailPoint-LogoIcon-RGB-Color.svg')}></img>
);
}
return (
<div onClick={(e) => setFilters(e)}>
<div className={styles.card} >
<div className={styles.cardData}>
<img className={styles.cardEye} src={useBaseUrl('/blog/eye-regular.svg')}></img>
<div className={styles.cardCommentText}>{post.views}</div>
<img className={styles.cardComment} src={useBaseUrl('/blog/comment-light.svg')}></img>
<div className={styles.cardCommentText}>{post.replies}</div>
</div>
<div className={styles.cardUser}>
<img className={styles.cardFace} src={useBaseUrl(post.creatorImage)}></img>
<div className={styles.cardName}>{post.name}</div>
</div>
{badge}
<div className={styles.cardText}>
<img className={styles.cardImage} src={useBaseUrl(post.image)}></img>
<div className={styles.cardTitle}>{post.title}</div>
<div className={styles.tags}>
{post.tags?.map((tag, index) => {
if (index > 2) {
return '';
}
return <div key={tag} className={styles.tag}>{tag}</div>;
})}
</div>
<ReactMarkdown className={styles.cardBody}>{post.excerpt}</ReactMarkdown>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,138 @@
/* Getting Started Card */
.card {
position: relative;
margin-top: 20px;
height: 650px;
/* UI Properties */
background: var(--dev-card-background);
box-shadow: var(--dev-card-shadow);
border: 1px solid var(--dev-card-background);
border-radius: 40px;
opacity: 1;
transition: all 0.3s;
max-width: 600px;
}
.card:hover {
cursor: pointer;
transform: translate(0px, -5px);
box-shadow: var(--dev-card-selected);
}
.cardText {
position: absolute;
margin: 22px;
min-width: 170px;
top: 10px;
left: 0;
}
.cardTitle {
font-size: 22px;
font-weight: 700;
}
.cardBody {
font-size: 16px;
font-weight: 500;
top: 10px;
left: 0;
}
.tag {
font-size: 16px;
font-weight: 500;
color: var(--dev-secondary-text);
background-color: var(--dev-tag-highlight);
padding: 0px 8px;
margin-left: 5px;
margin-top: 5px;
}
.tags {
margin: 0px;
width: 100%;
display: flex;
flex-wrap: wrap;
}
.cardUser {
position: absolute;
bottom: 10px;
left: 25px;
display: flex;
}
.cardFace {
border-radius: 9999px;
margin: auto;
height: 40px;
width: 40px;
}
.cardName {
min-width: 170px;
margin-top: 7px;
margin-left: 10px;
}
.cardData {
position: absolute;
bottom: 50px;
left: 20px;
display: flex;
align-items: center;
}
.cardEye {
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
margin-left: 5px;
height: 18px;
width: 18px;
}
.cardComment {
margin-left: 10px;
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
height: 18px;
width: 18px;
margin-bottom: 4px;
}
.cardCommentText {
color: #96a9ba;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
margin-left: 5px;
margin-bottom: 5px;
height: 20px;
font-size: 16px;
}
.cardBadge {
position: absolute;
right: 5px;
bottom: 0px;
margin-left: 5px;
height: 120px;
width: 120px;
}

View File

@@ -0,0 +1,108 @@
import React from 'react';
import styles from './styles.module.css';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import ThemedImage from '@theme/ThemedImage';
import {addDarkToFileName} from '../../../util/util';
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
import ReactMarkdown from 'react-markdown';
import BounceLoader from 'react-spinners/BounceLoader';
export default function MarketplaceCardDetail({details, rawPost}) {
const getDivText = (data, id) => {
const requirementPosition = data.indexOf('id="' + id + '"');
const requirementEndPosition = data.indexOf('</div>', requirementPosition);
if (requirementPosition > 0 && requirementEndPosition > 0) {
const validContent = data.substring(
requirementPosition + 6 + id.length,
requirementEndPosition,
);
const incorrectURLPattern = /upload:\/\/([^"]+)/g;
const correctURLPattern =
'https://developer.identitysoon.com/discuss/uploads/short-url/$1';
return validContent.replace(incorrectURLPattern, correctURLPattern);
} else {
return 'No requirements found for this marketplace item';
}
// https://developer.identitysoon.com/discuss/uploads/short-url/f9rlt0eoj7RnpndvL0CHjcEesV0.png?dl=1\
// upload://f9rlt0eoj7RnpndvL0CHjcEesV0.png
};
const goToLink = (link) => {
window.open(link, '_blank');
};
if (details) {
return (
<div className={styles.detailContainer}>
<div className={styles.detailHeader}>
<div className={styles.detailTitle}>{details.title}</div>
<img
className={styles.detailImage}
src={useBaseUrl(details.image)}></img>
</div>
<Tabs className={styles.detailTabs}>
<TabList>
<Tab>Details</Tab>
<Tab>Requirements</Tab>
<Tab>Support</Tab>
</TabList>
<TabPanel>
<ReactMarkdown className={styles.detailTabContent}>
{getDivText(rawPost, 'details')}
</ReactMarkdown>
<button
className={styles.modalButton}
onClick={async () => {
goToLink(details.link);
}}>
<div className={styles.modalButtonText}>
<img
className={styles.buttonImage}
src={useBaseUrl('/icons/discourse.svg')}></img>
See More
</div>
</button>
<button
className={styles.modalButton}
onClick={async () => {
console.log(getDivText(rawPost,'github'))
const regex = /\(([^)]+)\)/;
goToLink(getDivText(rawPost,'github').match(regex)[1]);
}}>
<div className={styles.modalButtonText}>
<img
className={styles.buttonImage}
src={useBaseUrl('/icons/github.svg')}></img>
GitHub
</div>
</button>
</TabPanel>
<TabPanel>
<ReactMarkdown className={styles.detailTabContent}>
{getDivText(rawPost, 'requirements')}
</ReactMarkdown>
</TabPanel>
<TabPanel>
<ReactMarkdown className={styles.detailTabContent}>
{getDivText(rawPost, 'support')}
</ReactMarkdown>
</TabPanel>
</Tabs>
</div>
);
} else {
return (
<BounceLoader
className={styles.spinnerCenter}
color={'#0033a1'}
loading={true}
size={150}
aria-label="Loading Spinner"
data-testid="loader"
/>
);
}
}

View File

@@ -0,0 +1,80 @@
.detailContainer {
max-width: 937px;
}
.detailHeader {
display: flex;
justify-content: center;
align-items: center;
margin: 20px;
}
.detailImage {
border-radius: 40px;
max-width: 400px;
min-width: 180px;
}
.detailTitle {
margin: 45px;
font-size: 48px;
font-weight: 500;
color: var(--dev-secondary-text);
text-shadow: rgba(17, 61, 158, 0.64) 1px 2px 2px;
}
.detailTabContent {
margin: 20px;
}
.detailTabs {
margin: 10px;
}
.modalButtonText {
position: relative;
top: -2px;
}
.modalButton {
border: 1px solid var(--ifm-color-primary);
box-shadow: 1px 4px 10px rgba(0, 0, 0, 0.12);
border-radius: 24px;
color: var(--ifm-color-primary);
margin: 10px;
padding: 10px;
text-align: center;
width: 125px;
background-color: #ffffff31;
}
.modalButton:hover {
cursor: pointer;
top: -2px;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.4);
background-color: #dae1e9;
color: #005fc4;
}
.spinnerCenter {
margin: 30vh 20vh;
}
@media only screen and (max-width: 768px) {
.detailImage {
display: none;
}
}
.buttonImage {
position: relative;
left: -7px;
top: 4px;
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
margin-left: 5px;
height: 18px;
width: 18px;
}

View File

@@ -0,0 +1,154 @@
import React from 'react';
import styles from './styles.module.css';
import MarketplaceCard from '../MarketplaceCard';
import Modal from 'react-modal';
import useBaseUrl from '@docusaurus/useBaseUrl';
import BounceLoader from 'react-spinners/BounceLoader';
import {
getMarketplacePosts,
getMarketplaceTopic,
getMarketplaceTopicRaw,
} from '../../../services/DiscourseService';
import MarketplaceCardDetail from '../MarketplaceCardDetail';
export default function MarketplaceCards({filterCallback}) {
const [cardData, setCardData] = React.useState();
const [detailsOpen, setDetailsOpen] = React.useState(false);
const [details, setDetails] = React.useState('');
const [loadingCards, setLoadingCards] = React.useState(true);
const getPosts = async () => {
const data = await getMarketplacePosts(filterCallback.join(','));
console.log(data)
const resultset = [];
if (data.topics) {
for (const topic of data.topics) {
if (topic.tags.length > 0) {
resultset.push(await getPostList(topic));
}
}
setCardData(resultset);
} else {
setCardData(undefined);
}
setLoadingCards(false);
};
const getDetails = async (data) => {
const raw = await getMarketplaceTopicRaw(data.id);
setDetails({data: data, raw: raw});
};
// callback that gets called when clicking on a card
const openDialog = (data) => {
setDetails({data: undefined, raw: undefined});
getDetails(data);
setDetailsOpen(true);
};
React.useEffect(() => {}, [detailsOpen, details]);
Modal.setAppElement('#__docusaurus');
React.useEffect(() => {
getPosts();
setCardData(undefined);
setLoadingCards(true);
}, [filterCallback]);
const xImage = useBaseUrl('/icons/circle-xmark-regular.svg')
if (cardData) {
return (
<div className={styles.center}>
<div className={styles.gridContainer}>
{cardData.map(function (a, index) {
return (
<MarketplaceCard
post={a}
key={index + a.link}
openDialogFunc={openDialog}></MarketplaceCard>
);
})}
</div>
<Modal
isOpen={detailsOpen}
className={styles.modal}
contentLabel="Details">
<div>
<div>
<MarketplaceCardDetail
details={details.data}
rawPost={details.raw}></MarketplaceCardDetail>
</div>
<img
className={styles.cardExit}
src={xImage}
onClick={async () => {
setDetailsOpen(false);
}}></img>
</div>
</Modal>
</div>
);
} else if (loadingCards) {
return (
<BounceLoader
className={styles.spinnerCenter}
color={'#0033a1'}
loading={true}
size={150}
aria-label="Loading Spinner"
data-testid="loader"
/>
);
} else {
return (
<div className={styles.noFound}>
{' '}
No Marketplace Item Found with the Given Search Criteria
</div>
);
}
}
async function getPostList(topic) {
const fullTopic = await getMarketplaceTopic(topic.id);
return {
id: topic.id,
name: fullTopic.details.created_by.name,
excerpt: styleExcerpt(topic.excerpt),
creatorImage: getavatarURL(fullTopic.details.created_by.avatar_template),
tags: topic.tags,
image: fullTopic.image_url,
link:
'https://developer.sailpoint.com/discuss/t/' +
topic.slug +
'/' +
topic.id,
title: topic.title,
views: fullTopic.views,
liked: topic.like_count,
replies: fullTopic.posts_count,
solution: topic.has_accepted_answer,
readTime: parseInt(fullTopic.word_count / 100),
};
}
function getavatarURL(avatar) {
return 'https://developer.sailpoint.com' + avatar.replace('{size}', '120');
}
function styleExcerpt(excerpt) {
if (excerpt) {
// remove any strings that have colons between them
excerpt = excerpt.replace(/:[^:]*:/g, '');
if (excerpt.length > 150) {
return excerpt.slice(0, 100) + '...';
} else {
return excerpt.replace('&hellip;', '');
}
} else {
return '';
}
}

View File

@@ -0,0 +1,78 @@
/* Getting Started Card container */
.gridContainer {
display: grid;
place-content: center;
grid-template-columns: repeat(auto-fit, minmax(345px, 1fr));
grid-gap: 40px;
margin-left: 40px;
margin-right: 40px;
}
.center {
margin: 0px auto;
max-width: 1300px;
margin-bottom: 50px;
}
.space {
height: 200px;
}
.noFound {
font-size: 28px;
font-weight: 500;
color: var(--dev-secondary-text);
padding: 8px;
margin: 50px;
}
.modal {
position: absolute;
top: 50%;
left: 50%;
right: auto;
bottom: auto;
margin-right: -50%;
transform: translate(-50%, -50%);
box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.25);
border-radius: 20px;
background-color: var(--dev-card-background);
max-height: 100%;
max-width: 90%;
min-height: 80%;
scroll-behavior: auto;
overflow-y: auto;
}
.cardExit {
position: absolute;
top: 6px;
right: 6px;
fill: #96a9bb;
/* computed from codepen https://codepen.io/sosuke/pen/Pjoqqp */
filter: invert(79%) sepia(12%) saturate(484%) hue-rotate(168deg) brightness(84%) contrast(84%);
margin-left: 5px;
height: 24px;
width: 24px;
transition: all 0.2s;
}
.cardExit:hover {
top: 8px;
cursor: pointer;
height: 23px;
width: 23px;
}
.spinnerCenter {
margin: 100px auto;
max-width: 1300px;
margin-bottom: 50px;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import styles from './styles.module.css';
export default function MarketplaceSidebarButton({
filterCallback,
text
}) {
const [isActive, setIsActive] = React.useState(false);
const activeClass = isActive ? styles.tagSelected : ''
function setFilters(e, f) {
filterCallback(f)
setIsActive(current => !current);
}
return <div key={text} onClick={(e) => setFilters(e, text)} className={activeClass + ' ' + styles.tag}>{text}</div>
}

View File

@@ -0,0 +1,36 @@
.tag {
font-size: 16px;
font-weight: 500;
color: var(--dev-secondary-text);
padding: 8px;
margin-right: 5px;
margin-top: 5px;
margin-bottom: 5px;
border-style: solid;
border-width: 1px;
border-color: var(--dev-text-color-normal);
transition: background-color 500ms;
}
.tagSelected {
font-size: 16px;
font-weight: 500;
background-color: var(--dev-secondary-text);
color: var(--dev-card-background);
padding: 8px;
margin-right: 5px;
margin-bottom: 5px;
margin-top: 5px;
border-style: solid;
border-width: 1px;
border-color: var(--dev-text-color-normal);
transition: background-color 500ms;
}
.tag:hover {
cursor: pointer;
background-color: var(--dev-text-color-normal);
color: var(--dev-card-background);
}

View File

@@ -0,0 +1,72 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
import { getTags } from '../../../services/DiscourseService';
import MarketplaceSidebarButton from './MarketplaceSidebarButton';
export default function MarketplaceSidebar({
filterCallback
}) {
const [tagProductData, setTagProductData] = React.useState();
const [tagTechnologyData, setTagTechnologyData] = React.useState();
const [filterTags, setFilterTags] = React.useState(true);
const getTagData = async () => {
const data = await getTags();
const tagTechnologyResultset = []
const tagProductResultset = []
for (const tagGroup of data.extras.tag_groups) {
if (tagGroup.id === 20) {
for (const tag of tagGroup.tags) {
tagProductResultset.push(tag.text)
}
}
if (tagGroup.id === 17) {
for (const tag of tagGroup.tags) {
tagTechnologyResultset.push(tag.text)
}
}
}
setTagProductData(tagProductResultset)
setTagTechnologyData(tagTechnologyResultset)
};
function toggleSeeAll() {
filterTags ? setFilterTags(false) : setFilterTags(true)
}
React.useEffect(() => {
getTagData();
}, []);
const filterText = filterTags ? 'See All Tags' : 'See Less Tags'
if (tagProductData && tagTechnologyData) {
return (
<div className={styles.sidebar}>
<div className={styles.tagHeader}>Items by Product</div>
<div className={styles.tagContainer}>
{tagProductData.map(function(a, index){
return <MarketplaceSidebarButton key={a} text={a} filterCallback={filterCallback}></MarketplaceSidebarButton>
})}
</div>
<div className={styles.tagHeader}>Items by Identity Governance</div>
<div className={styles.tagContainer}>
{tagTechnologyData.map(function(a, index){
return <div key={'div' + a} className={index > 10 && filterTags ? styles.hidden : ''} > <MarketplaceSidebarButton key={a} text={a} filterCallback={filterCallback}></MarketplaceSidebarButton></div>
})}
</div>
<div className={styles.seeAll} onClick={(e) => toggleSeeAll()}>
{filterText}
{/* <img className={styles.caretDown} src={useBaseUrl('/blog/caret-down-thin.svg')}></img> */}
</div>
</div>
);
} else {
return <div></div>;
}
}

View File

@@ -0,0 +1,40 @@
.sidebar {
width: 400px;
height: 100%;
margin-left: 50px;
}
.tagHeader {
margin-top: 30px;
font-size: 22px;
font-weight: 700;
}
.tagContainer {
display: flex;
flex-wrap: wrap;
}
.hidden {
display: none;
}
.seeAll {
margin-top: 5px;
margin-bottom: 10px;
color: var(--dev-secondary-text);
border-style: solid;
border-width: 1px;
width: 100%;
transition: background-color 500ms;
text-align: center;
}
.seeAll:hover {
cursor: pointer;
background-color: var(--dev-text-color-normal);
color: var(--dev-card-background);
}

View File

@@ -244,13 +244,16 @@
--dev-text-color-secondary: #0033a1;
--dev-secondary-text: #415364;
--dev-tag-highlight: #eaeef1;
--text-on-primary: #ffffff;
/*card css*/
--dev-card-background: #ffffff;
--dev-card-shadow: 0px 20px 60px #00000015;
--dev-card-background: #e9e9e963;
--dev-card-shadow: 0px 0px 0px #6b6b6b15;
--dev-main-card-shadow: 0px 20px 60px #00000015;
--dev-card-selected: 0 4px 5px rgba(0, 0, 0, 0.2);
--dev-button-hover: #c552ae10;
--ifm-github-logo: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E");
--ifm-medium-logo: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 1770 1000"><circle cx="500" cy="500" r="500"/><ellipse ry="475" rx="250" cy="501" cx="1296"/><ellipse cx="1682" cy="502" rx="88" ry="424"/></svg>');
@@ -283,9 +286,12 @@
/*card css*/
--dev-card-background: #2a2b2d;
--dev-card-shadow: 0 4px 5px rgba(0, 0, 0, 0.2);
--dev-card-selected: 0 5px 5px rgba(107, 107, 107, 0.2);
--dev-card-shadow: 0 0px 0px rgba(0, 0, 0, 0.2);
--dev-main-card-shadow: 0 4px 5px rgba(0, 0, 0, 0.2);
--dev-card-selected: 0 5px 5px rgba(151, 151, 151, 0.2);
--dev-secondary-text: #dae1e9;
--dev-tag-highlight: #00000075;
--dev-button-hover: #c552ae40;
/*main hero card*/
--main-hero-card-background: #202122;

View File

@@ -0,0 +1,64 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import AmbassadorBanner from '../components/ambassador/AmbassadorBanner';
import useBaseUrl from '@docusaurus/useBaseUrl';
import styles from './ambassador.module.css';
import AmbassadorCards from '../components/ambassador/AmbassadorCards';
export default function Ambassador() {
const {siteConfig} = useDocusaurusContext();
return (
<Layout description="The SailPoint Developer Community has everything you need to build, extend, and automate scalable identity solutions.">
<main>
{/* <AmbassadorBanner /> */}
<div className={styles.ambassadorPurposeContainer}>
<img
className={styles.ambassadorPurposeImage}
src={useBaseUrl('/ambassador/Ambassador-Program-Banner.png')}></img>
<div className={styles.ambassadorPurposeText}>
Our Ambassador Program offers not just exclusive benefits but also
an enriching learning experience on SailPoint; it's a journey to
expertise. As an Ambassador, your contributions foster community
growth, enabling knowledge-sharing among experts like you.
<Link
className={styles.link}
to="https://developer.sailpoint.com/discuss/t/announcing-the-developer-community-ambassador-program/10634">
<div className={styles.button}>Become an Ambassador</div>
</Link>
</div>
</div>
<div className={styles.headerText}>
<div className={styles.headerTextOne}>Expert Ambassadors</div>
</div>
<div className={styles.imageContainer}>
<div className={styles.spanLeft}></div>
<img
className={styles.ambassadorImage}
src={useBaseUrl('/icons/ExpertAmbassador.svg')}></img>
<div className={styles.spanLeft}></div>
</div>
<div className={styles.ambassadorCardContainer}>
<AmbassadorCards expert={true} />
</div>
<div className={styles.headerText}>
<div className={styles.headerTextOne}>Ambassadors</div>
</div>
<div className={styles.imageContainer}>
<div className={styles.spanLeft}></div>
<img
className={styles.ambassadorImage}
src={useBaseUrl('/icons/Ambassador.svg')}></img>
<div className={styles.spanLeft}></div>
</div>
<div className={styles.ambassadorCardContainer}>
<AmbassadorCards expert={false} />
</div>
</main>
</Layout>
);
}

View File

@@ -0,0 +1,142 @@
.gridContainer {
display: grid;
place-content: center;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
grid-gap: 40px;
margin-left: 40px;
margin-right: 40px;
}
.center {
margin: 50px auto;
max-width: 1000px;
}
.headerText {
text-align: center;
display: flex;
margin: 100px auto 0px auto;
width: calc(100% - 100px);
justify-content: center;
}
.headerTextOne {
color: var(--ifm-color-primary);
font-size: 30px;
font-weight: bold;
line-height: 100%;
}
.spanLeft {
width: 35%;
margin: 14px;
position: relative;
top: -45px;
border-bottom-width:2px;
border-bottom-style:solid;
border-bottom-color:var(--ifm-color-primary);
}
.imageContainer {
text-align: center;
display: flex;
justify-content: center;
}
.ambassadorImage {
margin-left: 5px;
padding: 20px;
height: 120px;
width: 120px;
}
.ambassadorPurposeContainer {
display: flex;
justify-content: center;
width: calc(75% + 75px);
align-items: center;
margin: 0px auto 0px auto;
}
.ambassadorPurposeText {
font-size: 21px;
padding: 50px 0px 50px 50px;
font-weight: 500;
color: var(--dev-text-color-normal);
}
.ambassadorPurposeImage {
width: 40%;
box-shadow: var(--dev-card-selected);
border-radius: 40px;
margin: 50px 50px 50px 0px;
}
@media only screen and (max-width: 1460px) {
.ambassadorPurposeContainer {
flex-direction: column;
}
.ambassadorPurposeImage {
width: 80%;
margin: 50px 50px 50px 50px;
}
.ambassadorPurposeText {
padding: 50px 50px 50px 50px;
}
}
.button:hover {
cursor: pointer;
top: -2px;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.2);
background-color: #c552ae10;
color: #cc27b0;
background-color: var(--dev-button-hover);
}
.button {
text-decoration: none;
text-align: center;
margin: 30px auto;
font-size: 20px;
font-weight: bold;
line-height: 100%;
/* Layout Properties */
width: 314px;
height: 61px;
line-height: 61px;
/* UI Properties */
background: transparent 0% 0% no-repeat padding-box;
opacity: 1;
box-shadow: 0px 20px 60px #00000015;
border: 2px solid #df61ca;
border-radius: 5px;
transition: all 0.3s;
}
.link {
color: #df61ca;
}
.link:hover {
color: #df61ca;
text-decoration: none;
}

41
src/pages/blog.js Normal file
View File

@@ -0,0 +1,41 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import BlogBanner from '../components/blog/BlogBanner';
import styles from './blog.module.css';
import BlogCards from '../components/blog/BlogCards';
import BlogSidebar from '../components/blog/BlogSidebar';
export default function Blog() {
const [filteredProduct, setFilteredProduct] = React.useState([]);
const {siteConfig} = useDocusaurusContext();
const handleClick = (data) => {
var tempFilter = filteredProduct.slice()
const index = tempFilter.indexOf(data);
if (index !== -1) {
tempFilter.splice(index, 1);
} else {
tempFilter.push(data)
}
setFilteredProduct(tempFilter)
};
return (
<Layout description="The SailPoint Developer Community has everything you need to build, extend, and automate scalable identity solutions.">
<main>
<BlogBanner />
<div className={styles.blogContainer}>
<div className={styles.blogSidbarContainer}><BlogSidebar filterCallback={handleClick}/></div>
<div className={styles.blogCardContainer}><BlogCards filterCallback={filteredProduct}/></div>
</div>
</main>
</Layout>
);
}

17
src/pages/blog.module.css Normal file
View File

@@ -0,0 +1,17 @@
.blogContainer {
display: flex;
}
.blogSidbarContainer {
flex: 5%;
}
@media only screen and (max-width: 870px) {
.blogSidbarContainer {
display: none;
}
}
.blogCardContainer {
flex: 95%;
}

41
src/pages/marketplace.js Normal file
View File

@@ -0,0 +1,41 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import MarketplaceBanner from '../components/marketplace/MarketplaceBanner';
import styles from './marketplace.module.css';
import MarketplaceCards from '../components/marketplace/MarketplaceCards';
import MarketplaceSidebar from '../components/marketplace/MarketplaceSidebar';
export default function Marketplace() {
const [filteredProduct, setFilteredProduct] = React.useState([]);
const {siteConfig} = useDocusaurusContext();
const handleClick = (data) => {
var tempFilter = filteredProduct.slice()
const index = tempFilter.indexOf(data);
if (index !== -1) {
tempFilter.splice(index, 1);
} else {
tempFilter.push(data)
}
setFilteredProduct(tempFilter)
};
return (
<Layout description="The SailPoint Developer Community has everything you need to build, extend, and automate scalable identity solutions.">
<main>
<MarketplaceBanner />
<div className={styles.blogContainer}>
<div className={styles.blogSidbarContainer}><MarketplaceSidebar filterCallback={handleClick}/></div>
<div className={styles.blogCardContainer}><MarketplaceCards filterCallback={filteredProduct}/></div>
</div>
</main>
</Layout>
);
}

View File

@@ -0,0 +1,17 @@
.blogContainer {
display: flex;
}
.blogSidbarContainer {
flex: 5%;
}
@media only screen and (max-width: 870px) {
.blogSidbarContainer {
display: none;
}
}
.blogCardContainer {
flex: 95%;
}

View File

@@ -8,3 +8,124 @@ export async function getTopPosts() {
return [];
}
}
export async function getAmbassadors(expert) {
try {
if (expert) {
const response = await fetch(
'https://developer.sailpoint.com/discuss/groups/ambassador_expert/members.json',
);
return await response.json();
} else {
const response = await fetch(
'https://developer.sailpoint.com/discuss/groups/ambassador/members.json',
);
return await response.json();
}
} catch (error) {
return [];
}
}
export async function getAmbassadorDetails(id) {
try {
const response = await fetch(
'https://developer.sailpoint.com/discuss/user-cards.json?user_ids=' + id.join(','),
);
return await response.json();
} catch (error) {
return [];
}
}
export async function checkImage(url) {
try {
const response = await fetch(
url,
);
console.log(response)
return true
} catch (error) {
return false;
}
}
export async function getBlogPosts(tags) {
let url = ''
if (tags) {
url = 'https://developer.sailpoint.com/discuss/search.json?q=category:blog+tags:' + tags
} else {
url = 'https://developer.sailpoint.com/discuss/search.json?q=category:blog'
}
try {
const response = await fetch(
url,
);
return await response.json();
} catch (error) {
return [];
}
}
export async function getMarketplacePosts(tags) {
let url = ''
if (tags) {
url = 'https://developer.sailpoint.com/discuss/search.json?q=category:marketplace+tags:' + tags
} else {
url = 'https://developer.sailpoint.com/discuss/search.json?q=category:marketplace'
}
try {
const response = await fetch(
url,
);
return await response.json();
} catch (error) {
return [];
}
}
export async function getTopic(id) {
try {
const response = await fetch(
'https://developer.sailpoint.com/discuss/t/' + id + '.json',
);
return await response.json();
} catch (error) {
return [];
}
}
export async function getMarketplaceTopic(id) {
try {
const response = await fetch(
'https://developer.sailpoint.com/discuss/t/' + id + '.json',
);
return await response.json();
} catch (error) {
return [];
}
}
export async function getMarketplaceTopicRaw(id) {
try {
const response = await fetch(
'https://developer.sailpoint.com/discuss/raw/' + id + '.json',
);
return await response.text();
} catch (error) {
return [];
}
}
export async function getTags() {
try {
const response = await fetch(
'https://developer.sailpoint.com/discuss/tags.json',
);
return await response.json();
} catch (error) {
return [];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -50,6 +50,18 @@ get:
**uncorrelated**: *eq*
- in: query
name: sorters
required: false
schema:
type: string
format: comma-separated
example: id,name
description: >-
Sort results using the standard syntax described in [V3 API Standard Collection Parameters](https://developer.sailpoint.com/idn/api/standard-collection-parameters#sorting-results)
Sorting is supported for the following fields: **id**, **name**, **created**, **modified**
responses:
'200':
description: List of account objects
@@ -58,7 +70,7 @@ get:
schema:
type: array
items:
oneOf:
anyOf:
- $ref: '../schemas/SlimAccount.yaml'
- $ref: '../schemas/FullAccount.yaml'
examples:
@@ -122,6 +134,8 @@ post:
description: >-
This API submits an account creation task and returns the task ID.
The `sourceId` where this account will be created must be included in the `attributes` object.
A token with ORG_ADMIN authority is required to call this API.
security:
- oauth2: [idn:accounts:manage]
@@ -130,7 +144,7 @@ post:
content:
application/json:
schema:
$ref: '../schemas/AccountAttributes.yaml'
$ref: "../schemas/AccountAttributesCreate.yaml"
responses:
'202':
description: Async task details

View File

@@ -28,6 +28,9 @@ get:
**category**: *eq*
**features**: *ca*
example: directConnect eq "true"
- $ref: '../../v3/parameters/limit.yaml'
- $ref: '../../v3/parameters/offset.yaml'

View File

@@ -55,6 +55,13 @@ get:
"name": "identity 1",
"type": "IDENTITY"
},
"segments": [
"1d126fe0-45e2-4aea-bc64-a07e9344ef26"
],
"manuallyUpdatedFields": {
"DISPLAY_NAME": true,
"DESCRIPTION": true
},
"id": "2c91808c74ff913f0175097daa9d59cd",
"name": "LauncherTest1",
"created": "2020-10-08T18:33:52.029Z",

View File

@@ -55,6 +55,13 @@ get:
"name": "identity 1",
"type": "IDENTITY"
},
"segments": [
"1d126fe0-45e2-4aea-bc64-a07e9344ef26"
],
"manuallyUpdatedFields": {
"DISPLAY_NAME": true,
"DESCRIPTION": true
},
"id": "2c91808c74ff913f0175097daa9d59cd",
"name": "LauncherTest1",
"created": "2020-10-08T18:33:52.029Z",

View File

@@ -49,6 +49,13 @@ get:
"name": "Addie Smith",
"type": "IDENTITY"
},
"segments": [
"1d126fe0-45e2-4aea-bc64-a07e9344ef26"
],
"manuallyUpdatedFields": {
"DISPLAY_NAME": true,
"DESCRIPTION": true
},
"id": "2c91808c74ff913f0175097daa9d59cd",
"name": "LauncherTest1",
"created": "2020-10-08T18:33:52.029Z",
@@ -167,6 +174,22 @@ patch:
}
}
]
Set entitlement manually updated fields:
description: >-
This example shows how to set an entitlement's manually updated fields values with patch request.
Values for all manually updateable fields must be specified in the request.
For now only two entitlement fields support this: DISPLAY_NAME and DESCRIPTION.
value:
[
{
"op": "replace",
"path": "/manuallyUpdatedFields",
"value": {
"DISPLAY_NAME": true,
"DESCRIPTION": true
}
}
]
responses:
'200':
description: Responds with the entitlement as updated.

View File

@@ -25,7 +25,7 @@ get:
name: configType
required: true
schema:
$ref: '../schemas/work-reassignment/WorkTypeEnum.yaml'
$ref: '../schemas/work-reassignment/ConfigTypeEnum.yaml'
description: Reassignment work type
example: accessRequests
- in: query

View File

@@ -2,7 +2,7 @@ get:
tags:
- Sources
summary: Downloads source accounts schema template
operationId: downloadSourceAccountsSchema
operationId: getSourceAccountsSchema
parameters:
- in: path
name: id
@@ -37,7 +37,7 @@ post:
summary: Uploads source accounts schema template
description: >-
This API uploads a source schema template file to configure a source's account attributes.
operationId: uploadSourceAccountsSchema
operationId: importSourceAccountsSchema
parameters:
- in: path
name: id

View File

@@ -1,8 +1,9 @@
#No Beta endpoint found for this operation.
get:
tags:
- Sources
summary: Downloads source entitlements schema template
operationId: downloadSourceEntitlementsSchema
operationId: getSourceEntitlementsSchema
parameters:
- in: path
name: id
@@ -37,13 +38,14 @@ get:
$ref: '../../v3/responses/500.yaml'
security:
- oauth2: [idn:source-schema:read, idn:source-schema:manage]
#No Beta endpoint found for this operation.
post:
tags:
- Sources
summary: Uploads source entitlements schema template
description: >-
This API uploads a source schema template file to configure a source's entitlement attributes.
operationId: uploadSourceEntitlementsSchema
operationId: importSourceEntitlementsSchema
parameters:
- in: path
name: id

View File

@@ -1,5 +1,7 @@
post:
operationId: uploadSourceConnectorFile
operationId: importSourceConnectorFile
security:
- oauth2: [ idn:sources-admin:manage ]
tags:
- Sources
summary: Upload connector file to source

View File

@@ -1,5 +1,7 @@
get:
operationId: getSource
security:
- oauth2: [ idn:sources:read ]
tags:
- Sources
summary: Get Source by ID
@@ -36,6 +38,8 @@ get:
$ref: '../../v3/responses/500.yaml'
put:
operationId: putSource
security:
- oauth2: [ idn:sources:manage ]
tags:
- Sources
summary: Update Source (Full)
@@ -62,6 +66,7 @@ put:
type: string
required: true
description: The Source id
example: 2c9180835d191a86015d28455b4a2329
requestBody:
required: true
content:
@@ -91,6 +96,8 @@ put:
$ref: '../../v3/responses/500.yaml'
patch:
operationId: updateSource
security:
- oauth2: [ idn:sources:manage ]
tags:
- Sources
summary: Update Source (Partial)
@@ -99,7 +106,6 @@ patch:
[JSON Patch](https://tools.ietf.org/html/rfc6902) standard.
Some fields are immutable and cannot be changed, such as:
* id
* type
* authoritative
@@ -225,7 +231,9 @@ patch:
'500':
$ref: '../../v3/responses/500.yaml'
delete:
operationId: deleteSource
security:
- oauth2: [ idn:sources:manage ]
operationId: delete
tags:
- Sources
summary: Delete Source by ID
@@ -270,13 +278,6 @@ delete:
deleteSource:
summary: Response returned when deleting a source
value: {"type": "TASK_RESULT", "id": "2c91808779ecf55b0179f720942f181a", "name": null}
links:
GetTaskStatusById:
operationId: getTaskStatus
parameters:
id: '$response.body#/id'
description: >
The `id` value returned in the response can be used as the `id` parameter in `GET /task-status/{id}`.
'400':
$ref: '../../v3/responses/400.yaml'
'401':

View File

@@ -1,5 +1,7 @@
get:
operationId: listSources
security:
- oauth2: [ idn:sources:read ]
tags:
- Sources
summary: Lists all sources in IdentityNow.
@@ -18,51 +20,22 @@ get:
example: name eq "#Employees"
description: >-
Filter results using the standard syntax described in [V3 API Standard Collection Parameters](https://developer.sailpoint.com/idn/api/standard-collection-parameters#filtering-results)
Filtering is supported for the following fields and operators:
**id**: *eq, in*
**name**: *co, eq, in, sw*
**type**: *eq, in*
**owner.id**: *eq, in*
**features**: *ca, co*
**created**: *eq*
**modified**: *eq*
**managementWorkgroup.id**: *eq*
**description**: *eq*
**authoritative**: *eq*
**healthy**: *eq*
**status**: *eq, in*
**connectionType**: *eq*
**connectorName**: *eq*
**id**: *eq, in*
**name**: *co, eq, in, sw*
**type**: *eq, in*
**owner.id**: *eq, in*
**features**: *ca, co*
**created**: *eq*
**modified**: *eq*
**managementWorkgroup.id**: *eq*
**description**: *eq*
**authoritative**: *eq*
**healthy**: *eq*
**status**: *eq, in*
**connectionType**: *eq*
**connectorName**: *eq*
- in: query
name: sorters
schema:
@@ -72,7 +45,6 @@ get:
description: >-
Sort results using the standard syntax described in [V3 API Standard Collection Parameters](https://developer.sailpoint.com/idn/api/standard-collection-parameters#sorting-results)
Sorting is supported for the following fields: **type, created, modified, name, owner.name, healthy, status**
- in: query
name: for-subadmin
@@ -109,6 +81,8 @@ get:
$ref: '../../v3/responses/500.yaml'
post:
operationId: createSource
security:
- oauth2: [ idn:sources:manage ]
tags:
- Sources
summary: Creates a source in IdentityNow.

View File

@@ -0,0 +1,23 @@
type: object
required:
- attributes
properties:
attributes:
description: The schema attribute values for the account
type: object
required:
- sourceId
properties:
sourceId:
type: string
description: Target source to create an account
example: 34bfcbe116c9407464af37acbaf7a4dc
additionalProperties:
type: string
example:
sourceId: 34bfcbe116c9407464af37acbaf7a4dc
city: Austin
displayName: John Doe
userName: jdoe
sAMAccountName: jDoe
mail: john.doe@sailpoint.com

View File

@@ -206,3 +206,13 @@ allOf:
type: string
description: Name of the source
example: Source with orphan entitlements
mandatoryCommentRequirement:
type: string
description: >-
Determines whether comments are required for decisions during certification reviews. You can require comments
for all decisions, revoke-only decisions, or no decisions. By default, comments are not required for decisions.
enum:
- "ALL_DECISIONS"
- "REVOKE_ONLY_DECISIONS"
- "NO_DECISIONS"
example: NO_DECISIONS

View File

@@ -13,11 +13,3 @@ properties:
enum:
- "CERTIFICATION"
example: CERTIFICATION
correlatedStatus:
description: >-
The correlatedStatus of the campaign. Only SOURCE_OWNER campaigns can be Uncorrelated.
An Uncorrelated certification campaign only includes Uncorrelated identities (An identity is uncorrelated if it has no accounts on an authoritative source).
enum:
- "CORRELATED"
- "UNCORRELATED"
example: CORRELATED

View File

@@ -8,6 +8,16 @@ properties:
type: string
description: The entitlement name
example: "LauncherTest2"
created:
type: string
description: Time when the entitlement was created
format: 'date-time'
example: "2020-10-08T18:33:52.029Z"
modified:
type: string
description: Time when the entitlement was last modified
format: 'date-time'
example: "2020-10-08T18:33:52.029Z"
attribute:
type: string
description: The entitlement attribute name
@@ -20,28 +30,30 @@ properties:
type: string
description: The object type of the entitlement from the source schema
example: "group"
description:
type: string
description: The description of the entitlement
example: "CN=LauncherTest2,OU=LauncherTestOrg,OU=slpt-automation,DC=TestAutomationAD,DC=local"
privileged:
type: boolean
default: false
description: True if the entitlement is privileged
example: true
cloudGoverned:
type: boolean
default: false
description: True if the entitlement is cloud governed
example: true
created:
description:
type: string
description: Time when the entitlement was created
format: 'date-time'
example: "2020-10-08T18:33:52.029Z"
modified:
type: string
description: Time when the entitlement was last modified
format: 'date-time'
example: "2020-10-08T18:33:52.029Z"
description: The description of the entitlement
example: "CN=LauncherTest2,OU=LauncherTestOrg,OU=slpt-automation,DC=TestAutomationAD,DC=local"
requestable:
type: boolean
default: false
description: True if the entitlement is requestable
example: true
attributes:
type: object
description: A map of free-form key-value pairs from the source system
example: { "fieldName": "fieldValue" }
additionalProperties: true
source:
type: object
properties:
@@ -57,11 +69,12 @@ properties:
type: string
description: The source name
example: ODS-AD-Source
attributes:
type: object
description: A map of free-form key-value pairs from the source system
example: { "fieldName": "fieldValue"}
additionalProperties: true
owner:
$ref: '../schemas/gov-entitlement/OwnerReferenceDto.yaml'
directPermissions:
type: array
items:
$ref: './PermissionDto.yaml'
segments:
type: array
items:
@@ -72,9 +85,15 @@ properties:
"f7b1b8a3-5fed-4fd4-ad29-82014e137e19",
"29cb6c06-1da8-43ea-8be4-b3125f248f2a"
]
directPermissions:
type: array
items:
$ref: './PermissionDto.yaml'
owner:
$ref: '../schemas/gov-entitlement/OwnerReferenceDto.yaml'
manuallyUpdatedFields:
description: >-
Object contains entitlement manually updated fields.
Field value is true if is was updated manually via entitlement import csv or patch endpoint.
Field value is false if that property value has not been changed after first entitlement aggregation.
Values for all manually updatable fields must be specified.
For now only two entitlement fields support this: DISPLAY_NAME and DESCRIPTION.
example: {
"DISPLAY_NAME": true,
"DESCRIPTION": true
}
$ref: '../schemas/gov-entitlement/ManuallyUpdatedFieldsDTO.yaml'

View File

@@ -6,4 +6,5 @@ enum:
- CCG
- VA
- INTERNAL
- IIQ_HARVESTER
- null

View File

@@ -4,6 +4,7 @@ properties:
type: string
description: Policy id
example: "0f11f2a4-7c94-4bf3-a2bd-742580fe3bde"
readOnly: true
name:
type: string
description: Policy Business Name
@@ -13,21 +14,25 @@ properties:
format: date-time
description: The time when this SOD policy is created.
example: "2020-01-01T00:00:00.000000Z"
readOnly: true
modified:
type: string
format: date-time
description: The time when this SOD policy is modified.
example: "2020-01-01T00:00:00.000000Z"
readOnly: true
description:
type: string
description: Optional description of the SOD policy
example: "This policy ensures compliance of xyz"
nullable: true
ownerRef:
$ref: '../../v3/schemas/BaseReferenceDto.yaml'
externalPolicyReference:
type: string
description: Optional External Policy Reference
example: "XYZ policy"
nullable: true
policyQuery:
type: string
description: Search query of the SOD policy
@@ -36,10 +41,12 @@ properties:
type: string
description: Optional compensating controls(Mitigating Controls)
example: "Have a manager review the transaction decisions for their \"out of compliance\" employee"
nullable: true
correctionAdvice:
type: string
description: Optional correction advice
example: "Based on the role of the employee, managers should remove access that is not required for their job function."
nullable: true
state:
type: string
description: whether the policy is enforced or not
@@ -57,11 +64,13 @@ properties:
type: string
description: Policy's creator ID
example: "0f11f2a4-7c94-4bf3-a2bd-742580fe3bde"
readOnly: true
modifierId:
type: string
description: Policy's modifier ID
example: "0f11f2a4-7c94-4bf3-a2bd-742580fe3bde"
nullable : true
readOnly: true
violationOwnerAssignmentConfig:
$ref: './ViolationOwnerAssignmentConfig.yaml'
nullable: true
@@ -69,6 +78,7 @@ properties:
type: boolean
description: defines whether a policy has been scheduled or not
example: true
default: false
type:
type: string
description: whether a policy is query based or conflicting access based
@@ -78,5 +88,6 @@ properties:
- CONFLICTING_ACCESS_BASED
example: GENERAL
conflictingAccessCriteria:
$ref: './ConflictingAccessCriteria.yaml'
nullable: true
allOf:
- $ref: './ConflictingAccessCriteria.yaml'
- nullable: true

View File

@@ -12,10 +12,18 @@ properties:
type: string
description: The connector script name
example: "servicenow"
features:
type: array
description: The list of features supported by the connector
nullable: true
items:
type: string
example: ["PROVISIONING", "SYNC_PROVISIONING", "SEARCH", "UNSTRUCTURED_TARGETS"]
directConnect:
type: boolean
description: true if the source is a direct connect source
example: true
default: false
connectorMetadata:
type: object
description: Object containing metadata pertinent to the UI to be used

View File

@@ -5,6 +5,7 @@ properties:
enum:
- MANAGER
- STATIC
- null
description: >-
Details about the violations owner.
@@ -12,6 +13,9 @@ properties:
STATIC - Governance Group or Identity
example: MANAGER
nullable: true
ownerRef:
$ref: '../../v3/schemas/BaseReferenceDto.yaml'
allOf:
- $ref: '../../v3/schemas/BaseReferenceDto.yaml'
- nullable: true

View File

@@ -36,6 +36,7 @@ value:
autoRevokeAllowed: false
recommendationsEnabled: false
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -43,4 +43,5 @@ value:
autoRevokeAllowed: false
recommendationsEnabled: false
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -42,4 +42,5 @@ value:
emailNotificationEnabled: true
autoRevokeAllowed: false
recommendationsEnabled: false
correlatedStatus: CORRELATED
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -38,5 +38,6 @@ value:
autoRevokeAllowed: false
recommendationsEnabled: false
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -34,6 +34,7 @@
emailNotificationEnabled: true
autoRevokeAllowed: false
recommendationsEnabled: false
mandatoryCommentRequirement: NO_DECISIONS
- id: b7e6459eed5247ac8b98a5fed81fe27f
name: Reporting Access Review
created: 2022-07-28T19:19:40.035Z
@@ -77,6 +78,7 @@
emailNotificationEnabled: true
autoRevokeAllowed: false
recommendationsEnabled: false
mandatoryCommentRequirement: NO_DECISIONS
- id: b9f41bc69e7a4291b9de0630396d030d
name: Campaign With Admin Role
created: 2022-08-02T13:40:36.857Z
@@ -120,6 +122,7 @@
emailNotificationEnabled: false
autoRevokeAllowed: false
recommendationsEnabled: false
mandatoryCommentRequirement: NO_DECISIONS
- id: b9f41bc69e7a4291b9de0630396d030d
name: AD Source Review
created: 2022-08-02T13:40:36.857Z
@@ -158,4 +161,5 @@
emailNotificationEnabled: true
autoRevokeAllowed: false
recommendationsEnabled: false
correlatedStatus: CORRELATED
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -9,6 +9,7 @@ value:
filter:
type: CAMPAIGN_FILTER
id: 0c46fb26c6b20967a55517ee90d15b93
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -16,4 +16,5 @@ value:
name: SailPoint Support
roleIds:
- b15d609fc5c8434b865fe552315fda8f
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -12,4 +12,5 @@ value:
searchCampaignInfo:
type: ACCESS
query: user
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -13,4 +13,5 @@ value:
sourceIds:
- 612b31b1a0f04aaf83123bdb80e70db6
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -15,4 +15,5 @@ value:
id: e0adaae69852e8fe8b8a3d48e5ce757c
emailNotificationEnabled: true
autoRevokeAllowed: false
recommendationsEnabled: false
recommendationsEnabled: false
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -16,4 +16,5 @@ value:
id: 2c9180876ab2c053016ab6f65dfd5aaa
emailNotificationEnabled: true
autoRevokeAllowed: false
recommendationsEnabled: false
recommendationsEnabled: false
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -18,4 +18,5 @@ value:
description: Identities with reporting abilities
emailNotificationEnabled: true
autoRevokeAllowed: false
recommendationsEnabled: false
recommendationsEnabled: false
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -18,4 +18,5 @@ value:
emailNotificationEnabled: true
autoRevokeAllowed: false
recommendationsEnabled: false
correlatedStatus: CORRELATED
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -22,3 +22,4 @@ value:
totalCertifications: 0
completedCertifications: 0
sourcesWithOrphanEntitlements: null
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -32,4 +32,5 @@ value:
totalCertifications: 0
completedCertifications: 0
sourcesWithOrphanEntitlements: null
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -31,3 +31,4 @@ value:
totalCertifications: 0
completedCertifications: 0
sourcesWithOrphanEntitlements: null
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -25,4 +25,5 @@ value:
completedCertifications: 0
sourcesWithOrphanEntitlements: null
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -38,6 +38,7 @@ value:
totalCertifications: 0
completedCertifications: 0
sourcesWithOrphanEntitlements: null
mandatoryCommentRequirement: NO_DECISIONS
- id: 1be8fc1103914bf0a4e14e316b6a7b7c
name: Manager Review
description: A review of everyone's access by their manager.
@@ -61,6 +62,7 @@ value:
totalCertifications: 5
completedCertifications: 0
sourcesWithOrphanEntitlements: []
mandatoryCommentRequirement: NO_DECISIONS
- id: 7e1a731e3fb845cfbe58112ba4673ee4
name: Search Campaign
description: Search Campaign for Identities
@@ -93,6 +95,7 @@ value:
totalCertifications: 6
completedCertifications: 0
sourcesWithOrphanEntitlements: []
mandatoryCommentRequirement: NO_DECISIONS
- id: ad3cf3dd50394b1bad646de4bc51b999
name: Source Owner Campaign
description: Example for Source Owner Campaign
@@ -118,4 +121,5 @@ value:
totalCertifications: 2
completedCertifications: 0
sourcesWithOrphanEntitlements: []
correlatedStatus: CORRELATED
correlatedStatus: CORRELATED
mandatoryCommentRequirement: NO_DECISIONS

View File

@@ -0,0 +1,20 @@
type: object
properties:
DISPLAY_NAME:
type: boolean
default: false
description: >-
True if the entitlements name was updated manually via entitlement import csv or patch endpoint.
False means that property value has not been change after first entitlement aggregation.
Field refers to [Entitlement response schema](https://developer.sailpoint.com/idn/api/beta/get-entitlement) > `name` property.
example: true
DESCRIPTION:
type: boolean
default: false
description: >-
True if the entitlement description was updated manually via entitlement import csv or patch endpoint.
False means that property value has not been change after first entitlement aggregation.
Field refers to [Entitlement response schema](https://developer.sailpoint.com/idn/api/beta/get-entitlement) > `description` property.
example: true

View File

@@ -1,7 +1,7 @@
type: string
description: Enum list of valid work types that can be selected for a Reassignment Configuration
enum:
- accessRequests
- certifications
- manualTasks
example: accessRequests
- ACCESS_REQUESTS
- CERTIFICATIONS
- MANUAL_TASKS
example: ACCESS_REQUESTS

View File

@@ -8,4 +8,4 @@ properties:
name:
type: string
description: Human-readable display name of the object
example: William.Wilson
example: William Wilson

View File

@@ -1,7 +0,0 @@
type: string
description: Enum list of valid work types that can be selected for a Reassignment Configuration
enum:
- accessRequests
- certifications
- manualTasks
example: accessRequests

View File

@@ -1568,7 +1568,7 @@ paths:
$ref: './beta/paths/sod-violation-report.yaml'
/sod-violations/predict:
$ref: './beta/paths/sod/predict-violations.yaml'
/sod-violation-report-status/{reportResultId}:
/sod-policies/sod-violation-report-status/{reportResultId}:
$ref: './beta/paths/sod-violation-report-status.yaml'
/sod-violation-report/run:
$ref: './beta/paths/sod-all-report-run.yaml'

View File

@@ -927,12 +927,6 @@ paths:
$ref: './v3/paths/password-org-config.yaml'
/sod-config/public-keys/target:
$ref: "./v3/paths/sod-public-key-target.yaml"
/sod-exceptions:
$ref: "./v3/paths/sod-exceptions.yaml"
/sod-exceptions/{id}:
$ref: "./v3/paths/sod-exception.yaml"
/sod-exceptions/bulk-create:
$ref: "./v3/paths/sod-exceptions-bulk-create.yaml"
/sod-policies:
$ref: './v3/paths/sod-policies.yaml'
/sod-policies/{id}:
@@ -947,7 +941,7 @@ paths:
$ref: './v3/paths/sod-violation-report.yaml'
/sod-risks/risks/{id}:
$ref: "./v3/paths/sod-arm-risk.yaml"
/sod-violation-report-status/{reportResultId}:
/sod-policies/sod-violation-report-status/{reportResultId}:
$ref: './v3/paths/sod-violation-report-status.yaml'
/sod-violations/predict:
$ref: "./v3/paths/sod-violations-predict.yaml"

View File

@@ -44,6 +44,18 @@ get:
**uncorrelated**: *eq*
required: false
- in: query
name: sorters
required: false
schema:
type: string
format: comma-separated
example: id,name
description: >-
Sort results using the standard syntax described in [V3 API Standard Collection Parameters](https://developer.sailpoint.com/idn/api/standard-collection-parameters#sorting-results)
Sorting is supported for the following fields: **id**, **name**, **created**, **modified**
responses:
"200":
description: List of account objects
@@ -71,6 +83,8 @@ post:
description: >-
This API submits an account creation task and returns the task ID.
The `sourceId` where this account will be created must be included in the `attributes` object.
A token with ORG_ADMIN authority is required to call this API.
security:
- oauth2: [idn:accounts:manage]

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