Skip to content

Commit c0e0780

Browse files
committed
Show a message that results are clipped
1 parent 4461b78 commit c0e0780

File tree

1 file changed

+52
-22
lines changed
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query

1 file changed

+52
-22
lines changed

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ArrowDownTrayIcon, ArrowsPointingOutIcon, ArrowTrendingUpIcon, ClipboardIcon, TableCellsIcon } from "@heroicons/react/20/solid";
22
import type { OutputColumnMetadata } from "@internal/clickhouse";
3-
import { WhereClauseCondition } from "@internal/tsql";
3+
import { type WhereClauseCondition } from "@internal/tsql";
44
import { useFetcher } from "@remix-run/react";
55
import {
66
redirect,
@@ -11,6 +11,7 @@ import parse from "parse-duration";
1111
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
1212
import { flushSync } from "react-dom";
1313
import { typedjson, useTypedFetcher, useTypedLoaderData } from "remix-typedjson";
14+
import simplur from "simplur";
1415
import { z } from "zod";
1516
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
1617
import { AlphaTitle } from "~/components/AlphaBadge";
@@ -24,7 +25,7 @@ import { autoFormatSQL, TSQLEditor } from "~/components/code/TSQLEditor";
2425
import { TSQLResultsTable } from "~/components/code/TSQLResultsTable";
2526
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
2627
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
27-
import { Button } from "~/components/primitives/Buttons";
28+
import { Button, LinkButton } from "~/components/primitives/Buttons";
2829
import { Callout } from "~/components/primitives/Callout";
2930
import { Card } from "~/components/primitives/charts/Card";
3031
import {
@@ -62,18 +63,18 @@ import { findProjectBySlug } from "~/models/project.server";
6263
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
6364
import { QueryPresenter, type QueryHistoryItem } from "~/presenters/v3/QueryPresenter.server";
6465
import type { action as titleAction } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-title";
66+
import { getLimit } from "~/services/platform.v3.server";
6567
import { executeQuery, type QueryScope } from "~/services/queryService.server";
6668
import { requireUser } from "~/services/session.server";
6769
import { downloadFile, rowsToCSV, rowsToJSON } from "~/utils/dataExport";
68-
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
70+
import { EnvironmentParamSchema, organizationBillingPath } from "~/utils/pathBuilder";
6971
import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags.server";
7072
import { querySchemas } from "~/v3/querySchemas";
7173
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
7274
import { QueryHelpSidebar } from "./QueryHelpSidebar";
7375
import { QueryHistoryPopover } from "./QueryHistoryPopover";
7476
import type { AITimeFilter } from "./types";
7577
import { formatQueryStats } from "./utils";
76-
import { getLimit } from "~/services/platform.v3.server";
7778

7879
/** Convert a Date or ISO string to ISO string format */
7980
function toISOString(value: Date | string): string {
@@ -199,6 +200,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
199200
hiddenColumns: null,
200201
explainOutput: null,
201202
generatedSql: null,
203+
periodClipped: null,
202204
},
203205
{ status: 403 }
204206
);
@@ -215,6 +217,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
215217
hiddenColumns: null,
216218
explainOutput: null,
217219
generatedSql: null,
220+
periodClipped: null,
218221
},
219222
{ status: 404 }
220223
);
@@ -231,6 +234,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
231234
hiddenColumns: null,
232235
explainOutput: null,
233236
generatedSql: null,
237+
periodClipped: null,
234238
},
235239
{ status: 404 }
236240
);
@@ -256,6 +260,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
256260
hiddenColumns: null,
257261
explainOutput: null,
258262
generatedSql: null,
263+
periodClipped: null,
259264
},
260265
{ status: 400 }
261266
);
@@ -274,30 +279,41 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
274279
defaultPeriod: DEFAULT_PERIOD,
275280
});
276281

