1import * as quicksight from "@distilled.cloud/aws/quicksight";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import { AWSEnvironment } from "../Environment.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 readQuickSightTags,
14 syncQuickSightTags,
15 toWireTags,
16 waitForSettled,
17} from "./internal.ts";
18
19/**
20 * Properties for an Amazon QuickSight dashboard — a published, read-only view
21 * built from a template, an analysis, or an inline definition.
22 */
23export interface DashboardProps {
24 /**
25 * Unique id of the dashboard within the account. Stable — changing it
26 * replaces the dashboard. If omitted, a unique id is generated.
27 */
28 dashboardId?: string;
29
30 /**
31 * Display name of the dashboard.
32 */
33 name: string;
34
35 /**
36 * Source of the dashboard content — a template or an existing analysis to
37 * clone. Provide either `sourceEntity` or `definition`.
38 */
39 sourceEntity?: quicksight.DashboardSourceEntity;
40
41 /**
42 * Inline definition of the dashboard content. Provide either `definition`
43 * or `sourceEntity`.
44 */
45 definition?: quicksight.DashboardVersionDefinition;
46
47 /**
48 * Parameters passed to the dashboard's datasets.
49 */
50 parameters?: quicksight.Parameters;
51
52 /**
53 * Resource-level permissions on the dashboard.
54 */
55 permissions?: quicksight.ResourcePermission[];
56
57 /**
58 * Publish options (e.g. ad-hoc filtering, export to CSV).
59 */
60 dashboardPublishOptions?: quicksight.DashboardPublishOptions;
61
62 /**
63 * ARN of the theme applied to the dashboard.
64 */
65 themeArn?: string;
66
67 /**
68 * Description of the created/updated version.
69 */
70 versionDescription?: string;
71
72 /**
73 * Tags to apply to the dashboard.
74 */
75 tags?: Record<string, string>;
76}
77
78export interface Dashboard extends Resource<
79 "AWS.QuickSight.Dashboard",
80 DashboardProps,
81 {
82 /** Unique id of the dashboard within the account. */
83 dashboardId: string;
84 /** ARN of the dashboard. */
85 arn: string;
86 /** Display name of the dashboard. */
87 name: string;
88 /** Current version status (e.g. `CREATION_SUCCESSFUL`). */
89 status: string;
90 },
91 never,
92 Providers
93> {}
94
95/**
96 * An Amazon QuickSight dashboard — a published, read-only view built from a
97 * template, an analysis, or an inline definition.
98 *
99 * QuickSight requires an active account subscription in the region. Without
100 * one, create operations fail with the typed `QuickSightSubscriptionRequired`
101 * error.
102 *
103 * ### Creating a Dashboard
104 * **Example:** Dashboard from a Template
105 * ```typescript
106 * const dashboard = yield* Dashboard("sales-overview", {
107 * name: "Sales Overview",
108 * sourceEntity: {
109 * SourceTemplate: {
110 * Arn: templateArn,
111 * DataSetReferences: [
112 * {
113 * DataSetPlaceholder: "sales",
114 * DataSetArn: dataset.arn,
115 * },
116 * ],
117 * },
118 * },
119 * });
120 * ```
121 *
122 * @resource
123 */
124export const Dashboard = Resource<Dashboard>("AWS.QuickSight.Dashboard");
125
126export const DashboardProvider = () =>
127 Provider.effect(
128 Dashboard,
129 Effect.gen(function* () {
130 const toId = (id: string, props: Partial<DashboardProps>) =>
131 props.dashboardId
132 ? Effect.succeed(props.dashboardId)
133 : createPhysicalName({ id, maxLength: 64 });
134
135 const readDashboard = Effect.fn(function* (
136 accountId: string,
137 dashboardId: string,
138 ) {
139 const response = yield* quicksight
140 .describeDashboard({
141 AwsAccountId: accountId,
142 DashboardId: dashboardId,
143 })
144 .pipe(
145 Effect.catchTag("ResourceNotFoundException", () =>
146 Effect.succeed(undefined),
147 ),
148 );
149 return response?.Dashboard;
150 });
151
152 const settle = (accountId: string, dashboardId: string) =>
153 waitForSettled(
154 dashboardId,
155 readDashboard(accountId, dashboardId).pipe(
156 Effect.map((d) =>
157 d === undefined ? undefined : { ...d, status: d.Version?.Status },
158 ),
159 ),
160 );
161
162 const toAttrs = (dashboard: quicksight.Dashboard) => ({
163 dashboardId: dashboard.DashboardId!,
164 arn: dashboard.Arn!,
165 name: dashboard.Name ?? "",
166 status: dashboard.Version?.Status ?? "",
167 });
168
169 return Dashboard.Provider.of({
170 stables: ["dashboardId", "arn"],
171
172 diff: Effect.fn(function* ({ id, olds = {}, news }) {
173 if (!isResolved(news)) return undefined;
174 if ((yield* toId(id, olds)) !== (yield* toId(id, news))) {
175 return { action: "replace" } as const;
176 }
177 }),
178
179 read: Effect.fn(function* ({ id, olds = {}, output }) {
180 const { accountId } = yield* AWSEnvironment.current;
181 const dashboardId = output?.dashboardId ?? (yield* toId(id, olds));
182 const dashboard = yield* readDashboard(accountId, dashboardId);
183 if (dashboard === undefined) return undefined;
184 const attrs = toAttrs(dashboard);
185 const tags = yield* readQuickSightTags(attrs.arn);
186 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
187 }),
188
189 reconcile: Effect.fn(function* ({ id, news, output, session }) {
190 const { accountId } = yield* AWSEnvironment.current;
191 const dashboardId = output?.dashboardId ?? (yield* toId(id, news));
192 const internalTags = yield* createInternalTags(id);
193 const desiredTags = { ...internalTags, ...news.tags };
194
195 // 1. Observe.
196 let observed = yield* readDashboard(accountId, dashboardId);
197
198 // 2. Ensure — create if missing.
199 if (observed === undefined) {
200 yield* quicksight
201 .createDashboard({
202 AwsAccountId: accountId,
203 DashboardId: dashboardId,
204 Name: news.name,
205 SourceEntity: news.sourceEntity,
206 Definition: news.definition,
207 Parameters: news.parameters,
208 Permissions: news.permissions,
209 DashboardPublishOptions: news.dashboardPublishOptions,
210 ThemeArn: news.themeArn,
211 VersionDescription: news.versionDescription,
212 Tags: toWireTags(desiredTags),
213 })
214 .pipe(
215 Effect.catchTag("ResourceExistsException", () => Effect.void),
216 );
217 } else {
218 // 3. Sync — publish a new version.
219 yield* quicksight.updateDashboard({
220 AwsAccountId: accountId,
221 DashboardId: dashboardId,
222 Name: news.name,
223 SourceEntity: news.sourceEntity,
224 Definition: news.definition,
225 Parameters: news.parameters,
226 DashboardPublishOptions: news.dashboardPublishOptions,
227 ThemeArn: news.themeArn,
228 VersionDescription: news.versionDescription,
229 });
230 }
231
232 observed = yield* settle(accountId, dashboardId);
233 if (observed === undefined) {
234 return yield* Effect.fail(
235 new Error(
236 `QuickSight dashboard '${dashboardId}' not found after reconcile`,
237 ),
238 );
239 }
240
241 // 3b. Sync tags.
242 yield* syncQuickSightTags(observed.Arn!, desiredTags);
243
244 yield* session.note(dashboardId);
245 return toAttrs(observed);
246 }),
247
248 delete: Effect.fn(function* ({ output }) {
249 const { accountId } = yield* AWSEnvironment.current;
250 yield* quicksight
251 .deleteDashboard({
252 AwsAccountId: accountId,
253 DashboardId: output.dashboardId,
254 })
255 .pipe(
256 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
257 );
258 }),
259
260 list: () =>
261 Effect.gen(function* () {
262 const { accountId } = yield* AWSEnvironment.current;
263 return yield* quicksight.listDashboards
264 .pages({ AwsAccountId: accountId })
265 .pipe(
266 Stream.runCollect,
267 Effect.map((chunk) =>
268 Array.from(chunk)
269 .flatMap((page) => page.DashboardSummaryList ?? [])
270 .flatMap((s) =>
271 s.DashboardId !== undefined && s.Arn !== undefined
272 ? [
273 {
274 dashboardId: s.DashboardId,
275 arn: s.Arn,
276 name: s.Name ?? "",
277 status: "",
278 },
279 ]
280 : [],
281 ),
282 ),
283 );
284 }),
285 });
286 }),
287 );