Merge pull request #13 from sailpoint-oss/added-report-form

Added pagination report and example source description update form
This commit is contained in:
Luke Hagar
2024-03-05 09:25:02 -05:00
committed by GitHub
25 changed files with 635 additions and 83 deletions

View File

@@ -1,10 +1,10 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/logo.ico" /> <link rel="icon" href="/logo.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>IdentityNow Admin Console</title> <title>IdentityNow Starter Application</title>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-theme="wintry"> <body data-theme="wintry">

View File

@@ -17,22 +17,32 @@ export const handle: Handle = async ({ event, resolve }) => {
if (hasSession) { if (hasSession) {
event.locals.session = getSession(event.cookies); event.locals.session = getSession(event.cookies);
if (hasIdnSession) { if (hasIdnSession) {
event.locals.idnSession = await getToken(event.cookies); const session = await getToken(event.cookies);
if (!session) {
event.locals.hasIdnSession = false;
event.locals.idnSession = undefined;
} else {
event.locals.idnSession = session;
const lastToken = lastCheckedToken(event.cookies); const lastToken = lastCheckedToken(event.cookies);
const tokenDetails = getTokenDetails(event.cookies); const tokenDetails = getTokenDetails(event.cookies);
if (tokenDetails && lastToken != '' && lastToken === event.locals.idnSession.access_token) { if (tokenDetails && lastToken != '' && lastToken === event.locals.idnSession.access_token) {
event.locals.tokenDetails = tokenDetails event.locals.tokenDetails = tokenDetails;
} else { } else {
event.locals.tokenDetails = await checkToken( const tempTokenDetails = await checkToken(
event.locals.session.baseUrl, event.locals.session.baseUrl,
event.locals.idnSession.access_token event.locals.idnSession.access_token
); );
if (tempTokenDetails) {
event.locals.tokenDetails = tempTokenDetails;
event.cookies.set('tokenDetails', JSON.stringify(event.locals.tokenDetails), { event.cookies.set('tokenDetails', JSON.stringify(event.locals.tokenDetails), {
path: '/', path: '/',
httpOnly: false, httpOnly: false,
secure: false secure: false
}); });
}} }
}
}
}
} }
if (event.url.pathname.startsWith('/home') || event.url.pathname.startsWith('/api')) { if (event.url.pathname.startsWith('/home') || event.url.pathname.startsWith('/api')) {

View File

@@ -6,12 +6,13 @@
export let onPageChange: (e: CustomEvent<number>) => void; export let onPageChange: (e: CustomEvent<number>) => void;
export let onAmountChange: (e: CustomEvent<number>) => void; export let onAmountChange: (e: CustomEvent<number>) => void;
export let onGo: (e: KeyboardEvent | MouseEvent) => void; export let onGo: (e: KeyboardEvent | MouseEvent) => void;
export let filters: string = ''; export let filters: string | undefined = undefined;
export let sorters: string = ''; export let sorters: string | undefined = undefined;
</script> </script>
<div class=" p-4 flex flex-row flex-wrap justify-between gap-4"> <div class=" p-4 flex flex-row flex-wrap justify-between gap-4">
<div class="flex flex-row gap-1"> <div class="flex flex-row gap-1">
{#if filters !== undefined}
<input <input
on:keydown={onGo} on:keydown={onGo}
bind:value={filters} bind:value={filters}
@@ -20,6 +21,8 @@
type="text" type="text"
placeholder="Filter" placeholder="Filter"
/> />
{/if}
{#if sorters !== undefined}
<input <input
on:keydown={onGo} on:keydown={onGo}
bind:value={sorters} bind:value={sorters}
@@ -28,7 +31,10 @@
type="text" type="text"
placeholder="Sorter" placeholder="Sorter"
/> />
{/if}
{#if filters !== undefined || sorters !== undefined}
<button on:click={onGo} class="btn variant-filled-primary !text-white"> Go </button> <button on:click={onGo} class="btn variant-filled-primary !text-white"> Go </button>
{/if}
</div> </div>
<p class="my-auto">Total Count: {totalCount}</p> <p class="my-auto">Total Count: {totalCount}</p>
<Paginator <Paginator

View File

@@ -7,7 +7,7 @@ export function formatDate(date: string | null | undefined) {
} }
export function getLimit(url: URL) { export function getLimit(url: URL) {
return url.searchParams.get('limit') || '250'; return url.searchParams.get('limit') || '5';
} }
export function getFilters(url: URL) { export function getFilters(url: URL) {
@@ -71,10 +71,10 @@ export function createOnGo(params: PaginationParams, path: string) {
if (e.type !== 'click' && (e as KeyboardEvent).key !== 'Enter') return; if (e.type !== 'click' && (e as KeyboardEvent).key !== 'Enter') return;
const urlParams = new URLSearchParams(); const urlParams = new URLSearchParams();
urlParams.set('page', params.page); if (params.page != '') urlParams.set('page', params.page);
urlParams.set('limit', params.limit); if (params.limit != '') urlParams.set('limit', params.limit);
urlParams.set('sorters', params.sorters); if (params.sorters != '') urlParams.set('sorters', params.sorters);
urlParams.set('filters', params.filters); if (params.filters != '') urlParams.set('filters', params.filters);
console.log(`${path}?${urlParams.toString()}`); console.log(`${path}?${urlParams.toString()}`);

View File

@@ -1,5 +1,4 @@
import HomeSvg from '$lib/Components/SVGs/HomeSVG.svelte'; import HomeSvg from '$lib/Components/SVGs/HomeSVG.svelte';
import MessagesSvg from '$lib/Components/SVGs/MessagesSVG.svelte';
import ReportsSvg from '$lib/Components/SVGs/ReportsSVG.svelte'; import ReportsSvg from '$lib/Components/SVGs/ReportsSVG.svelte';
export const navigation = [ export const navigation = [
@@ -13,10 +12,20 @@ export const navigation = [
icon: HomeSvg icon: HomeSvg
}, },
{ {
url: '/home/Example Pages', url: '/home/example-pages',
name: 'Reports', name: 'Example Pages',
description: 'a list of example pages showcasing how to implement the IdentityNow SDK.', description: 'a list of example pages showcasing how to implement the IdentityNow SDK.',
icon: ReportsSvg icon: ReportsSvg
},
{
url: '/home/example-form',
name: 'Example Form',
description: 'A form example using the IdentityNow SDK.'
},
{
url: '/home/form-integration',
name: 'SailPoint Form Integration',
description: 'A form example using the IdentityNow SDK.'
} }
] ]
} }

View File

@@ -73,7 +73,7 @@ export function setTokenDetails(cookies: Cookies, tokenDetails: TokenDetails) {
}); });
} }
export async function checkToken(apiUrl: string, token: string): Promise<TokenDetails> { export async function checkToken(apiUrl: string, token: string): Promise<TokenDetails | undefined> {
const body = 'token=' + token; const body = 'token=' + token;
const url = `${apiUrl}/oauth/check_token/`; const url = `${apiUrl}/oauth/check_token/`;
const response = await axios.post(url, body).catch(function (err) { const response = await axios.post(url, body).catch(function (err) {
@@ -85,15 +85,19 @@ export async function checkToken(apiUrl: string, token: string): Promise<TokenDe
} }
return undefined; return undefined;
}); });
// if (response) { if (!response) {
// console.log(response.data); return undefined;
// } }
const tokenDetails = response!.data;
const tokenDetails = response.data;
return tokenDetails; return tokenDetails;
} }
export async function refreshToken(apiUrl: string, refreshToken: string): Promise<IdnSession> { export async function refreshToken(
apiUrl: string,
refreshToken: string
): Promise<IdnSession | undefined> {
const url = `${apiUrl}/oauth/token?grant_type=refresh_token&client_id=sailpoint-cli&refresh_token=${refreshToken}`; const url = `${apiUrl}/oauth/token?grant_type=refresh_token&client_id=sailpoint-cli&refresh_token=${refreshToken}`;
const response = await axios.post(url).catch(function (err) { const response = await axios.post(url).catch(function (err) {
if (err.response) { if (err.response) {
@@ -104,10 +108,12 @@ export async function refreshToken(apiUrl: string, refreshToken: string): Promis
} }
return undefined; return undefined;
}); });
// if (response) {
// console.log(response.data) if (!response) {
// } return undefined;
const idnSession: IdnSession = response!.data as IdnSession; }
const idnSession: IdnSession = response.data as IdnSession;
return idnSession; return idnSession;
} }
@@ -147,7 +153,7 @@ export function getSession(cookies: Cookies): Session {
return JSON.parse(sessionString) as Session; return JSON.parse(sessionString) as Session;
} }
export async function getToken(cookies: Cookies): Promise<IdnSession> { export async function getToken(cookies: Cookies): Promise<IdnSession | undefined> {
const sessionString = cookies.get('session'); const sessionString = cookies.get('session');
const idnSessionString = cookies.get('idnSession'); const idnSessionString = cookies.get('idnSession');
@@ -171,12 +177,17 @@ export async function getToken(cookies: Cookies): Promise<IdnSession> {
if (isJwtExpired(idnSession.access_token)) { if (isJwtExpired(idnSession.access_token)) {
console.log('Refreshing IdnSession token...'); console.log('Refreshing IdnSession token...');
const newSession = await refreshToken(session.baseUrl, idnSession.refresh_token); const newSession = await refreshToken(session.baseUrl, idnSession.refresh_token);
if (newSession) {
cookies.set('idnSession', JSON.stringify(newSession), { cookies.set('idnSession', JSON.stringify(newSession), {
path: '/', path: '/',
httpOnly: false, httpOnly: false,
secure: false secure: false
}); });
return Promise.resolve(newSession); return Promise.resolve(newSession);
} else {
console.log('IdnSession token is expired');
return Promise.resolve(undefined);
}
} else { } else {
console.log('IdnSession token is good'); console.log('IdnSession token is good');
return Promise.resolve(idnSession); return Promise.resolve(idnSession);

View File

@@ -177,7 +177,7 @@
<!-- <svelte:fragment slot="sidebarRight">Sidebar Right</svelte:fragment> --> <!-- <svelte:fragment slot="sidebarRight">Sidebar Right</svelte:fragment> -->
<!-- <svelte:fragment slot="pageHeader">Page Header</svelte:fragment> --> <!-- <svelte:fragment slot="pageHeader">Page Header</svelte:fragment> -->
<!-- Router Slot --> <!-- Router Slot -->
<div class="flex flex-col"> <div class="flex flex-col h-full">
{#if crumbs.length > 0} {#if crumbs.length > 0}
<div class="pl-2 pt-2 pr-2"> <div class="pl-2 pt-2 pr-2">
<ol class="breadcrumb card p-2"> <ol class="breadcrumb card p-2">

View File

@@ -0,0 +1,53 @@
// src/routes/api/post.ts
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import { error, json } from '@sveltejs/kit';
import {
CustomFormsBetaApi,
type CustomFormsBetaApiCreateFormInstanceRequest
} from 'sailpoint-api-client';
export const POST = async ({ request, locals }) => {
const config = createConfiguration(locals.session!.baseUrl, locals.idnSession!.access_token);
const api = new CustomFormsBetaApi(config);
const { formDefinitionId, formInput } = await request.json();
if (!formDefinitionId || !formInput) {
error(400, 'formDefinitionId and formInput are required');
}
const expireDate = new Date();
expireDate.setDate(expireDate.getDate() + 1);
const formInstance: CustomFormsBetaApiCreateFormInstanceRequest = {
body: {
createdBy: {
id: 'BOOYAH',
type: 'SOURCE'
},
expire: expireDate.toISOString(),
formDefinitionId: formDefinitionId,
formInput: {
...formInput
},
recipients: [
{
id: locals.idnSession?.identity_id,
type: 'IDENTITY'
}
],
standAloneForm: true,
state: 'ASSIGNED',
ttl: 1571827560
}
};
const formInstanceResp = await api.createFormInstance(formInstance);
// Process the request body and perform any necessary operations
// ...
// Return the response
return json({ formDefinitionId, formInput, formInstanceResp: formInstanceResp.data });
};

View File

@@ -1,18 +1,11 @@
<script lang="ts"> <div class="grid place-content-center h-full">
import ResourceLinksCard from '$lib/Components/HomepageCards/ResourceLinksCard.svelte'; <p class="text-center px-60">
import StatusCard from '$lib/Components/HomepageCards/StatusCard.svelte'; This starter application is meant to be an example of how you can build on top of the
import SupportLinksCard from '$lib/Components/HomepageCards/SupportLinksCard.svelte'; IdentityNow UI development kit to build your own applications and tools for IdentityNow
import TenantLinksCard from '$lib/Components/HomepageCards/TenantLinksCard.svelte';
export let data; <br />
console.log(data);
</script>
<div class="flex flex-col gap-2 grow"> On the left hand side you will see some example pages showcasing some different kinds of pages
<div class="flex flex-row flex-wrap gap-2 grow"> you could build. Each page is meant to be a starting point for you to build your own pages.
<StatusCard /> </p>
<TenantLinksCard tenantUrl={data.session?.tenantUrl} />
<ResourceLinksCard />
<SupportLinksCard />
</div>
</div> </div>

View File

@@ -0,0 +1,51 @@
import { createConfiguration } from '$lib/sailpoint/sdk';
import {
Paginator,
SourcesApi,
type Source,
type SourcesApiUpdateSourceRequest
} from 'sailpoint-api-client';
import type { Actions } from './$types';
export const actions = {
default: async ({ locals, request }) => {
const data = await request.formData();
console.log('default action');
console.log('data', data);
const config = createConfiguration(locals.session!.baseUrl, locals.idnSession!.access_token);
const api = new SourcesApi(config);
const source = JSON.parse(data.get('source')?.toString() || '{}');
const updatedDescription = data.get('updatedDescription')?.toString();
const params: SourcesApiUpdateSourceRequest = {
id: source.id,
jsonPatchOperation: [{ op: 'replace', path: '/description', value: updatedDescription }]
};
const resp = await api.updateSource(params);
if (resp.status !== 200) {
return { status: 'error', error: resp.statusText };
}
return { status: 'success' };
}
} satisfies Actions;
export const load = async ({ locals }) => {
const config = createConfiguration(locals.session!.baseUrl, locals.idnSession!.access_token);
const api = new SourcesApi(config);
const sourceResp = Paginator.paginate(api, api.listSources, { limit: 1000 });
const sources = new Promise<Source[]>((resolve) => {
sourceResp.then((response) => {
resolve(response.data);
});
});
return { sources };
};

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import { enhance } from '$app/forms';
import Progress from '$lib/Components/Progress.svelte';
import { onMount } from 'svelte';
import type { Writable } from 'svelte/store';
import { localStorageStore } from '@skeletonlabs/skeleton';
export let data;
const selectedSource: Writable<any> = localStorageStore('selectedSource', undefined);
const updatedDescription: Writable<string> = localStorageStore('updatedDescription', '');
onMount(async () => {
const sources = await data.sources;
if (sources.length > 0) {
$selectedSource = JSON.stringify(sources[0]);
$updatedDescription = sources[0].description || '';
}
});
const handleChange = (e: any) => {
$updatedDescription = JSON.parse(e.target.value).description || '';
};
$: console.log($selectedSource);
</script>
<div class="flex justify-center flex-col align-middle gap-2">
<div class="card p-4">
<p class="text-2xl text-center">Example Form</p>
</div>
<form method="POST" class="card p-4">
<p class="text-2xl text-center">Update Source Description</p>
<div class="flex flex-col gap-4">
{#await data.sources}
<div class="flex flex-row justify-center">
<Progress width="w-[100px]" />
</div>
{:then sources}
<label>
<span>Sources:</span>
<select
on:change={handleChange}
name="source"
placeholder="Select a source"
bind:value={$selectedSource}
class="select"
>
<option hidden disabled>Select a source</option>
{#each sources as source}
{@const sourceString = JSON.stringify(source)}
<option value={sourceString} selected={$selectedSource === sourceString}>
{source.name} - {source.type}
</option>
{/each}
</select>
</label>
<label>
<span>Description:</span>
<textarea name="updatedDescription" class="textarea" bind:value={$updatedDescription} />
</label>
{/await}
<button type="submit" class="btn variant-filled">Submit</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,44 @@
import { getLimit, getPage, getSorters } from '$lib/Utils.js';
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import type { IdentityDocument, Search, SearchApiSearchPostRequest } from 'sailpoint-api-client';
import { SearchApi } from 'sailpoint-api-client';
export const load = async ({ url, locals }) => {
const config = createConfiguration(locals.session!.baseUrl, locals.idnSession!.access_token);
const api = new SearchApi(config);
const page = getPage(url);
const limit = getLimit(url);
const sorters = getSorters(url);
const search: Search = {
indices: ['identities'],
query: {
query: `*`
},
sort: sorters !== '' ? [sorters] : ['name']
};
const requestParams: SearchApiSearchPostRequest = {
search,
offset: Number(page) * Number(limit),
limit: Number(limit),
count: true
};
const reportResp = api.searchPost(requestParams);
const totalCount = new Promise<number>((resolve) => {
reportResp.then((response) => {
resolve(response.headers['x-total-count']);
});
});
const reportData = new Promise<IdentityDocument[]>((resolve) => {
reportResp.then((response) => {
resolve(response.data);
});
});
return { reportData, totalCount, params: { page, limit, sorters } };
};

View File

@@ -0,0 +1,123 @@
<script lang="ts">
import Paginator from '$lib/Components/Paginator.svelte';
import Progress from '$lib/Components/Progress.svelte';
import {
TriggerCodeModal,
createOnAmountChange,
createOnGo,
createOnPageChange,
formatDate
} from '$lib/Utils.js';
import { getModalStore } from '@skeletonlabs/skeleton';
export let data;
const modalStore = getModalStore();
$: onPageChange = createOnPageChange(
{ ...data.params, filters: '', sorters },
'/home/reports/list-of-identities'
);
$: onAmountChange = createOnAmountChange(
{ ...data.params, filters: '', sorters },
'/home/reports/list-of-identities'
);
$: onGo = createOnGo(
{ ...data.params, filters: '', sorters },
'/home/reports/list-of-identities'
);
let sorters = data.params.sorters || '';
</script>
<div class="flex justify-center flex-col align-middle gap-2">
<div class="card p-4">
<p class="text-2xl text-center">List of all identities</p>
</div>
{#await data.reportData}
<div class="grid h-full place-content-center p-8">
<Progress width="w-[100px]" />
</div>
{:then reportData}
{#await data.totalCount then totalCount}
{#if totalCount > 250 || Number(data.params.limit) < totalCount}
<div class="card p-4">
<Paginator
{onAmountChange}
{onGo}
{onPageChange}
settings={{
page: Number(data.params.page),
limit: Number(data.params.limit),
size: totalCount,
amounts: [5, 10, 50, 100, 250]
}}
bind:sorters
{totalCount}
/>
</div>
{/if}
{/await}
{#if reportData.length === 0}
<div class="card p-4">
<p class=" text-center text-success-500">No inactive identities with access found</p>
</div>
{:else}
<div class="table-container">
<table class="table">
<thead class="table-head">
<th> Name </th>
<th> DisplayName </th>
<th> Sources </th>
<th> Created </th>
<th> Modified </th>
<th> Access Count </th>
<th> Entitlement Count </th>
<th> Role Count </th>
<th></th>
</thead>
<tbody class="table-body">
{#each reportData as identity}
<tr>
<td>
{identity.name}
</td>
<td>
{identity.displayName}
</td>
<td>
{identity.accounts?.map((account) => account.source?.name).join(', ')}
</td>
<td>
{formatDate(identity.created)}
</td>
<td>
{formatDate(identity.modified)}
</td>
<td>
{identity.accessCount}
</td>
<td>
{identity.entitlementCount}
</td>
<td>
{identity.roleCount}
</td>
<td>
<div class="flex flex-col justify-center gap-1">
<button
on:click={() => TriggerCodeModal(identity, modalStore)}
class="btn btn-sm variant-filled-primary text-sm !text-white"
>
View
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/await}
</div>

View File

@@ -1,23 +1,28 @@
export const reports = [ export const reports = [
{ {
url: '/home/reports/inactive-identities-with-access', url: '/home/example-pages/list-of-identities',
name: 'List of Identities',
description: 'This report will show all identities in the system'
},
{
url: '/home/example-pages/inactive-identities-with-access',
name: 'Inactive Identities With Access', name: 'Inactive Identities With Access',
description: description:
'This report will show all identities that are inactive but still have access in sources' 'This report will show all identities that are inactive but still have access in sources'
}, },
{ {
url: '/home/reports/missing-cloud-life-cycle-state', url: '/home/example-pages/missing-cloud-life-cycle-state',
name: 'Missing Cloud Life Cycle State', name: 'Missing Cloud Life Cycle State',
description: 'This report will show all identities that are missing a cloud life cycle state' description: 'This report will show all identities that are missing a cloud life cycle state'
}, },
{ {
url: '/home/reports/source-owner-configured', url: '/home/example-pages/source-owner-configured',
name: 'Source Owner Configured', name: 'Source Owner Configured',
description: 'This report will show all sources and their configured owners' description: 'This report will show all sources and their configured owners'
}, },
{ {
url: '/home/reports/source-aggregations', url: '/home/example-pages/source-aggregations',
name: 'Source Aggregations', name: 'Source Aggregations',
description: 'This report will show all sources and their most recent aggregation events' description: 'This report will show all sources and their most recent aggregation events'
} }

View File

@@ -0,0 +1,19 @@
import { createConfiguration } from '$lib/sailpoint/sdk.js';
import {
CustomFormsBetaApi,
type ExportFormDefinitionsByTenant200ResponseInnerBeta
} from 'sailpoint-api-client';
export const load = async ({ locals }) => {
const config = createConfiguration(locals.session!.baseUrl, locals.idnSession!.access_token);
const api = new CustomFormsBetaApi(config);
const formsResp = api.exportFormDefinitionsByTenant({});
const forms = new Promise<ExportFormDefinitionsByTenant200ResponseInnerBeta[]>((resolve) => {
formsResp.then((response) => {
resolve(response.data);
});
});
return { forms };
};

View File

@@ -0,0 +1,160 @@
<script lang="ts">
import type { ExportFormDefinitionsByTenant200ResponseInnerBeta } from 'sailpoint-api-client';
import Progress from '$lib/Components/Progress.svelte';
export let data;
console.log(data);
let selectedForm: ExportFormDefinitionsByTenant200ResponseInnerBeta;
let dialog: HTMLDialogElement;
let inputs = {};
let loading = false;
let formUrl: string;
let conditions = new Map();
function parseFormConditions(conditions) {
let parsedConditionals = new Map();
console.log(conditions);
for (const condition of conditions || []) {
for (const rule of condition.rules) {
console.log(rule);
const temp = parsedConditionals.get(rule.source) || [];
parsedConditionals.set(rule.source, Array.from(new Set([...temp, rule.value])));
}
}
console.log(parsedConditionals);
return parsedConditionals;
}
$: if (selectedForm) {
conditions = parseFormConditions(selectedForm.object?.formConditions);
}
</script>
<dialog class="card p-8" bind:this={dialog}>
<div class="flex flex-col gap-4">
{#if selectedForm}
<p id="name" class="text-center">
Name: {selectedForm.object?.name}
</p>
<p>
Description: <br />
{selectedForm.object?.description}
</p>
<div>
<p>Form Inputs:</p>
{#if selectedForm.object?.formInput}
{#each selectedForm.object?.formInput as input}
{#if conditions.get(input.label)}
<label class="label" for={input.label}>
<span>{input.label}</span>
<select class="input" id={input.label} bind:value={inputs[input.label]}>
{#each conditions.get(input.label) as condition}
<option value={condition}>{condition}</option>
{/each}
</select>
</label>
{:else}
<label class="label" for={input.label}>
<span>{input.label}</span>
<input
class="input"
id={input.label}
bind:value={inputs[input.label]}
type="text"
/>
</label>
{/if}
{/each}
{/if}
</div>
{/if}
{#if loading}
<div class="flex flex-row justify-center">
<Progress width="w-[80px]" />
</div>
{:else}
{#if formUrl}
<a class="btn variant-filled-secondary" href={formUrl} target="_blank" rel="noreferrer">
Open Form
</a>
{/if}
<button
class="btn variant-filled-primary"
on:click={async () => {
loading = true;
console.log(inputs);
const formResp = await fetch('/api/sailpoint/form/create-instance', {
method: 'POST',
body: JSON.stringify({ formDefinitionId: selectedForm.object?.id, formInput: inputs })
});
const respData = await formResp.json();
console.log(respData);
formUrl = respData.formInstanceResp.standAloneFormUrl;
loading = false;
}}
>
{#if formUrl}
Refresh form link
{:else}
Create form link
{/if}
</button>
{/if}
<button
class="btn variant-filled-warning"
on:click={() => {
dialog.close();
}}
>
Close
</button>
</div>
</dialog>
<div>
{#await data.forms}
<div class="flex flex-row justify-center">
<Progress width="w-[80px]" />
</div>
{:then forms}
<div class="flex flex-row">
{#each forms as form}
<div class="card flex flex-col p-4 gap-4">
<p id="name" class="text-center">
Name: {form.object?.name}
</p>
<p>
ID: {form.object?.id}
</p>
<p>
Description: <br />
{form.object?.description}
</p>
<div>
<p>Form Inputs:</p>
{#if form.object?.formInput}
{#each form.object?.formInput as input}
<p class="">
{input.label}
</p>
{/each}
{/if}
</div>
<button
class="btn variant-filled-primary"
on:click={() => {
selectedForm = form;
dialog.showModal();
}}
>
Assign form
</button>
</div>
{/each}
</div>
{/await}
</div>