282+
// Calculate the effective "from" date the user is requesting (for period clipping check)
283+
// This is null only when the user specifies just a "to" date (rare case)
284+
let requestedFromDate: Date | null = null;
285+
if (timeFilter.from) {
286+
requestedFromDate = new Date(timeFilter.from);
287+
} else if (!timeFilter.to) {
288+
// Period specified (or default) - calculate from now
289+
const periodMs = parse(timeFilter.period ?? DEFAULT_PERIOD) ?? 7 * 24 * 60 * 60 * 1000;
290+
requestedFromDate = new Date(Date.now() - periodMs);
291+
}
292+
293+
// Build the fallback WHERE condition based on what the user specified
277294
let triggeredAtFallback: WhereClauseCondition;
278295
if (timeFilter.from && timeFilter.to) {
279-
// Both from and to specified - use BETWEEN
280296
triggeredAtFallback = { op: "between", low: timeFilter.from, high: timeFilter.to };
281297
} else if (timeFilter.from) {
282-
// Only from specified
283298
triggeredAtFallback = { op: "gte", value: timeFilter.from };
284299
} else if (timeFilter.to) {
285-
// Only to specified
286300
triggeredAtFallback = { op: "lte", value: timeFilter.to };
287301
} else {
288-
// Period specified (or default) - calculate from now
289-
const periodMs = parse(timeFilter.period ?? DEFAULT_PERIOD) ?? 7 * 24 * 60 * 60 * 1000;
290-
triggeredAtFallback = { op: "gte", value: new Date(Date.now() - periodMs) };
302+
triggeredAtFallback = { op: "gte", value: requestedFromDate! };
291303
}
292304

293-
const maxQueryPeriod = await getLimit(project.organizationId, "queryPeriodDays", 5);
305+
const maxQueryPeriod = await getLimit(project.organizationId, "queryPeriodDays", 30);
306+
const maxQueryPeriodDate = new Date(Date.now() - maxQueryPeriod * 24 * 60 * 60 * 1000);
307+
308+
// Check if the requested time period exceeds the plan limit
309+
const periodClipped = requestedFromDate !== null && requestedFromDate < maxQueryPeriodDate;
294310

295311
// Force tenant isolation and time period limits
296312
const enforcedWhereClause = {
297313
organization_id: { op: "eq", value: project.organizationId },
298314
project_id: scope === "project" || scope === "environment" ? { op: "eq", value: project.id } : undefined,
299315
environment_id: scope === "environment" ? { op: "eq", value: environment.id } : undefined,
300-
triggered_at: { op: "gte", value: new Date(Date.now() - maxQueryPeriod * 24 * 60 * 60 * 1000) }
316+
triggered_at: { op: "gte", value: maxQueryPeriodDate },
301317
} satisfies Record<string, WhereClauseCondition | undefined>;
302318

303319
try {
@@ -341,6 +357,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
341357
explainOutput: null,
342358
generatedSql: null,
343359
queryId: null,
360+
periodClipped: null,
344361
},
345362
{ status: 400 }
346363
);
@@ -355,6 +372,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
355372
explainOutput: result.explainOutput ?? null,
356373
generatedSql: result.generatedSql ?? null,
357374
queryId,
375+
periodClipped: periodClipped ? maxQueryPeriod : null,
358376
});
359377
} catch (err) {
360378
const errorMessage = err instanceof Error ? err.message : "Unknown error executing query";
@@ -368,6 +386,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
368386
explainOutput: null,
369387
generatedSql: null,
370388
queryId: null,
389+
periodClipped: null,
371390
},
372391
{ status: 500 }
373392
);
@@ -781,16 +800,27 @@ export default function Page() {
781800
</div>
782801
) : results?.rows && results?.columns ? (
783802
<div className="flex h-full flex-col overflow-hidden">
784-
{results.hiddenColumns && results.hiddenColumns.length > 0 && (
785-
<div className="p-2">
786-
<Callout variant="warning" className="shrink-0 text-sm">
787-
<code>SELECT *</code> doesn't return all columns because it's slow. The
788-
following columns are not shown:{" "}
789-
<span className="font-mono text-xs">
790-
{results.hiddenColumns.join(", ")}
791-
</span>
792-
. Specify them explicitly to include them.
793-
</Callout>
803+
{(results.hiddenColumns?.length || results.periodClipped) && (
804+
<div className="flex flex-col gap-2 p-2">
805+
{results.hiddenColumns && results.hiddenColumns.length > 0 && (
806+
<Callout variant="warning" className="shrink-0 text-sm">
807+
<code>SELECT *</code> doesn't return all columns because it's slow. The
808+
following columns are not shown:{" "}
809+
<span className="font-mono text-xs">
810+
{results.hiddenColumns.join(", ")}
811+
</span>
812+
. Specify them explicitly to include them.
813+
</Callout>
814+
)}
815+
{results.periodClipped && (
816+
<Callout
817+
variant="pricing"
818+
cta={<LinkButton variant="primary/small" to={organizationBillingPath({ slug: organization.slug })}>Upgrade</LinkButton>}
819+
className="items-center"
820+
>
821+
{simplur`Results are limited to the last ${results.periodClipped} day[|s] based on your plan.`}
822+
</Callout>
823+
)}
794824
</div>
795825
)}
796826
<div className="h-full bg-charcoal-900 p-2">

0 commit comments

Comments
 (0)