diff --git a/.changeset/rotten-guests-decide.md b/.changeset/rotten-guests-decide.md new file mode 100644 index 00000000..511318cf --- /dev/null +++ b/.changeset/rotten-guests-decide.md @@ -0,0 +1,5 @@ +--- +'@propeldata/ui-kit': minor +--- + +Added experimental Report component diff --git a/packages/ui-kit/src/components/Report/Report.module.scss b/packages/ui-kit/src/components/Report/Report.module.scss new file mode 100644 index 00000000..b5c24b07 --- /dev/null +++ b/packages/ui-kit/src/components/Report/Report.module.scss @@ -0,0 +1,66 @@ +.container { + height: 100%; + align-items: center; +} + +.container > header { + display: flex; + justify-content: space-between; +} + +.container > header > p { + display: flex; + align-items: center; + gap: 4px; +} + +.container > header > p > a { + color: #7f56d9; + display: flex; + align-items: center; + text-decoration: none; +} + +.reportGrid { + display: grid; + + height: 100%; + + gap: 1rem; +} + +.loader { + height: 100% !important; +} + +.loader > div[data-role="loader-animation"] { + height: 100% !important; +} + +.card { + display: block; +} + +.card > div { + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + +.clickable { + cursor: pointer; +} + +.cardTitle { + align-self: flex-start; +} + +.cardTitle > h2 { + margin: 0; + font-size: 18px; +} + +.pie > div > canvas { + max-height: 240px !important; +} diff --git a/packages/ui-kit/src/components/Report/Report.stories.tsx b/packages/ui-kit/src/components/Report/Report.stories.tsx new file mode 100644 index 00000000..0007308f --- /dev/null +++ b/packages/ui-kit/src/components/Report/Report.stories.tsx @@ -0,0 +1,253 @@ +import React from 'react' + +import { Meta, StoryObj } from '@storybook/react' +import { quotedStringRegex, storybookCodeTemplate } from '../../helpers' +import { Report } from './Report' +import { AccessTokenProvider } from '../AccessTokenProvider' +import { ThemeProvider } from '../ThemeProvider' + +type Story = StoryObj + +const meta: Meta = { + title: 'Components/Report', + component: Report, + parameters: { + controls: { sort: 'alpha' }, + imports: 'PieChart, RelativeTimeRange', + transformBody: (body: string) => body.replace(quotedStringRegex('LAST_N_DAYS'), 'RelativeTimeRange.LastNDays'), + codeTemplate: storybookCodeTemplate + } +} + +export default meta + +export const Basic: Story = { + args: { + title: 'My Report', + layout: [ + ['timeSeries', 'timeSeries', 'leaderboard'], + ['counter1', 'counter2', 'leaderboard'], + ['timeSeries2', 'timeSeries2', 'timeSeries2'] + ], + charts: [ + { + id: 'timeSeries', + query: `query TimeSeriesQuery($input: TimeSeriesInput!) { + timeSeries(input: $input) { + labels + values + } + }`, + title: 'Time Series Title', + type: 'timeSeries', + variables: `{ + "input": { + "metric": { + "count": { + "dataPool": { + "id": "DPO01HB9W3DCA1756WT6CJ6BV9H48" + } + } + }, + "timeZone": "UTC", + "granularity": "DAY", + "timeRange": { + "relative": "LAST_N_DAYS", + "n": 30 + }, + "filters": [] + } + }` + }, + { + id: `leaderboard`, + query: `query LeaderboardQuery($input: LeaderboardInput!) { + leaderboard(input: $input) { + headers + rows + } + }`, + type: 'pie', + variables: `{ + "input": { + "metric": { + "count": { + "dataPool": { + "name": "TacoSoft Demo Data" + } + } + }, + "sort": "DESC", + "timeZone": "UTC", + "timeRange": { + "relative": "LAST_N_DAYS", + "n": 30 + }, + "rowLimit": 10, + "dimensions": [{ + "columnName": "taco_name" + }], + "filters": [] + } + }` + }, + { + id: 'counter1', + query: `query CounterQuery($input: CounterInput!) { + counter(input: $input) { + value + } + }`, + type: 'counter', + variables: `{ + "input": { + "metric": { + "count": { + "dataPool": { + "id": "DPO01HB9W3DCA1756WT6CJ6BV9H48" + } + } + }, + "timeZone": "UTC", + "timeRange": { + "relative": "LAST_N_DAYS", + "n": 30 + }, + "filters": [] + } + }` + }, + { + id: 'counter2', + query: `query CounterQuery($input: CounterInput!) { + counter(input: $input) { + value + } + }`, + type: 'counter', + variables: `{ + "input": { + "metric": { + "count": { + "dataPool": { + "id": "DPO01HB9W3DCA1756WT6CJ6BV9H48" + } + } + }, + "timeZone": "UTC", + "timeRange": { + "relative": "LAST_N_DAYS", + "n": 30 + }, + "filters": [ + { + "column": "quantity", + "operator": "EQUALS", + "value": "2" + } + ] + } + }` + }, + { + id: 'timeSeries2', + query: `query TimeSeriesQuery($input: TimeSeriesInput!) { + timeSeries(input: $input) { + labels + values + } + }`, + type: 'timeSeries', + variables: `{ + "input": { + "metric": { + "count": { + "dataPool": { + "id": "DPO01HB9W3DCA1756WT6CJ6BV9H48" + } + } + }, + "timeZone": "UTC", + "granularity": "DAY", + "timeRange": { + "relative": "LAST_N_DAYS", + "n": 30 + }, + "filters": [ + { + "column": "quantity", + "operator": "EQUALS", + "value": "2" + } + ] + } + }` + } + ] + }, + render: (args) => ( + + + + ) +} + +export const Test: Story = { + args: { + layout: [ + ['a60675eefa49741608c1610f8f8b25116', 'a60675eefa49741608c1610f8f8b25116', 'a76d31602c6224c2ca3e2e36d2afcc30e'], + ['a19c883f0281c4fa8852d453faba28b8d', 'a877df96b498b45bf84b1a21d022a4fb5', 'a76d31602c6224c2ca3e2e36d2afcc30e'], + ['a7315e8ad8d004c9597e211d75f9117ba', 'a7315e8ad8d004c9597e211d75f9117ba', 'a7315e8ad8d004c9597e211d75f9117ba'] + ], + charts: [ + { + id: 'a60675eefa49741608c1610f8f8b25116', + type: 'counter', + query: '{}', + variables: '{}' + }, + { + id: 'a76d31602c6224c2ca3e2e36d2afcc30e', + type: 'counter', + query: '{}', + variables: '{}' + }, + { + id: 'a19c883f0281c4fa8852d453faba28b8d', + type: 'counter', + query: '{}', + variables: '{}' + }, + { + id: 'a877df96b498b45bf84b1a21d022a4fb5', + type: 'counter', + query: '{}', + variables: '{}' + }, + { + id: 'a7315e8ad8d004c9597e211d75f9117ba', + type: 'counter', + query: '{}', + variables: '{}' + } + ], + reportCardProps: { className: 'shadow-sm' } + }, + render: (args) => ( + + + + ) +} + +export const Connected: Story = { + args: { + query: { + accessToken: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoX3RpbWUiOjE3MTI3NzkzNzEsImNsaWVudF9pZCI6IkFQUDAxSEZQVFc4NjM1WVQ2Q1E5MjVFNUtZM1JOIiwiZXhwIjoxNzEyNzgyOTcxLCJpYXQiOjE3MTI3NzkzNzEsImlzcyI6Imh0dHBzOi8vYXV0aC51cy1lYXN0LTIuZGV2LnByb3BlbGRhdGEuY29tIiwianRpIjoiOTc5OThhYzgtOWMzMC00OTgxLWI2NDMtMzZiNTU0ODk4YzAxIiwic2NvcGUiOiJtZXRyaWM6cXVlcnkgbWV0cmljOnN0YXRzIGRhdGFfcG9vbDpxdWVyeSBkYXRhX3Bvb2w6cmVhZCBkYXRhX3Bvb2w6c3RhdHMgYWRtaW4gYXBwbGljYXRpb246YWRtaW4gbWV0cmljOnJlYWQiLCJzdWIiOiJBUFAwMUhGUFRXODYzNVlUNkNROTI1RTVLWTNSTiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInZlcnNpb24iOjF9.YaZO2h-rt3puhecWBkWYqMI55VBRIOp5CqQx1WOG_JM', + propelApiUrl: 'https://api.us-east-2.dev.propeldata.com/graphql', + reportId: 'RPT01HV4RBM3WETGC1F9ERQZGBENG' + } + }, + render: (args) => +} diff --git a/packages/ui-kit/src/components/Report/Report.tsx b/packages/ui-kit/src/components/Report/Report.tsx new file mode 100644 index 00000000..452563cd --- /dev/null +++ b/packages/ui-kit/src/components/Report/Report.tsx @@ -0,0 +1,124 @@ +import React from 'react' +import classNames from 'classnames' + +import { ErrorFallback } from '../ErrorFallback' +import { withContainer } from '../withContainer' +import { Card } from '../Card' +import { useReportComponents } from '../../hooks' +import { PieChartProps, PieChart } from '../PieChart' + +import componentStyles from './Report.module.scss' +import { ReportProps } from './Report.types' +import { buildGridTemplateAreas } from './utils' +import { TimeSeries, TimeSeriesProps } from '../TimeSeries' +import { Leaderboard, LeaderboardProps } from '../Leaderboard' +import { Counter, CounterProps } from '../Counter' +import { Loader } from '../Loader' +import { useReport } from '../../hooks/useReport' + +const componentMap = { + timeSeries: (args: TimeSeriesProps) => , + leaderboard: (args: LeaderboardProps) => , + counter: (args: CounterProps) => , + pie: (args: PieChartProps) => ( + ({ ...config, options: { ...config.options, maintainAspectRatio: true } })} + {...args} + /> + ), + '': () => null +} + +export const ReportComponent = React.forwardRef((props, ref) => { + const { + title: titleProp, + layout: layoutProp, + charts: chartsProp, + clickable = false, + onCardClick, + reportCardProps, + propelApiUrl, + query, + className, + style, + ...rest + } = props + + const isStatic = query?.accessToken == null + + const { data, isLoading: isLoadingReport } = useReport({ ...query, enabled: !isStatic }) + + const charts = isStatic ? chartsProp : data?.report?.charts + const layout = isStatic ? layoutProp : data?.report?.layout + const title = isStatic ? titleProp : data?.report?.uniqueName + + const { charts: reportCharts, isLoading: isLoadingComponents } = useReportComponents(charts, { + propelApiUrl: propelApiUrl ?? query?.propelApiUrl, + accessToken: query?.accessToken + }) + + const isLoading = (!isStatic && isLoadingReport) || isLoadingComponents + + const gridTemplateAreas = buildGridTemplateAreas(layout) + + return ( +
+ {title != null && ( +
+

{title}

+

+ Powered by{' '} + + Propel + +

+
+ )} +
'1fr').join(' '), + gridTemplateColumns: layout?.[0]?.map(() => '1fr').join(' ') + }} + > + {reportCharts?.map((chart, chartIdx) => ( + clickable && (onCardClick?.(chart) ?? reportCardProps?.onClick?.(e))} + style={{ ...reportCardProps?.style, gridArea: `report-area-${chart.id}` }} + > +
{chart.title != null &&

{chart.title}

}
+
+ {!isLoading ? ( + chart.result != null && + chart.type != null && + componentMap[chart.type as 'timeSeries' | 'leaderboard' | 'counter' | 'pie']({ + ...chart.result[chart.type === 'pie' ? 'leaderboard' : chart.type] + }) + ) : ( + + )} +
+
+ ))} +
+
+ ) +}) + +ReportComponent.displayName = 'ReportComponent' + +/** + * @deprecated + * This component is EXPERIMENTAL, we do not recommend using it in production. + */ +export const Report = withContainer(ReportComponent, ErrorFallback) as typeof ReportComponent diff --git a/packages/ui-kit/src/components/Report/Report.types.ts b/packages/ui-kit/src/components/Report/Report.types.ts new file mode 100644 index 00000000..51f1aaf5 --- /dev/null +++ b/packages/ui-kit/src/components/Report/Report.types.ts @@ -0,0 +1,38 @@ +export interface ChartProp { + id?: string + title?: string + query?: string + type?: 'timeSeries' | 'leaderboard' | 'counter' | '' | string + variables?: string +} + +export interface ReportQueryProp { + reportId: string + /** + * Access token used for the query. While you can pass this one to each component, we recommend wrapping components in the `AccessTokenProvider` instead: + * @example + * ```jsx + * + * + * + * + * + * ``` + * */ + accessToken?: string + propelApiUrl?: string + enabled?: boolean +} + +export interface ReportProps { + title?: string + layout?: string[][] + charts?: ChartProp[] + clickable?: boolean + onCardClick?: (chart: ChartProp | null | undefined) => void + reportCardProps?: React.HTMLAttributes + propelApiUrl?: string + query?: ReportQueryProp + className?: string + style?: React.CSSProperties +} diff --git a/packages/ui-kit/src/components/Report/index.ts b/packages/ui-kit/src/components/Report/index.ts new file mode 100644 index 00000000..e3818666 --- /dev/null +++ b/packages/ui-kit/src/components/Report/index.ts @@ -0,0 +1,2 @@ +export { Report } from './Report' +export * from './Report.types' diff --git a/packages/ui-kit/src/components/Report/utils.ts b/packages/ui-kit/src/components/Report/utils.ts new file mode 100644 index 00000000..53cb759b --- /dev/null +++ b/packages/ui-kit/src/components/Report/utils.ts @@ -0,0 +1,3 @@ +export function buildGridTemplateAreas(layout?: Array | null | undefined>) { + return layout?.map((row) => `"${row?.map((cell) => `report-area-${cell}`).join(' ')}"`).join(' ') +} diff --git a/packages/ui-kit/src/components/index.ts b/packages/ui-kit/src/components/index.ts index 95c0709b..916fba7f 100644 --- a/packages/ui-kit/src/components/index.ts +++ b/packages/ui-kit/src/components/index.ts @@ -14,3 +14,4 @@ export * from './TimeSeries' export * from './withContainer' export * from './FilterProvider' export * from './SimpleFilter' +export * from './Report' diff --git a/packages/ui-kit/src/helpers/graphql/public.graphql b/packages/ui-kit/src/helpers/graphql/public.graphql index 23432505..87ddab18 100644 --- a/packages/ui-kit/src/helpers/graphql/public.graphql +++ b/packages/ui-kit/src/helpers/graphql/public.graphql @@ -729,9 +729,17 @@ enum DataSourceType { """ CLICKHOUSE """ + Indicates a BigQuery Data Source. + """ + BIGQUERY + """ Indicates a Snowflake Data Source. """ Snowflake + """ + Indicates an internal Data Source. + """ + INTERNAL } """ @@ -760,9 +768,15 @@ enum DataSourceStatus { DELETING } +type InternalConnectionSettings { + _: Boolean +} + union ConnectionSettings = SnowflakeConnectionSettings + | InternalConnectionSettings | HttpConnectionSettings + | KafkaConnectionSettings | S3ConnectionSettings | WebhookConnectionSettings @@ -1221,8 +1235,8 @@ type DataPool implements Node & Common { dataRetentionInDays: Int! "The name of the Data Pool's table." table: String! - "The Data Pool's primary timestamp column." - timestamp: Timestamp! + "The Data Pool's primary timestamp column, if any." + timestamp: Timestamp "The Data Pool's tenant ID, if configured." tenant: Tenant @deprecated(reason: "Will be removed; use Data Pool Access Policies instead") "The Data Pool's unique ID column. Propel uses the primary timestamp and a unique ID to compose a primary key for determining whether records should be inserted, deleted, or updated within the Data Pool." @@ -1240,7 +1254,13 @@ type DataPool implements Node & Common { "Settings related to Data Pool syncing." syncing: DataPoolSyncing! "The list of Syncs of the Data Pool." - syncs(filter: SyncsFilter, first: Int, after: String, last: Int, before: String): SyncConnection + syncs( + filter: SyncsFilter @deprecated(reason: "No longer supported") + first: Int + after: String + last: Int + before: String + ): SyncConnection "The list of Metrics powered by the Data Pool." metrics(first: Int, after: String, last: Int, before: String): MetricConnection "The Deletion Jobs that were historically issued to this Data Pool, sorted by creation time, in descending order." @@ -1260,6 +1280,161 @@ type DataPool implements Node & Common { dataPoolAccessPolicies(first: Int, after: String, last: Int, before: String): DataPoolAccessPolicyConnection! "Validates a custom expression against the Data Pool's available columns. If the provided expression is invalid, the ValidateExpressionResult response will contain a reason explaining why." validateExpression(expression: String!): ValidateExpressionResult! + "The Data Pool's table settings." + tableSettings: TableSettings + "The Data Pool's columns that participate in its PARTITION BY clause." + partitionByColumns: [DataPoolColumn!] + "The Data Pool's columns that participate in its PRIMARY KEY clause." + primaryKeyColumns: [DataPoolColumn!] + "The Data Pool's columns that participate in its ORDER BY clause." + orderByColumns: [DataPoolColumn!] +} + +""" +A Data Pool's table settings. + +These describe how the Data Pool's table is created in ClickHouse. +""" +type TableSettings { + "The ClickHouse table engine for the Data Pool's table." + engine: TableEngine + "The PARTITION BY clause for the Data Pool's table." + partitionBy: [String!] + "The PRIMARY KEY clause for the Data Pool's table." + primaryKey: [String!] + "The ORDER BY clause for the Data Pool's table." + orderBy: [String!] +} + +""" +A Data Pool's table settings. + +These describe how the Data Pool's table is created in ClickHouse. +""" +input TableSettingsInput { + """ + The ClickHouse table engine for the Data Pool's table. + + This field is optional. A default will be chosen based on the Data Pool's `timestamp` and `uniqueId` values, if specified. + """ + engine: TableEngineInput + """ + The PARTITION BY clause for the Data Pool's table. + + This field is optional. A default will be chosen based on the Data Pool's `timestamp` and `uniqueId` values, if specified. + """ + partitionBy: [String!] + """ + The PRIMARY KEY clause for the Data Pool's table. + + This field is optional. A default will be chosen based on the Data Pool's `timestamp` and `uniqueId` values, if specified. + """ + primaryKey: [String!] + """ + The ORDER BY clause for the Data Pool's table. + + This field is optional. A default will be chosen based on the Data Pool's `timestamp` and `uniqueId` values, if specified. + """ + orderBy: [String!] +} + +""" +ClickHouse table engine types. +""" +enum TableEngineType { + "The MergeTree table engine." + MERGE_TREE + "The ReplacingMergeTree table engine." + REPLACING_MERGE_TREE + "The SummingMergeTree table engine." + SUMMING_MERGE_TREE +} + +""" +An interface for accessing a table engine's type. +""" +interface HasTableEngineType { + "The type of the table engine." + type: TableEngineType! +} + +""" +A Data Pool's table engine. +""" +union TableEngine = MergeTreeTableEngine | ReplacingMergeTreeTableEngine | SummingMergeTreeTableEngine + +""" +A Data Pool's table engine. +""" +input TableEngineInput @oneOf { + """ + Field for specifying the MergeTree table engine. + """ + mergeTree: MergeTreeTableEngineInput + """ + Field for specifying the ReplacingMergeTree table engine. + """ + replacingMergeTree: ReplacingMergeTreeTableEngineInput + """ + Field for specifying the SummingMergeTree table engine. + """ + summingMergeTree: SummingMergeTreeTableEngineInput +} + +""" +Parameters for the MergeTree table engine. +""" +type MergeTreeTableEngine implements HasTableEngineType { + "The type is always `MERGE_TREE`." + type: TableEngineType! +} + +""" +Parameters for the MergeTree table engine. +""" +input MergeTreeTableEngineInput { + "The type is always `MERGE_TREE`." + type: TableEngineType +} + +""" +Parameters for the ReplacingMergeTree table engine. +""" +type ReplacingMergeTreeTableEngine implements HasTableEngineType { + "The type is always `REPLACING_MERGE_TREE`." + type: TableEngineType! + "The `ver` parameter to the ReplacingMergeTree engine." + ver: String +} + +""" +Parameters for the ReplacingMergeTree table engine. +""" +input ReplacingMergeTreeTableEngineInput { + "The type is always `REPLACING_MERGE_TREE`." + type: TableEngineType + "The `ver` parameter to the ReplacingMergeTree engine." + ver: String +} + +""" +Parameters for the SummingMergeTree table engine. +""" +type SummingMergeTreeTableEngine implements HasTableEngineType { + "The type is always `SUMMING_MERGE_TREE`." + type: TableEngineType! + "The columns argument for the SummingMergeTree table engine" + columns: [String!] +} + +""" +Parameters for the SummingMergeTree table engine. +""" +input SummingMergeTreeTableEngineInput { + "The type is always `SUMMING_MERGE_TREE`." + type: TableEngineType + "The columns argument for the SummingMergeTree table engine" + columns: [String!] } """ @@ -1333,7 +1508,7 @@ type Dimension { """ The statistics for the dimension values. Fetching statistics incurs query costs. """ - stats: DimensionStatistics + stats: DimensionStatistics @deprecated(reason: "Issue normal queries for calculating stats") } """ @@ -1362,6 +1537,41 @@ type DimensionStatistics { query: QueryInfo! } +type MaterializedView implements Node & Common { + "The Materialized View's unique identifier." + id: ID! + "The Materialized View's unique name." + uniqueName: String! + "The Materialized View's description." + description: String! + "The Materialized View's Account." + account: Account! + "The Materialized View's Environment." + environment: Environment! + "The Materialized View's creation date and time in UTC." + createdAt: DateTime! + "The Materialized View's last modification date and time in UTC." + modifiedAt: DateTime! + """ + The Materialized View's creator. It can be either a User ID, an Application ID, or "system" if it was created by Propel. + """ + createdBy: String! + """ + The Materialized View's last modifier. It can be either a User ID, an Application ID, or "system" if it was modified by Propel. + """ + modifiedBy: String! + "The SQL that the Materialized View executes." + sql: String! + """ + The Materialized View's destination (AKA "target") Data Pool. + """ + destination: DataPool! + "The Materialized View's source Data Pool." + source: DataPool! + "Other Data Pools queried by the Materialized View." + others: [DataPool!]! +} + """ The fields of a filter. @@ -1595,11 +1805,19 @@ The fields for creating a Data Pool. """ input CreateDataPoolInputV2 { "The Data Source that will be used to create the Data Pool." - dataSource: ID! + dataSource: ID "The table that the Data Pool will sync from." - table: String! - "The table's primary timestamp column. Propel uses the primary timestamp to order and partition your data in Data Pools. It will serve as the time dimension for your Metrics." - timestamp: TimestampInput! + table: String + """ + The table's primary timestamp column. + + Propel uses the primary timestamp to order and partition your data in Data Pools. It's part of what makes Propel + fast for larger data sets. It will also serve as the time dimension for your Metrics. + + If you do not provide a primary timestamp column, you will need to supply an alternate timestamp when querying your + Data Pool or its Metrics using the TimeRangeInput. + """ + timestamp: TimestampInput "The Data Pool's unique name. If not specified, Propel will set the ID as the unique name." uniqueName: String "The Data Pool's description." @@ -1623,6 +1841,12 @@ input CreateDataPoolInputV2 { Policies in order to query the Data Pool and its Metrics. """ accessControlEnabled: Boolean + """ + Override the Data Pool's table settings. These describe how the Data Pool's table is created in ClickHouse, and a + default will be chosen based on the Data Pool's `timestamp` and `uniqueId` values, if any. You can override these + defaults in order to specify a custom table engine, custom ORDER BY, etc. + """ + tableSettings: TableSettingsInput } """ @@ -1773,19 +1997,22 @@ type Sync implements Node { """ The number of new records contained within the Sync, if known. This excludes filtered records. """ - newRecords: String + newRecords: String @deprecated(reason: "All records are considered to be processed; see `processedRecords` instead") """ The number of updated records contained within the Sync, if known. This excludes filtered records. """ updatedRecords: String + @deprecated(reason: "All records are considered to be processed; see `processedRecords` instead") """ The number of deleted records contained within the Sync, if known. This excludes filtered records. """ deletedRecords: String + @deprecated(reason: "All records are considered to be processed; see `processedRecords` instead") """ The number of filtered records contained within the Sync, due to issues such as a missing timestamp Dimension, if any are known to be invalid. """ invalidRecords: String + @deprecated(reason: "All records are considered to be processed; see `processedRecords` instead") """ The (compressed) size of the Sync, in bytes, if known. """ @@ -2124,9 +2351,9 @@ type Metric implements Node & Common { """ dimensions: [Dimension!]! """ - The Metric's timestamp. This is the same as its Data Pool's timestamp. + The Metric's timestamp, if any. This is the same as its Data Pool's timestamp, if any. """ - timestamp: Dimension! + timestamp: Dimension """ The Metric's measure. Access this from the Metric's `settings` object instead. """ @@ -2144,17 +2371,19 @@ type Metric implements Node & Common { """ settings: MetricSettings! """ - The Metric data in counter format. A single metric value for the given time range and filters. + Query the Metric in counter format. Returns the Metric's value for the given time range and filters. """ - counter(input: CounterInput!): CounterResponse + counter(input: CounterInput!): CounterResponse @deprecated(reason: "Use the top-level `counter` query instead") """ - The Metric data in time series format. Arrays of timestamps and Metric values for the given time range and filters. + Query the Metric in time series format. Returns arrays of timestamps and the Metric's values for the given time range and filters. """ timeSeries(input: TimeSeriesInput!): TimeSeriesResponse + @deprecated(reason: "Use the top-level `timeSeries` query instead") """ - The Metric data in leaderboard format. A table (array of rows) with the selected dimensions and corresponding Metric values for the given time range and filters. + Query the Metric in leaderboard format. Returns a table (array of rows) with the selected dimensions and the Metric's corresponding values for the given time range and filters. """ leaderboard(input: LeaderboardInput!): LeaderboardResponse + @deprecated(reason: "Use the top-level `leaderboard` query instead") """ List the Policies associated to the Metric. """ @@ -2217,7 +2446,13 @@ type CountMetricSettings { """ Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. """ - filters: [Filter!] + filters: [Filter!] @deprecated(reason: "Use `filterSql` instead") + """ + Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. + + You can provide the filters in the form of SQL. + """ + filterSql: String } """ @@ -2227,7 +2462,13 @@ type SumMetricSettings { """ Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. """ - filters: [Filter!] + filters: [Filter!] @deprecated(reason: "Use `filterSql` instead") + """ + Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. + + You can provide the filters in the form of SQL. + """ + filterSql: String """ The Dimension to be summed. """ @@ -2241,7 +2482,13 @@ type CountDistinctMetricSettings { """ Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. """ - filters: [Filter!] + filters: [Filter!] @deprecated(reason: "Use `filterSql` instead") + """ + Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. + + You can provide the filters in the form of SQL. + """ + filterSql: String """ The Dimension where the count distinct operation is going to be performed. """ @@ -2255,7 +2502,13 @@ type AverageMetricSettings { """ Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. """ - filters: [Filter!] + filters: [Filter!] @deprecated(reason: "Use `filterSql` instead") + """ + Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. + + You can provide the filters in the form of SQL. + """ + filterSql: String """ The Dimension to be averaged. """ @@ -2269,7 +2522,13 @@ type MinMetricSettings { """ Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. """ - filters: [Filter!] + filters: [Filter!] @deprecated(reason: "Use `filterSql` instead") + """ + Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. + + You can provide the filters in the form of SQL. + """ + filterSql: String """ The Dimension to select the minimum from. """ @@ -2283,7 +2542,13 @@ type MaxMetricSettings { """ Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. """ - filters: [Filter!] + filters: [Filter!] @deprecated(reason: "Use `filterSql` instead") + """ + Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. + + You can provide the filters in the form of SQL. + """ + filterSql: String """ The Dimension to select the maximum from. """ @@ -2297,7 +2562,13 @@ type CustomMetricSettings { """ Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. """ - filters: [Filter!] + filters: [Filter!] @deprecated(reason: "Use `filterSql` instead") + """ + Metric Filters allow defining a Metric with a subset of records from the given Data Pool. If no Metric Filters are present, all records will be included. To filter at query time, add Dimensions and use the `filters` property on the `timeSeriesInput`, `counterInput`, or `leaderboardInput` objects. There is no need to add `filters` to be able to filter at query time. + + You can provide the filters in the form of SQL. + """ + filterSql: String """ The expression that defines the aggregation function for this Metric. """ @@ -2798,6 +3069,11 @@ If both relative and absolute time ranges are provided, the relative time range If a `LAST_N` relative time period is selected, an `n` ≥ 1 must be provided. If no `n` is provided or `n` < 1, a `BAD_REQUEST` error will be returned. """ input TimeRangeInput { + """ + The timestamp field to use when querying. Defaults to the timestamp configured on the Data Pool or Metric, if any. + Set this to filter on an alternative timestamp field. + """ + timestamp: String """ The relative time period. """ @@ -3637,7 +3913,9 @@ type DeletionJob implements Node & Job { "The current Deletion Job's status." status: JobStatus! "The list of filters that will be used for deleting data. Data matching the filters will be deleted." - filters: [Filter!]! + filters: [Filter!]! @deprecated(reason: "Use `filterSql` instead") + "The filters that will be used for deleting data, in the form of SQL. Data matching the filters will be deleted." + filterSql: String "The error that occurred while deleting data, if any." error: Error "The current progress of the Deletion Job, from 0.0 to 1.0." @@ -3673,7 +3951,7 @@ input DeletionRequestInput { "The Data Pool that is going to get the data deleted" dataPool: ID! "The list of filters that will be used for deleting data. Data matching these filters will be deleted." - filters: [FilterInput!]! + filters: [FilterInput!] } """ @@ -3683,7 +3961,7 @@ input CreateDeletionJobInput { "The Data Pool that is going to get the data deleted" dataPool: ID! "The list of filters that will be used for deleting data. Data matching these filters will be deleted." - filters: [FilterInput!]! + filters: [FilterInput!] } """ @@ -3778,7 +4056,9 @@ type UpdateDataPoolRecordsJob implements Node & Job { "The current UpdateDataPoolRecords Job's status" status: JobStatus! "The list of filters that will be used for updating data. Data matching the filters will be updated." - filters: [Filter!]! + filters: [Filter!] @deprecated(reason: "Use `filterSql` instead") + "The filters that will be used for updating data, in the form of SQL. Data matching the filters will be updated." + filterSql: String "Describes how the job will update the records." set: [UpdateDataPoolRecordsJobSetColumn!]! "The error that occurred while updating data, if any." @@ -3800,7 +4080,7 @@ input CreateUpdateDataPoolRecordsJobInput { "The Data Pool that is going to get its records updated." dataPool: ID! "The list of filters that will be used for updating records. Records matching these filters will be updated." - filters: [FilterInput!]! + filters: [FilterInput!] "Describes how the job will update the records." set: [UpdateDataPoolRecordsJobSetColumnInput!]! } @@ -3861,7 +4141,11 @@ type DataPoolAccessPolicy implements Node & Common { """ Row-level filters that the Access Policy applies before executing queries. """ - rows: [Filter!]! + rows: [Filter!]! @deprecated(reason: "Use `filtersSql` instead") + """ + Row-level filters that the Access Policy applies before executing queries, in the form of SQL. + """ + filterSql: String """ Applications that are assigned to this Data Pool Access Policy. """ @@ -4186,6 +4470,18 @@ type Mutation { """ modifyHttpDataSource(input: ModifyHttpDataSourceInput!): DataSourceResponse! """ + This mutation creates a new Kafka Data Source. + + The mutation returns the newly created Data Source (or an error message if creating the Data Source fails). + """ + createKafkaDataSource(input: CreateKafkaDataSourceInput!): DataSourceResponse! + """ + This mutation selects a Data Source by its ID or unique name and modifies it to have the given unique name, description, and connection settings. + + If any of the optional arguments are omitted, those properties will be unchanged on the Data Source. + """ + modifyKafkaDataSource(input: ModifyKafkaDataSourceInput!): DataSourceResponse! + """ Creates a new Amazon S3 Data Source pointed at the specified S3 bucket. Returns the newly created Data Source (or an error message if creating the Data Source fails). @@ -4221,6 +4517,16 @@ input SqlV1Input { query: String! } +""" +Input for describing SqlV1 inputs. +""" +input DescribeSqlV1Input { + """ + The SQL query. + """ + query: String! +} + type SqlColumnResponse { """ The name of the returned column. @@ -4256,6 +4562,16 @@ type SqlResponse { info: QueryInfo! } +""" +Response from the describe SQL API. +""" +type DescribeSqlResponse { + """ + The columns that the query would return. + """ + columns: [SqlColumnResponse!]! +} + """ The Data Grid connection. @@ -4393,7 +4709,24 @@ type TopValuesResponse { values: [String!]! } +type ReportType implements Node & Common { + id: ID! + uniqueName: String! + description: String! + application: Application! + layout: [[String]]! + charts: [ChartType]! + createdAt: DateTime! + modifiedAt: DateTime! + createdBy: String! + modifiedBy: String! + account: Account! + environment: Environment! +} + type Query { + report(id: ID!): ReportType @deprecated(reason: "EXPERIMENTAL") + """ This query returns the Application specified by the given ID. @@ -4467,6 +4800,16 @@ type Query { """ dataPools(first: Int, after: String, last: Int, before: String): DataPoolConnection """ + This query returns the Materialized Views within the Environment. + + The `materializedViews` query uses [cursor-based pagination](/docs/api/pagination) typical of GraphQL APIs. You can use the pairs of parameters `first` and `after` or `last` and `before` to page forward or backward through the results, respectively. + + For forward pagination, the `first` parameter defines the number of results to return, and the `after` parameter defines the cursor to continue from. You should pass the cursor for the _last_ result of the current page to `after`. + + For backward pagination, the `last` parameter defines the number of results to return, and the `before` parameter defines the cursor to continue from. You should pass the cursor for the _first_ result of the current page to `before`. + """ + materializedViews(first: Int, after: String, last: Int, before: String): MaterializedViewConnection + """ This query returns the Data Pool Access Policy specified by the given ID. A Data Pool Access Policy limits the data that Applications can access within a Data Pool. @@ -4530,6 +4873,10 @@ type Query { """ sqlV1(input: SqlV1Input!): SqlResponse! """ + Describe SQL statements Data Pools. + """ + describeSqlV1(input: DescribeSqlV1Input!): DescribeSqlResponse! + """ This query returns the individual records of a Data Pool with the convenience of built-in pagination, filtering, and sorting. """ dataGrid(input: DataGridInput!): DataGridConnection! @@ -4542,15 +4889,19 @@ type Query { """ topValues(input: TopValuesInput!): TopValuesResponse! """ - Query a Metric in counter format. A single Metric value for the given time range and filters. + Query a metric in counter format. Returns a single metric value for the given time range and filters. """ counter(input: CounterInput!): CounterResponse """ - Query a Metric in time series format. Arrays of timestamps and Metric values for the given time range and filters. + Query metrics in counter format. Returns a metric value for each input in the array of inputs. + """ + counters(input: [CounterInput!]!): [CounterResponse]! + """ + Query a metric in time series format. Returns arrays of timestamps and metric values for the given time range and filters. """ timeSeries(input: TimeSeriesInput!): TimeSeriesResponse """ - Query a Metric in leaderboard format. A table (array of rows) with the selected dimensions and corresponding Metric values for the given time range and filters. + Query a metric in leaderboard format. Returns a table (array of rows) with the selected dimensions and the metric's corresponding values for the given time range and filters. """ leaderboard(input: LeaderboardInput!): LeaderboardResponse """ @@ -4594,6 +4945,14 @@ type Query { last: Int before: String ): UpdateDataPoolRecordsJobConnection! + """ + This query returns the Materialized View specified by the given ID. + """ + materializedView(id: ID!): MaterializedView + """ + This query returns the Materialized View specified by its unique name. + """ + materializedViewByName(uniqueName: String!): MaterializedView } """ @@ -4924,6 +5283,42 @@ type DeletionJobEdge { node: DeletionJob! } +""" +The Materialized View connection object. + +Learn more about [pagination in GraphQL](https://www.propeldata.com/docs/api/pagination). +""" +type MaterializedViewConnection { + """ + The Materialized View connection's edges. + """ + edges: [MaterializedViewEdge!]! + """ + The Materialized View connection's nodes. + """ + nodes: [MaterializedView!]! + """ + The Materialized View connection's page info. + """ + pageInfo: PageInfo! +} + +""" +The Materialized View edge object. + +Learn more about [pagination in GraphQL](https://www.propeldata.com/docs/api/pagination). +""" +type MaterializedViewEdge { + """ + The edge's cursor. + """ + cursor: String! + """ + The edge's node. + """ + node: MaterializedView! +} + """ The Metric connection object. @@ -5230,6 +5625,118 @@ input PartialHttpConnectionSettingsInput { tables: [HttpDataSourceTableInput!] } +input CreateKafkaDataSourceInput { + """ + The Kafka Data Source's connection settings + """ + connectionSettings: KafkaConnectionSettingsInput! + """ + The Kafka Data Source's description. + """ + description: String + """ + The Kafka Data Source's unique name. If not specified, Propel will set the ID as unique name. + """ + uniqueName: String +} + +""" +The Kafka Data Source connection settings. +""" +type KafkaConnectionSettings { + """ + The type of authentication to use. Can be SCRAM-SHA-256, SCRAM-SHA-512, PLAIN or NONE + """ + auth: String! + """ + The bootstrap server(s) to connect to + """ + bootstrapServers: [String!]! + """ + The password for the provided user + """ + password: String! + """ + Whether the the connection to the Kafka servers is encrypted or not + """ + tls: Boolean + """ + The user for authenticating against the Kafka servers + """ + user: String! +} + +""" +The Kafka Data Source connection settings. +""" +input KafkaConnectionSettingsInput { + """ + The type of authentication to use. Can be SCRAM-SHA-256, SCRAM-SHA-512, PLAIN or NONE + """ + auth: String! + """ + The bootstrap server(s) to connect to + """ + bootstrapServers: [String!]! + """ + The password for the provided user + """ + password: String! + """ + Whether the the connection to the Kafka servers is encrypted or not + """ + tls: Boolean + """ + The user for authenticating against the Kafka servers + """ + user: String! +} + +input ModifyKafkaDataSourceInput { + """ + The Kafka Data Source's new connection settings. If not provided this property will not be modified. + """ + connectionSettings: PartialKafkaConnectionSettingsInput + """ + The Kafka Data Source's new description. If not provided this property will not be modified. + """ + description: String + """ + The ID or unique name of the Kafka Data Source to modify. + """ + idOrUniqueName: idOrUniqueName! + """ + The Kafka Data Source's new unique name. If not provided this property will not be modified. + """ + uniqueName: String +} + +""" +The Kafka Data Source connection settings. +""" +input PartialKafkaConnectionSettingsInput { + """ + The type of authentication to use. Can be SCRAM-SHA-256, SCRAM-SHA-512, PLAIN or NONE If not provided this property will not be modified. + """ + auth: String + """ + The bootstrap server(s) to connect to If not provided this property will not be modified. + """ + bootstrapServers: [String!]! + """ + The password for the provided user If not provided this property will not be modified. + """ + password: String + """ + Whether the the connection to the Kafka servers is encrypted or not If not provided this property will not be modified. + """ + tls: Boolean + """ + The user for authenticating against the Kafka servers If not provided this property will not be modified. + """ + user: String +} + input CreateS3DataSourceInput { """ The S3 Data Source's connection settings @@ -5387,15 +5894,21 @@ type WebhookConnectionSettings { """ columns: [WebhookDataSourceColumn!] """ - The tenant ID column, if configured. + Override the Data Pool's table settings. These describe how the Data Pool's table is created in ClickHouse, and a + default will be chosen based on the Data Pool's `timestamp` and `uniqueId` values, if any. You can override these + defaults in order to specify a custom table engine, custom ORDER BY, etc. + """ + tableSettings: TableSettings + """ + The tenant ID column, if any. """ tenant: String """ - The primary timestamp column. + The primary timestamp column, if any. """ - timestamp: String! + timestamp: String """ - The unique ID column. Propel uses the primary timestamp and a unique ID to compose a primary key for determining whether records should be inserted, deleted, or updated. + The unique ID column, if any. Propel uses the primary timestamp and a unique ID to compose a primary key for determining whether records should be inserted, deleted, or updated. """ uniqueId: String """ @@ -5404,6 +5917,14 @@ type WebhookConnectionSettings { webhookUrl: String! } +type ChartType { + id: String! + type: String! + title: String! + query: String! + variables: String! +} + """ The Webhook Data Source connection settings. """ @@ -5417,15 +5938,21 @@ input WebhookConnectionSettingsInput { """ columns: [WebhookDataSourceColumnInput!] """ - The tenant ID column, if configured. + Override the Data Pool's table settings. These describe how the Data Pool's table is created in ClickHouse, and a + default will be chosen based on the Data Pool's `timestamp` and `uniqueId` values, if any. You can override these + defaults in order to specify a custom table engine, custom ORDER BY, etc. + """ + tableSettings: TableSettingsInput + """ + The tenant ID column, if any. """ tenant: String """ - The primary timestamp column. + The primary timestamp column, if any. """ - timestamp: String! + timestamp: String """ - The unique ID column. Propel uses the primary timestamp and a unique ID to compose a primary key for determining whether records should be inserted, deleted, or updated. + The unique ID column, if any. Propel uses the primary timestamp and a unique ID to compose a primary key for determining whether records should be inserted, deleted, or updated. """ uniqueId: String } diff --git a/packages/ui-kit/src/helpers/graphql/queries/Report.graphql b/packages/ui-kit/src/helpers/graphql/queries/Report.graphql new file mode 100644 index 00000000..74782780 --- /dev/null +++ b/packages/ui-kit/src/helpers/graphql/queries/Report.graphql @@ -0,0 +1,13 @@ +query Report($id: ID!) { + report(id: $id) { + uniqueName + layout + charts { + id + type + title + query + variables + } + } +} diff --git a/packages/ui-kit/src/hooks/index.ts b/packages/ui-kit/src/hooks/index.ts index de80fb34..05d19d95 100644 --- a/packages/ui-kit/src/hooks/index.ts +++ b/packages/ui-kit/src/hooks/index.ts @@ -6,3 +6,4 @@ export * from './useRecordsById' export * from './useSql' export * from './useTimeSeries' export * from './useTopValues' +export * from './useReportComponents' diff --git a/packages/ui-kit/src/hooks/useReport/index.ts b/packages/ui-kit/src/hooks/useReport/index.ts new file mode 100644 index 00000000..4096c83f --- /dev/null +++ b/packages/ui-kit/src/hooks/useReport/index.ts @@ -0,0 +1 @@ +export * from './useReport' diff --git a/packages/ui-kit/src/hooks/useReport/useReport.ts b/packages/ui-kit/src/hooks/useReport/useReport.ts new file mode 100644 index 00000000..e4d332d2 --- /dev/null +++ b/packages/ui-kit/src/hooks/useReport/useReport.ts @@ -0,0 +1,31 @@ +import { useAccessToken } from '../../components' +import { PROPEL_GRAPHQL_API_ENDPOINT, ReportQuery, useReportQuery } from '../../helpers' +import { ReportQueryProp } from '../../components/Report/Report.types' + +export function useReport(props: Partial) { + const { accessToken: accessTokenFromProp, propelApiUrl, reportId, enabled = true } = props + + const { accessToken: accessTokenFromProvider, isLoading: isLoadingAccessToken } = useAccessToken() + + const accessToken = accessTokenFromProp ?? accessTokenFromProvider + + const { data, isLoading } = useReportQuery( + { + endpoint: propelApiUrl ?? PROPEL_GRAPHQL_API_ENDPOINT, + fetchParams: { + headers: { + 'content-type': 'application/graphql-response+json', + authorization: `Bearer ${accessToken}` + } + } + }, + { + id: reportId ?? '' + }, + { + enabled: accessToken != null && enabled + } + ) + + return { data, isLoading: isLoading || isLoadingAccessToken } +} diff --git a/packages/ui-kit/src/hooks/useReportComponents/index.ts b/packages/ui-kit/src/hooks/useReportComponents/index.ts new file mode 100644 index 00000000..f9f2d43a --- /dev/null +++ b/packages/ui-kit/src/hooks/useReportComponents/index.ts @@ -0,0 +1 @@ +export * from './useReportComponents' diff --git a/packages/ui-kit/src/hooks/useReportComponents/useReportComponents.ts b/packages/ui-kit/src/hooks/useReportComponents/useReportComponents.ts new file mode 100644 index 00000000..3b3a36d9 --- /dev/null +++ b/packages/ui-kit/src/hooks/useReportComponents/useReportComponents.ts @@ -0,0 +1,74 @@ +import { PROPEL_GRAPHQL_API_ENDPOINT } from '../../helpers' +import { ChartProp } from '../../components/Report' +import { useAccessToken } from '../../components' +import { useEffect, useMemo, useState } from 'react' + +function fetcher(endpoint: string, requestInit: RequestInit, query: string, variables?: string) { + return async (): Promise => { + const res = await fetch(endpoint, { + method: 'POST', + ...requestInit, + body: JSON.stringify({ query, variables }) + }) + + const json = await res.json() + + if (json.errors) { + const { message } = json.errors[0] + + throw new Error(message) + } + + return json.data + } +} + +interface Options { + propelApiUrl?: string + accessToken?: string +} + +export function useReportComponents(charts: Array | null | undefined, options?: Options) { + const [result, setResult] = useState(null) + const [isLoading, setIsLoading] = useState(true) + + const { accessToken: accessTokenFromProvider } = useAccessToken() + + const accessToken = options?.accessToken ?? accessTokenFromProvider + + const fetchers = useMemo( + () => + charts?.map((chart) => + fetcher( + options?.propelApiUrl ?? PROPEL_GRAPHQL_API_ENDPOINT, + { + headers: { + 'content-type': 'application/graphql-response+json', + authorization: `Bearer ${accessToken}` + } + }, + chart?.query ?? '', + JSON.parse(chart?.variables ?? '{}') + ) + ), + [accessToken, options?.propelApiUrl, charts] + ) + + useEffect(() => { + async function getData() { + if (fetchers != null) { + const result = await Promise.allSettled(fetchers.map((fetcher) => fetcher())) + setResult(result.map((res) => (res.status === 'fulfilled' ? res.value : null))) + setIsLoading(false) + } + } + + if (accessToken != null) { + getData() + } + }, [accessToken, fetchers]) + + console.log(result) + + return { charts: charts?.map((chart, idx) => ({ ...chart, result: result != null ? result[idx] : null })), isLoading } +}