From 0222dadc97ac43904332e5e4bad4c8b967fb0bbe Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:24:24 +0200 Subject: [PATCH 1/8] Migrate Query OSS Insight to dashboard, deprecate unused Toolpad pages Migrates the queryOssInsight tool from the Toolpad app to code-infra-dashboard as a new /query-oss-insight page backed by a server-side /api/oss-insight proxy (OSS Insight does not send CORS headers for our origin). Deprecates auditClosedIssues, auditLabelChanges, communityCore and communityPerMonth: their Toolpad pages are stripped to a notice (keeping their aliases so old URLs don't 404) and the now-orphaned backend functions and resource files are removed. --- .../(dashboard)/query-oss-insight/page.tsx | 9 + .../app/api/oss-insight/route.ts | 54 ++++ .../src/views/Landing.tsx | 7 + .../src/views/QueryOssInsight.tsx | 150 +++++++++ .../toolpad/pages/auditClosedIssues/page.yml | 63 +--- .../toolpad/pages/auditLabelChanges/page.yml | 64 +--- .../toolpad/pages/communityCore/page.yml | 288 +---------------- .../toolpad/pages/communityPerMonth/page.yml | 163 +--------- .../toolpad/pages/queryOssInsight/page.yml | 72 +---- .../toolpad/resources/functions.ts | 293 ------------------ .../resources/queryAuditClosedIssues.ts | 101 ------ .../resources/queryAuditLabelChanges.ts | 133 -------- .../toolpad/resources/queryOssInsight.ts | 18 -- 13 files changed, 247 insertions(+), 1168 deletions(-) create mode 100644 apps/code-infra-dashboard/app/(dashboard)/query-oss-insight/page.tsx create mode 100644 apps/code-infra-dashboard/app/api/oss-insight/route.ts create mode 100644 apps/code-infra-dashboard/src/views/QueryOssInsight.tsx delete mode 100644 apps/tools-public/toolpad/resources/queryAuditClosedIssues.ts delete mode 100644 apps/tools-public/toolpad/resources/queryAuditLabelChanges.ts delete mode 100644 apps/tools-public/toolpad/resources/queryOssInsight.ts diff --git a/apps/code-infra-dashboard/app/(dashboard)/query-oss-insight/page.tsx b/apps/code-infra-dashboard/app/(dashboard)/query-oss-insight/page.tsx new file mode 100644 index 000000000..1cdc86832 --- /dev/null +++ b/apps/code-infra-dashboard/app/(dashboard)/query-oss-insight/page.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import type { Metadata } from 'next'; +import QueryOssInsight from '@/views/QueryOssInsight'; + +export const metadata: Metadata = { title: 'Query OSS Insight' }; + +export default function QueryOssInsightPage() { + return ; +} diff --git a/apps/code-infra-dashboard/app/api/oss-insight/route.ts b/apps/code-infra-dashboard/app/api/oss-insight/route.ts new file mode 100644 index 000000000..03b59cf56 --- /dev/null +++ b/apps/code-infra-dashboard/app/api/oss-insight/route.ts @@ -0,0 +1,54 @@ +import { type NextRequest, NextResponse } from 'next/server'; + +const OSS_INSIGHT_ORIGIN = 'https://api.ossinsight.io'; + +export async function GET(request: NextRequest) { + const slug = request.nextUrl.searchParams.get('slug'); + + if (!slug) { + return NextResponse.json({ error: 'Missing required parameter: slug' }, { status: 400 }); + } + + const response = await fetch(`${OSS_INSIGHT_ORIGIN}/gh/repo/${slug}`, { + next: { revalidate: 3600 }, + }); + + if (!response.ok) { + return NextResponse.json( + { error: `OSS Insight returned ${response.status} for ${slug}` }, + { status: response.status }, + ); + } + + const json = await response.json(); + return NextResponse.json({ id: json.data.id }); +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + const { repositoryId, sql } = body; + + if (!repositoryId || !sql) { + return NextResponse.json( + { error: 'Missing required parameters: repositoryId and sql' }, + { status: 400 }, + ); + } + + const response = await fetch(`${OSS_INSIGHT_ORIGIN}/q/playground`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ type: 'repo', sql, id: repositoryId }), + }); + + if (!response.ok) { + const detail = (await response.text()).slice(0, 500); + return NextResponse.json( + { error: `OSS Insight returned ${response.status}: ${detail}` }, + { status: response.status }, + ); + } + + const json = await response.json(); + return NextResponse.json({ rows: json.data }); +} diff --git a/apps/code-infra-dashboard/src/views/Landing.tsx b/apps/code-infra-dashboard/src/views/Landing.tsx index 93d69b7ec..5a929cdde 100644 --- a/apps/code-infra-dashboard/src/views/Landing.tsx +++ b/apps/code-infra-dashboard/src/views/Landing.tsx @@ -19,6 +19,7 @@ import DownloadIcon from '@mui/icons-material/Download'; import FindInPageIcon from '@mui/icons-material/FindInPage'; import SpeedIcon from '@mui/icons-material/Speed'; import PeopleIcon from '@mui/icons-material/People'; +import StorageIcon from '@mui/icons-material/Storage'; import Link from '@mui/material/Link'; import CardActionArea from '@mui/material/CardActionArea'; import Heading from '../components/Heading'; @@ -87,6 +88,12 @@ const tools: Tool[] = [ icon: , path: '/mui-about', }, + { + name: 'Query OSS Insight', + description: 'Run arbitrary SQL against the OSS Insight playground for a GitHub repository.', + icon: , + path: '/query-oss-insight', + }, ]; export default function Landing() { diff --git a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx new file mode 100644 index 000000000..d9732fbb5 --- /dev/null +++ b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx @@ -0,0 +1,150 @@ +'use client'; + +import * as React from 'react'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import NoSsr from '@mui/material/NoSsr'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { DataGridPremium, type GridColDef } from '@mui/x-data-grid-premium'; +import Heading from '../components/Heading'; +import ErrorDisplay from '../components/ErrorDisplay'; +import { useSearchParamsState } from '../hooks/useSearchParamsState'; +import { fetchJson } from '../utils/http'; + +interface RepoDetails { + id: number; +} + +type QueryRow = Record; + +// Injected on each grid row to give DataGrid a stable id; excluded from the +// generated columns since those derive from the raw query result keys. +const ROW_ID = '__rowIndex'; + +interface QueryResult { + rows: QueryRow[]; +} + +async function runQuery(repositoryId: number, sql: string): Promise { + const response = await fetch('/api/oss-insight', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ repositoryId, sql }), + }); + if (!response.ok) { + const body = await response.json().catch(() => null); + throw new Error(body?.error ?? `HTTP ${response.status}: ${response.statusText}`); + } + return response.json(); +} + +export default function QueryOssInsight() { + const [searchParams, setSearchParams] = useSearchParamsState( + { slug: { defaultValue: 'mui/material-ui' } }, + { replace: true }, + ); + const { slug } = searchParams; + + const [sql, setSql] = React.useState(''); + + const repoQuery = useQuery({ + queryKey: ['oss-insight-repo', slug], + queryFn: () => fetchJson(`/api/oss-insight?${new URLSearchParams({ slug })}`), + enabled: Boolean(slug), + staleTime: 5 * 60 * 1000, + retry: false, + }); + + const repositoryId = repoQuery.data?.id ?? null; + + const mutation = useMutation({ + mutationFn: () => runQuery(repositoryId!, sql), + }); + + const rows = React.useMemo(() => mutation.data?.rows ?? [], [mutation.data]); + + const gridRows = React.useMemo( + () => rows.map((row, index) => ({ ...row, [ROW_ID]: index })), + [rows], + ); + + const columns = React.useMemo( + () => + rows.length > 0 + ? Object.keys(rows[0]).map((field) => ({ field, flex: 1, minWidth: 120 })) + : [], + [rows], + ); + + return ( + + Query OSS Insight + + Run arbitrary SQL against the{' '} + + OSS Insight + {' '} + playground for a GitHub repository. + + + setSearchParams({ slug: event.target.value })} + sx={{ minWidth: 280 }} + /> + + {repositoryId !== null ? `Repository ID: ${repositoryId}` : 'Not found'} + + + setSql(event.target.value)} + multiline + minRows={4} + fullWidth + slotProps={{ htmlInput: { style: { fontFamily: 'monospace' } } }} + sx={{ mb: 2 }} + /> + + + + {mutation.isError ? ( + + ) : null} + + {/* Remove once https://github.com/mui/mui-x/issues/17077 is fixed */} + + row[ROW_ID] as number} + loading={mutation.isPending} + density="compact" + disableRowSelectionOnClick + sx={{ height: '100%' }} + /> + + + + ); +} diff --git a/apps/tools-public/toolpad/pages/auditClosedIssues/page.yml b/apps/tools-public/toolpad/pages/auditClosedIssues/page.yml index 53d97cb6f..ac8052f14 100644 --- a/apps/tools-public/toolpad/pages/auditClosedIssues/page.yml +++ b/apps/tools-public/toolpad/pages/auditClosedIssues/page.yml @@ -3,69 +3,16 @@ apiVersion: v1 kind: page spec: - displayName: Audit closed issues + title: Audit closed Issue alias: - xj43hyd - title: Audit closed Issue + displayName: Audit closed issues content: - - component: PageRow - name: pageRow1 - children: - - component: Text - name: text - layout: - columnSize: 0.6565656565656566 - verticalAlign: center - props: - value: 'Filter for GitHub slug:' - - component: TextField - name: gitHubSlug - layout: - columnSize: 1.3434343434343434 - props: - label: GitHub slug - defaultValue: linear - fullWidth: true - component: Text - name: text1 + name: text layout: columnSize: 1 props: mode: markdown - value: Find issues closed by a slug. - - component: DataGrid - name: dataGrid - layout: - columnSize: 1 - height: 548 - props: - rows: - $$jsExpression: >- - queryAuditClosedIssues.rows - .filter((issue) => { - return issue.timelineItems.some((event) => event.actor === gitHubSlug.value) - }) - .map((issue) => ({ - ...issue, - closedAt: issue.timelineItems[0].createdAt, - })) - columns: - - field: title - type: string - width: 279 - headerName: Title - - field: url - type: link - width: 362 - headerName: URL - - field: closedAt - type: string - width: 200 - headerName: Closed at - height: 480 - queries: - - name: queryAuditClosedIssues - mode: query - query: - function: queryAuditClosedIssues.ts#queryAuditClosedIssues - kind: local + value: This page is deprecated and no longer maintained. + display: shell diff --git a/apps/tools-public/toolpad/pages/auditLabelChanges/page.yml b/apps/tools-public/toolpad/pages/auditLabelChanges/page.yml index 445f83f46..617ace944 100644 --- a/apps/tools-public/toolpad/pages/auditLabelChanges/page.yml +++ b/apps/tools-public/toolpad/pages/auditLabelChanges/page.yml @@ -3,70 +3,16 @@ apiVersion: v1 kind: page spec: - displayName: Audit label activity + title: Audit label activity alias: - xj43hyd - title: Audit label activity + displayName: Audit label activity content: - - component: PageRow - name: pageRow1 - children: - - component: Text - name: text - layout: - columnSize: 0.6565656565656566 - verticalAlign: center - props: - value: 'Filter for GitHub slub:' - - component: TextField - name: gitHubSlug - layout: - columnSize: 1.3434343434343434 - props: - label: GitHub slug - defaultValue: zannager - fullWidth: true - component: Text - name: text1 + name: text layout: columnSize: 1 props: mode: markdown - value: "Build for: - https://www.notion.so/mui-org/GitHub-community-issues-PRs-Tier-1-12a8\ - 4fdf50e44595afc55343dac00fca#0711365e6f2343bfbbb0c9c78bb2bc8d." - - component: DataGrid - name: dataGrid - layout: - columnSize: 1 - height: 548 - props: - rows: - $$jsExpression: > - queryAuditLabelChanges.rows - .filter((issue) => { - return issue.timelineItems.some((event) => event.actor === gitHubSlug.value) - }) - .map((issue) => ({ - ...issue, - timelineItems: issue.timelineItems.map((event) => event.label), - })) - columns: - - field: title - type: string - width: 321 - headerName: Title - - field: url - type: link - width: 341 - headerName: URL - - field: timelineItems - type: json - width: 515 - headerName: Labels - height: 480 - queries: - - name: queryAuditLabelChanges - query: - function: queryAuditLabelChanges.ts#queryAuditLabelChanges - kind: local + value: This page is deprecated and no longer maintained. + display: shell diff --git a/apps/tools-public/toolpad/pages/communityCore/page.yml b/apps/tools-public/toolpad/pages/communityCore/page.yml index 91ffc308e..f4626b331 100644 --- a/apps/tools-public/toolpad/pages/communityCore/page.yml +++ b/apps/tools-public/toolpad/pages/communityCore/page.yml @@ -4,291 +4,15 @@ apiVersion: v1 kind: page spec: title: Community Core + alias: + - 9r8fshsf + displayName: Community Core content: - - component: DataGrid - name: DataGrid - layout: - columnSize: 1 - props: - rows: - $$jsExpression: > - PRsOpenandReviewedQuery.data.map((item) => ({ - ...item, // use the spread operator to copy existing properties - ratio: Math.round((item.reviewed * 100) / item.opened) / 100, // add a new property to each object - })) - columns: - - field: event_month - type: string - width: 105 - - field: reviewed_by - type: string - width: 165 - - field: reviewed - type: number - width: 138 - - field: opened - type: number - width: 151 - - field: ratio - type: number - width: 141 - component: Text name: text layout: columnSize: 1 props: - value: Community PRs reviews - - component: Chart - name: reviews - layout: - columnSize: 1 - props: - data: - - kind: line - label: michaldudak - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "michaldudak") - xKey: event_month - yKey: reviewed - color: '#1976d2' - - kind: line - label: mnajdova - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "mnajdova") - xKey: event_month - yKey: reviewed - color: '#9c27b0' - - label: siriwatknp - kind: line - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "siriwatknp") - color: '#e91e63' - xKey: event_month - yKey: reviewed - - label: mj12albert - kind: line - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "mj12albert") - color: '#009688' - xKey: event_month - yKey: reviewed - - label: DiegoAndai - kind: line - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "DiegoAndai") - color: '#ff5722' - xKey: event_month - yKey: reviewed - - label: brijeshb42 - kind: line - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "brijeshb42") - color: '#ff9800' - xKey: event_month - yKey: reviewed - - component: Text - name: text1 - layout: - columnSize: 1 - props: - value: PRs created - - component: Chart - name: reviews1 - layout: - columnSize: 1 - props: - data: - - kind: line - label: michaldudak - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "michaldudak") - xKey: event_month - yKey: opened - color: '#1976d2' - - kind: line - label: mnajdova - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "mnajdova") - xKey: event_month - yKey: opened - color: '#9c27b0' - - label: siriwatknp - kind: line - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "siriwatknp") - color: '#e91e63' - xKey: event_month - yKey: opened - - label: mj12albert - kind: line - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "mj12albert") - color: '#009688' - xKey: event_month - yKey: opened - - label: DiegoAndai - kind: line - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "DiegoAndai") - color: '#ff5722' - xKey: event_month - yKey: opened - - label: brijeshb42 - kind: line - data: - $$jsExpression: | - [...PRsOpenandReviewedQuery.data] - .reverse() - .filter((entry) => entry.reviewed_by === "brijeshb42") - color: '#ff9800' - xKey: event_month - yKey: opened - - component: Text - name: text2 - layout: - columnSize: 1 - props: - value: Community support ratio - - component: Chart - name: reviews2 - layout: - columnSize: 1 - props: - data: - - kind: line - label: michaldudak - data: - $$jsExpression: > - [...PRsOpenandReviewedQuery.data] - .map((entry) => ({ ...entry, ratio: entry.reviewed / entry.opened })) - .reverse() - .filter((entry) => entry.reviewed_by === "michaldudak") - xKey: event_month - yKey: ratio - color: '#1976d2' - - kind: line - label: mnajdova - data: - $$jsExpression: > - [...PRsOpenandReviewedQuery.data] - .map((entry) => ({ ...entry, ratio: entry.reviewed / entry.opened })) - .reverse() - .filter((entry) => entry.reviewed_by === "mnajdova") - xKey: event_month - yKey: ratio - color: '#9c27b0' - - label: siriwatknp - kind: line - data: - $$jsExpression: > - [...PRsOpenandReviewedQuery.data] - .map((entry) => ({ ...entry, ratio: entry.reviewed / entry.opened })) - .reverse() - .filter((entry) => entry.reviewed_by === "siriwatknp") - color: '#e91e63' - xKey: event_month - yKey: ratio - - label: mj12albert - kind: line - data: - $$jsExpression: > - [...PRsOpenandReviewedQuery.data] - .map((entry) => ({ ...entry, ratio: entry.reviewed / entry.opened })) - .reverse() - .filter((entry) => entry.reviewed_by === "mj12albert") - color: '#009688' - xKey: event_month - yKey: ratio - - label: DiegoAndai - kind: line - data: - $$jsExpression: > - [...PRsOpenandReviewedQuery.data] - .map((entry) => ({ ...entry, ratio: entry.reviewed / entry.opened })) - .reverse() - .filter((entry) => entry.reviewed_by === "DiegoAndai") - color: '#ff5722' - xKey: event_month - yKey: ratio - - label: brijeshb42 - kind: line - data: - $$jsExpression: > - [...PRsOpenandReviewedQuery.data] - .map((entry) => ({ ...entry, ratio: entry.reviewed / entry.opened })) - .reverse() - .filter((entry) => entry.reviewed_by === "brijeshb42") - color: '#ff9800' - xKey: event_month - yKey: ratio - - component: Chart - name: chart - layout: - columnSize: 1 - props: - data: - - kind: line - label: pr_community_count - data: - $$jsExpression: | - PrsPerMonth.data - xKey: event_month - yKey: pr_community_count - color: '#7cb342' - - kind: line - label: pr_maintainers_count - data: - $$jsExpression: | - PrsPerMonth.data - xKey: event_month - yKey: pr_maintainers_count - color: '#27aeef' - height: 300 - queries: - - name: PRsOpenandReviewedQuery - query: - function: PRsOpenandReviewedQuery - kind: local - - name: PrsPerMonth - query: - function: functions.ts#PRsPerMonth - kind: local - parameters: - - name: repositoryId - value: '23083156' - alias: - - 9r8fshsf - displayName: Community Core + mode: markdown + value: This page is deprecated and no longer maintained. + display: shell diff --git a/apps/tools-public/toolpad/pages/communityPerMonth/page.yml b/apps/tools-public/toolpad/pages/communityPerMonth/page.yml index c35f040f6..22bf53e27 100644 --- a/apps/tools-public/toolpad/pages/communityPerMonth/page.yml +++ b/apps/tools-public/toolpad/pages/communityPerMonth/page.yml @@ -4,166 +4,15 @@ apiVersion: v1 kind: page spec: title: Community per month + alias: + - ck33hgb + displayName: Community per month content: - - component: TextField - name: slug - layout: - columnSize: 1 - props: - label: Repository slug - defaultValue: mui/material-ui - component: Text name: text layout: columnSize: 1 props: - value: - $$jsExpression: | - (() => { - if (getRepositoryDetails.data) { - return `Repository ID: ${getRepositoryDetails.data.data.id}` - } else { - return "Not found" - } - })() - - component: Text - name: text1 - layout: - columnSize: 1 - props: - value: 'Community: PRs merged per month' - variant: h6 - sx: - mt: 2 - - component: Chart - name: chart - layout: - columnSize: 1 - props: - data: - - kind: line - label: pr_community_count - data: - $$jsExpression: > - PRsPerMonth.rows.map((row) => ({ - ...row, - ratio: - Math.round((row.pr_community_count / row.pr_maintainers_count) * 1000) / 10, - })) - xKey: event_month - yKey: pr_community_count - color: '#7cb342' - - kind: line - label: pr_maintainers_count - data: - $$jsExpression: > - PRsPerMonth.rows.map((row) => ({ - ...row, - ratio: - Math.round((row.pr_community_count / row.pr_maintainers_count) * 1000) / 10, - })) - xKey: event_month - yKey: pr_maintainers_count - color: '#27aeef' - - kind: line - label: ratio - data: - $$jsExpression: > - PRsPerMonth.rows.map((row) => ({ - ...row, - ratio: - Math.round((row.pr_community_count / row.pr_maintainers_count) * 1000) / 10, - })) - xKey: event_month - yKey: ratio - color: '#ea5545' - height: 300 - - component: Text - name: text2 - layout: - columnSize: 1 - props: - value: 'Community: Unique contributors per month' - variant: h6 - sx: - mt: 2 - - component: Chart - name: chart1 - layout: - columnSize: 1 - props: - data: - - kind: line - label: community_count - data: - $$jsExpression: > - ContributorsPerMonth.rows.map((row) => ({ - ...row, - community_count: row.pr_community_count, - maintainers_count: row.pr_maintainers_count, - ratio: - Math.round((row.pr_community_count / row.pr_maintainers_count) * 100) / 100, - })) - xKey: event_month - yKey: community_count - color: '#7cb342' - - kind: line - label: maintainers_count - data: - $$jsExpression: > - ContributorsPerMonth.rows.map((row) => ({ - ...row, - community_count: row.pr_community_count, - maintainers_count: row.pr_maintainers_count, - ratio: - Math.round((row.pr_community_count / row.pr_maintainers_count) * 100) / 100, - })) - xKey: event_month - yKey: maintainers_count - color: '#27aeef' - - kind: line - label: ratio - data: - $$jsExpression: > - ContributorsPerMonth.rows.map((row) => ({ - ...row, - community_count: row.pr_community_count, - maintainers_count: row.pr_maintainers_count, - ratio: - Math.round((row.pr_community_count / row.pr_maintainers_count) * 100) / 100, - })) - xKey: event_month - yKey: ratio - color: '#ea5545' - height: 300 - queries: - - name: PRsPerMonth - query: - function: PRsPerMonth - kind: local - parameters: - - name: repositoryId - value: - $$jsExpression: | - getRepositoryDetails.data.data.id - - name: getRepositoryDetails - query: - function: getRepositoryDetails - kind: local - parameters: - - name: slug - value: - $$jsExpression: | - slug.value - - name: ContributorsPerMonth - query: - function: ContributorsPerMonth - kind: local - parameters: - - name: repositoryId - value: - $$jsExpression: | - getRepositoryDetails.data.data.id - alias: - - ck33hgb - displayName: Community per month + mode: markdown + value: This page is deprecated and no longer maintained. + display: shell diff --git a/apps/tools-public/toolpad/pages/queryOssInsight/page.yml b/apps/tools-public/toolpad/pages/queryOssInsight/page.yml index d31ebe74f..39c69a0f2 100644 --- a/apps/tools-public/toolpad/pages/queryOssInsight/page.yml +++ b/apps/tools-public/toolpad/pages/queryOssInsight/page.yml @@ -4,77 +4,15 @@ apiVersion: v1 kind: page spec: title: Query OSS Insight - display: shell - authorization: - allowAll: true + displayName: Query OSS Insight content: - - component: TextField - name: slug - props: - label: Repository slug - defaultValue: mui/material-ui - layout: - columnSize: 1 - component: Text name: text layout: columnSize: 1 - horizontalAlign: start - verticalAlign: center props: + mode: markdown value: - $$jsExpression: | - (() => { - if (getRepositoryDetails.data) { - return `Repository ID: ${getRepositoryDetails.data.data.id}` - } else { - return "Not found" - } - })() - - component: codeComponent.Textarea - name: textarea - - component: Button - name: button - props: - content: Run query - onClick: - $$jsExpressionAction: queryOssInsight.fetch() - loading: - $$jsExpression: queryOssInsight.isLoading - - component: Paper - name: paper - children: - - component: Text - name: text1 - props: - value: - $$jsExpression: queryOssInsight.error?.message ?? '' - - component: codeComponent.AutoDataGrid - name: autoDataGrid - props: - rows: - $$jsExpression: queryOssInsight.rows - queries: - - name: queryOssInsight - mode: mutation - query: - function: queryOssInsight.ts#queryOssInsight - kind: local - parameters: - - name: repositoryId - value: - $$jsExpression: getRepositoryDetails.data?.data?.id - - name: query - value: - $$jsExpression: textarea.value - enabled: true - - name: getRepositoryDetails - query: - function: functions.ts#getRepositoryDetails - kind: local - parameters: - - name: slug - value: - $$jsExpression: | - slug.value - displayName: Query OSS Insight + "This page has moved to the code-infra-dashboard: + [Query OSS Insight](https://frontend-public.mui.com/query-oss-insight)" + display: shell diff --git a/apps/tools-public/toolpad/resources/functions.ts b/apps/tools-public/toolpad/resources/functions.ts index 0617e4ad5..76aa415c9 100644 --- a/apps/tools-public/toolpad/resources/functions.ts +++ b/apps/tools-public/toolpad/resources/functions.ts @@ -2,111 +2,6 @@ import { request } from 'graphql-request'; import mysql from 'mysql2/promise'; import SSH2Promise from 'ssh2-promise'; -export async function getRepositoryDetails(slug: string) { - const res = await fetch(`https://api.ossinsight.io/gh/repo/${slug}`, { - method: 'GET', - }); - if (res.status !== 200) { - throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 500)}`); - } - return res.json(); -} - -export async function PRsOpenandReviewedQuery() { - const openQuery = ` -with pr_opened as ( - SELECT - number, - date_format(created_at, '%Y-%m-01') AS event_month, - actor_login - FROM - github_events ge - WHERE - type = 'PullRequestEvent' - AND action = 'opened' - AND repo_id = 23083156 - AND ge.created_at >= '2021-12-01' - AND actor_login NOT LIKE '%bot' - AND actor_login NOT LIKE '%[bot]' - AND ge.actor_login NOT LIKE 'mnajdova' - AND ge.actor_login NOT LIKE 'michaldudak' - AND ge.actor_login NOT LIKE 'siriwatknp' - AND ge.actor_login NOT LIKE 'oliviertassinari' - AND ge.actor_login NOT LIKE 'mj12albert' - AND ge.actor_login NOT LIKE 'DiegoAndai' - AND ge.actor_login NOT LIKE 'brijeshb42' -), pr_reviewed as ( - SELECT - number, - date_format(created_at, '%Y-%m-01') AS event_month, - actor_login - FROM - github_events ge - WHERE - ge.repo_id = 23083156 - AND ge.type = 'PullRequestReviewEvent' - AND ge.action = 'created' - AND ge.created_at >= '2021-12-01' - AND ge.actor_login NOT LIKE '%bot' - AND ge.actor_login NOT LIKE '%[bot]' - AND ge.actor_login IN ('mnajdova','michaldudak','siriwatknp','oliviertassinari','mj12albert', 'DiegoAndai', 'brijeshb42') -), pr_reviewed_with_open_by as ( - SELECT - pr_reviewed.event_month, - pr_reviewed.number, - pr_reviewed.actor_login as reviewed_by, - pr_opened.actor_login as open_by - FROM pr_reviewed - JOIN pr_opened on pr_opened.number = pr_reviewed.number) -, pr_open_by_core as ( - SELECT - number, - date_format(created_at, '%Y-%m-01') AS event_month, - actor_login - FROM - github_events ge - WHERE - type = 'PullRequestEvent' - AND action = 'opened' - AND repo_id = 23083156 - AND ge.created_at >= '2021-12-01' - -- AND ge.created_at < '2023-01-01' - AND actor_login NOT LIKE '%bot' - AND actor_login NOT LIKE '%[bot]' - AND ge.actor_login IN - ('mnajdova','michaldudak','siriwatknp','oliviertassinari','mj12albert', 'DiegoAndai', 'brijeshb42') -), final_table AS ( - SELECT - n.event_month, - n.reviewed_by, - COUNT(DISTINCT n.number) as reviewed, - COUNT(DISTINCT p.number) as opened - FROM pr_reviewed_with_open_by n - JOIN - pr_open_by_core p ON p.actor_login = n.reviewed_by AND p.event_month = n.event_month - GROUP BY - event_month, - reviewed_by - ORDER BY - event_month DESC -) - -SELECT * FROM final_table - `; - const res = await fetch('https://api.ossinsight.io/q/playground', { - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ sql: openQuery, type: 'repo', id: '23083156' }), - method: 'POST', - }); - if (res.status !== 200) { - throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 500)}`); - } - const data = await res.json(); - return data.data; -} - export async function queryCommitStatuses(repository: string) { if (!process.env.GITHUB_TOKEN) { throw new Error(`Env variable GITHUB_TOKEN not configured`); @@ -257,191 +152,3 @@ FROM return ratio[0]; } - -export async function PRsPerMonth(repositoryId: string, startDate: string) { - if (!repositoryId) { - return []; - } - - startDate = startDate || '2016-01-01'; - - const openQuery = ` -with maintainers as ( - SELECT - DISTINCT ge.actor_login - FROM - github_events ge - WHERE - ge.repo_id = ${repositoryId} - AND ge.type = 'PullRequestEvent' - /* maintainers are defined as the ones that are allowed to merge PRs */ - AND ge.action = 'closed' - AND ge.pr_merged = 1 - AND ge.created_at >= '2016-01-01' -), pr_merged AS ( - SELECT - number, - date_format(created_at, '%Y-%m-01') AS event_month, - actor_login - FROM - github_events ge - WHERE - type = 'PullRequestEvent' - AND action = 'closed' - AND ge.pr_merged = 1 - AND repo_id = ${repositoryId} - AND ge.created_at >= '${startDate}' -), pr_opened as ( - SELECT - number, - date_format(created_at, '%Y-%m-01') AS event_month, - actor_login - FROM - github_events ge - WHERE - type = 'PullRequestEvent' - AND action = 'opened' - AND repo_id = ${repositoryId} - AND ge.created_at >= '2016-01-01' - AND actor_login NOT LIKE '%bot' - AND actor_login NOT LIKE '%[bot]' -), pr_merged_with_open_by as ( - SELECT - pr_merged.event_month, - pr_merged.number, - pr_opened.actor_login as open_by, - pr_merged.actor_login as merged_by - FROM - pr_merged - JOIN pr_opened on pr_opened.number = pr_merged.number -), pr_stats as ( - SELECT - pr_community.event_month, - COUNT(DISTINCT pr_community.number) AS pr_community_count, - COUNT(DISTINCT pr_maintainers.number) AS pr_maintainers_count - FROM pr_merged_with_open_by as pr_community - LEFT JOIN pr_merged_with_open_by as pr_maintainers - ON pr_community.event_month = pr_maintainers.event_month - WHERE - pr_community.open_by NOT IN (SELECT actor_login FROM maintainers) - AND pr_maintainers.open_by IN (SELECT actor_login FROM maintainers) - GROUP BY - pr_community.event_month - ORDER BY - pr_community.event_month asc -) - -SELECT * FROM pr_stats ge; - `; - - const res = await fetch('https://api.ossinsight.io/q/playground', { - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ - sql: openQuery, - type: 'repo', - id: repositoryId, - }), - method: 'POST', - }); - if (res.status !== 200) { - throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 500)}`); - } - const data = await res.json(); - return data.data.map((x) => ({ x: x.month, y: x.prs, ...x })); -} - -export async function ContributorsPerMonth(repositoryId: string, startDate: string) { - if (!repositoryId) { - return []; - } - - startDate = startDate || '2016-01-01'; - - const openQuery = ` -with maintainers as ( - SELECT - DISTINCT ge.actor_login - FROM - github_events ge - WHERE - ge.repo_id = ${repositoryId} - AND ge.type = 'PullRequestEvent' - /* maintainers are defined as the ones that are allowed to merge PRs */ - AND ge.action = 'closed' - AND ge.pr_merged = 1 - AND ge.created_at >= '2016-01-01' -), pr_merged AS ( - SELECT - number, - date_format(created_at, '%Y-%m-01') AS event_month, - actor_login - FROM - github_events ge - WHERE - type = 'PullRequestEvent' - AND action = 'closed' - AND ge.pr_merged = 1 - AND repo_id = ${repositoryId} - AND ge.created_at >= '${startDate}' -), pr_opened as ( - SELECT - number, - date_format(created_at, '%Y-%m-01') AS event_month, - actor_login - FROM - github_events ge - WHERE - type = 'PullRequestEvent' - AND action = 'opened' - AND repo_id = ${repositoryId} - AND ge.created_at >= '2016-01-01' - AND actor_login NOT LIKE '%bot' - AND actor_login NOT LIKE '%[bot]' -), pr_merged_with_open_by as ( - SELECT - pr_merged.event_month, - pr_merged.number, - pr_opened.actor_login as open_by, - pr_merged.actor_login as merged_by - FROM - pr_merged - JOIN pr_opened on pr_opened.number = pr_merged.number -), pr_stats as ( - SELECT - pr_community.event_month, - COUNT(DISTINCT pr_community.open_by) AS pr_community_count, - COUNT(DISTINCT pr_maintainers.open_by) AS pr_maintainers_count - FROM pr_merged_with_open_by as pr_community - LEFT JOIN pr_merged_with_open_by as pr_maintainers - ON pr_community.event_month = pr_maintainers.event_month - WHERE - pr_community.open_by NOT IN (SELECT actor_login FROM maintainers) - AND pr_maintainers.open_by IN (SELECT actor_login FROM maintainers) - GROUP BY - pr_community.event_month - ORDER BY - pr_community.event_month asc -) - -SELECT * FROM pr_stats ge; - `; - - const res = await fetch('https://api.ossinsight.io/q/playground', { - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ - sql: openQuery, - type: 'repo', - id: repositoryId, - }), - method: 'POST', - }); - if (res.status !== 200) { - throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 500)}`); - } - const data = await res.json(); - return data.data.map((x) => ({ x: x.month, y: x.prs, ...x })); -} diff --git a/apps/tools-public/toolpad/resources/queryAuditClosedIssues.ts b/apps/tools-public/toolpad/resources/queryAuditClosedIssues.ts deleted file mode 100644 index 10ac70d98..000000000 --- a/apps/tools-public/toolpad/resources/queryAuditClosedIssues.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { request } from 'graphql-request'; - -interface CloseTimelineItem { - actor: { - login: string; - }; - createdAt: string; -} - -interface Issue { - number: number; - url: string; - title: string; - timelineItems: { - nodes: CloseTimelineItem[]; - }; -} - -const query1 = ` -issues(first: 100, orderBy: { direction: DESC, field: UPDATED_AT }) { - nodes { - number - url - title - timelineItems(itemTypes: CLOSED_EVENT, last: 10) { - nodes { - ... on ClosedEvent { - actor { - login - } - createdAt - } - } - } - } -} -`; - -export async function queryAuditClosedIssues(githubUser?: string) { - if (!process.env.GITHUB_TOKEN) { - throw new Error(`Env variable GITHUB_TOKEN not configured`); - } - - const endpoint = 'https://api.github.com/graphql'; - const token = process.env.GITHUB_TOKEN; - - const query = ` - { - base_ui: repository(owner: "mui", name: "base-ui") { - ${query1} - } - mui_public: repository(owner: "mui", name: "mui-public") { - ${query1} - } - material_ui: repository(owner: "mui", name: "material-ui") { - ${query1} - } - mui_x: repository(owner: "mui", name: "mui-x") { - ${query1} - } - pigment_css: repository(owner: "mui", name: "pigment-css") { - ${query1} - } - } - `; - - const response: any = await request( - endpoint, - query, - {}, - { - Authorization: `Bearer ${token}`, - }, - ); - - const data = [ - ...response.base_ui.issues.nodes, - ...response.mui_public.issues.nodes, - ...response.material_ui.issues.nodes, - ...response.mui_x.issues.nodes, - ...response.pigment_css.issues.nodes, - ] - .map((issue: Issue) => ({ - ...issue, - timelineItems: issue.timelineItems.nodes - .map((item: CloseTimelineItem) => { - return { - createdAt: item.createdAt, - // An actor can delete his account. - actor: item.actor?.login, - }; - }) - .filter((item) => !githubUser || item.actor === githubUser), - })) - .filter((issue) => issue.timelineItems.length > 0) - .sort((a, b) => { - return a.timelineItems[0].createdAt < b.timelineItems[0].createdAt ? 1 : -1; - }); - - return data; -} diff --git a/apps/tools-public/toolpad/resources/queryAuditLabelChanges.ts b/apps/tools-public/toolpad/resources/queryAuditLabelChanges.ts deleted file mode 100644 index 5901fdef7..000000000 --- a/apps/tools-public/toolpad/resources/queryAuditLabelChanges.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { request } from 'graphql-request'; - -interface LabelTimelineItem { - label: { - name: string; - }; - actor: { - login: string; - }; - createdAt: string; -} - -interface Issue { - number: number; - url: string; - title: string; - timelineItems: { - nodes: LabelTimelineItem[]; - }; -} - -const query1 = ` -pullRequests(first: 50, orderBy: {direction: DESC, field: CREATED_AT}) { - nodes { - number - url - title - timelineItems(itemTypes: LABELED_EVENT, first: 100) { - nodes { - ... on LabeledEvent { - label { - name - } - actor { - login - } - createdAt - } - } - } - } -} -issues(first: 50, orderBy: { direction: DESC, field: CREATED_AT }) { - nodes { - number - url - title - timelineItems(itemTypes: LABELED_EVENT, first: 100) { - nodes { - ... on LabeledEvent { - label { - name - } - actor { - login - } - createdAt - } - } - } - } -} -`; - -export async function queryAuditLabelChanges() { - if (!process.env.GITHUB_TOKEN) { - throw new Error(`Env variable GITHUB_TOKEN not configured`); - } - - const endpoint = 'https://api.github.com/graphql'; - const token = process.env.GITHUB_TOKEN; - - const query = ` - { - base_ui: repository(owner: "mui", name: "base-ui") { - ${query1} - } - mui_public: repository(owner: "mui", name: "mui-public") { - ${query1} - } - material_ui: repository(owner: "mui", name: "material-ui") { - ${query1} - } - mui_x: repository(owner: "mui", name: "mui-x") { - ${query1} - } - pigment_css: repository(owner: "mui", name: "pigment-css") { - ${query1} - } - } - `; - - const response: any = await request( - endpoint, - query, - {}, - { - Authorization: `Bearer ${token}`, - }, - ); - - // console.log('response', response.materialui); - - const data = [ - ...response.base_ui.pullRequests.nodes, - ...response.base_ui.issues.nodes, - ...response.mui_public.pullRequests.nodes, - ...response.mui_public.issues.nodes, - ...response.material_ui.pullRequests.nodes, - ...response.material_ui.issues.nodes, - ...response.mui_x.pullRequests.nodes, - ...response.mui_x.issues.nodes, - ...response.pigment_css.pullRequests.nodes, - ...response.pigment_css.issues.nodes, - ] - .map((issue: Issue) => ({ - ...issue, - timelineItems: issue.timelineItems.nodes.map((item: LabelTimelineItem) => { - return { - createdAt: item.createdAt, - label: item.label.name, - // An actor can delete his account. - actor: item.actor?.login, - }; - }), - })) - .filter((issue) => issue.timelineItems.length > 0) - .sort((a, b) => { - return a.timelineItems[0].createdAt < b.timelineItems[0].createdAt ? 1 : -1; - }); - - return data; -} diff --git a/apps/tools-public/toolpad/resources/queryOssInsight.ts b/apps/tools-public/toolpad/resources/queryOssInsight.ts deleted file mode 100644 index 7e950cb0c..000000000 --- a/apps/tools-public/toolpad/resources/queryOssInsight.ts +++ /dev/null @@ -1,18 +0,0 @@ -export async function queryOssInsight(repositoryId: string, query: string) { - const res = await fetch('https://api.ossinsight.io/q/playground', { - headers: { - 'content-type': 'application/json', - }, - method: 'POST', - body: JSON.stringify({ - type: 'repo', - sql: query, - id: repositoryId, - }), - }); - if (res.status !== 200) { - throw new Error(`HTTP ${res.status}: ${(await res.text()).slice(0, 500)}`); - } - const json = await res.json(); - return json.data; -} From b5a1beb63c11d8e5c85c02b131df32037512dd2f Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:20:19 +0200 Subject: [PATCH 2/8] Lay out Query OSS Insight editor and results side-by-side The SQL textarea grew unbounded with the query, pushing the results grid off-screen. Put the editor and grid in two height-bounded panes: side-by-side on md+, stacked (editor above grid) on small screens, each scrolling internally. --- .../src/views/QueryOssInsight.tsx | 121 +++++++++++------- 1 file changed, 75 insertions(+), 46 deletions(-) diff --git a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx index d9732fbb5..a656aaba7 100644 --- a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx +++ b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx @@ -96,54 +96,83 @@ export default function QueryOssInsight() { {' '} playground for a GitHub repository. - - setSearchParams({ slug: event.target.value })} - sx={{ minWidth: 280 }} - /> - - {repositoryId !== null ? `Repository ID: ${repositoryId}` : 'Not found'} - - - setSql(event.target.value)} - multiline - minRows={4} - fullWidth - slotProps={{ htmlInput: { style: { fontFamily: 'monospace' } } }} - sx={{ mb: 2 }} - /> - - - - {mutation.isError ? ( - - ) : null} - - {/* Remove once https://github.com/mui/mui-x/issues/17077 is fixed */} - - row[ROW_ID] as number} - loading={mutation.isPending} - density="compact" - disableRowSelectionOnClick - sx={{ height: '100%' }} + + setSearchParams({ slug: event.target.value })} + sx={{ flex: 1 }} + /> + + {repositoryId !== null ? `Repository ID: ${repositoryId}` : 'Not found'} + + + setSql(event.target.value)} + multiline + slotProps={{ htmlInput: { style: { fontFamily: 'monospace' } } }} + sx={{ + flex: 1, + minHeight: 0, + '& .MuiInputBase-root': { + height: '100%', + alignItems: 'flex-start', + overflow: 'auto', + }, + '& .MuiInputBase-inputMultiline': { height: '100% !important' }, + }} /> - + + + + {mutation.isError ? ( + + ) : null} + + + {/* Remove once https://github.com/mui/mui-x/issues/17077 is fixed */} + + row[ROW_ID] as number} + loading={mutation.isPending} + density="compact" + disableRowSelectionOnClick + sx={{ height: '100%' }} + /> + + ); From ffaeeb5f09f89340cbc72dcb4cb48ca5a0c8edbc Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:32:44 +0200 Subject: [PATCH 3/8] Derive query result columns from OSS Insight field schema Columns were derived from the first row's keys, so a query returning zero rows rendered 'No columns'. The playground response includes a `fields` schema (in SELECT order) even for an empty result set; use it to build the grid columns so they always show and a zero-row result correctly reads as 'No rows'. --- apps/code-infra-dashboard/app/api/oss-insight/route.ts | 7 ++++++- apps/code-infra-dashboard/src/views/QueryOssInsight.tsx | 9 ++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/code-infra-dashboard/app/api/oss-insight/route.ts b/apps/code-infra-dashboard/app/api/oss-insight/route.ts index 03b59cf56..790f19bf2 100644 --- a/apps/code-infra-dashboard/app/api/oss-insight/route.ts +++ b/apps/code-infra-dashboard/app/api/oss-insight/route.ts @@ -50,5 +50,10 @@ export async function POST(request: NextRequest) { } const json = await response.json(); - return NextResponse.json({ rows: json.data }); + // `fields` carries the column schema in SELECT order even when `data` is + // empty, so the grid can render its columns for a zero-row result set. + return NextResponse.json({ + rows: json.data, + fields: (json.fields ?? []).map((field: { name: string }) => field.name), + }); } diff --git a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx index a656aaba7..38c042014 100644 --- a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx +++ b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx @@ -25,6 +25,7 @@ const ROW_ID = '__rowIndex'; interface QueryResult { rows: QueryRow[]; + fields: string[]; } async function runQuery(repositoryId: number, sql: string): Promise { @@ -64,6 +65,7 @@ export default function QueryOssInsight() { }); const rows = React.useMemo(() => mutation.data?.rows ?? [], [mutation.data]); + const fields = React.useMemo(() => mutation.data?.fields ?? [], [mutation.data]); const gridRows = React.useMemo( () => rows.map((row, index) => ({ ...row, [ROW_ID]: index })), @@ -71,11 +73,8 @@ export default function QueryOssInsight() { ); const columns = React.useMemo( - () => - rows.length > 0 - ? Object.keys(rows[0]).map((field) => ({ field, flex: 1, minWidth: 120 })) - : [], - [rows], + () => fields.map((field) => ({ field, flex: 1, minWidth: 120 })), + [fields], ); return ( From 368ff3b580385b99e9e51137f36362bfb86aba46 Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:33:16 +0200 Subject: [PATCH 4/8] Seed Query OSS Insight from URL and add a share link Both the repository slug and SQL are now backed by the URL search params, so a link pre-fills the editor and the input survives reloads. Local drafts keep typing responsive and are committed to the URL on Run. A CopyButton next to Run copies an absolute link encoding the current input. --- .../src/components/CopyButton.tsx | 5 +-- .../src/views/QueryOssInsight.tsx | 34 +++++++++++++++---- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/apps/code-infra-dashboard/src/components/CopyButton.tsx b/apps/code-infra-dashboard/src/components/CopyButton.tsx index 9f6352cf1..622c63dfc 100644 --- a/apps/code-infra-dashboard/src/components/CopyButton.tsx +++ b/apps/code-infra-dashboard/src/components/CopyButton.tsx @@ -9,10 +9,11 @@ import type { SxProps, Theme } from '@mui/material/styles'; interface CopyButtonProps { text: string; + title?: string; sx?: SxProps; } -export default function CopyButton({ text, sx }: CopyButtonProps) { +export default function CopyButton({ text, title = 'Copy to clipboard', sx }: CopyButtonProps) { const [copied, setCopied] = React.useState(false); const handleCopy = () => { @@ -23,7 +24,7 @@ export default function CopyButton({ text, sx }: CopyButtonProps) { }; return ( - + {copied ? : } diff --git a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx index 38c042014..1a677d404 100644 --- a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx +++ b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; +import { usePathname } from 'next/navigation'; import { useMutation, useQuery } from '@tanstack/react-query'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -10,6 +11,7 @@ import Typography from '@mui/material/Typography'; import { DataGridPremium, type GridColDef } from '@mui/x-data-grid-premium'; import Heading from '../components/Heading'; import ErrorDisplay from '../components/ErrorDisplay'; +import CopyButton from '../components/CopyButton'; import { useSearchParamsState } from '../hooks/useSearchParamsState'; import { fetchJson } from '../utils/http'; @@ -42,13 +44,18 @@ async function runQuery(repositoryId: number, sql: string): Promise } export default function QueryOssInsight() { + // Search params are the source of truth: they seed the inputs (so a link can + // pre-fill the editor) and survive reloads. Local drafts keep typing snappy + // without writing to the URL on every keystroke; they are committed on Run. const [searchParams, setSearchParams] = useSearchParamsState( - { slug: { defaultValue: 'mui/material-ui' } }, + { slug: { defaultValue: 'mui/material-ui' }, sql: { defaultValue: '' } }, { replace: true }, ); - const { slug } = searchParams; - const [sql, setSql] = React.useState(''); + const [slug, setSlug] = React.useState(searchParams.slug); + const [sql, setSql] = React.useState(searchParams.sql); + React.useEffect(() => setSlug(searchParams.slug), [searchParams.slug]); + React.useEffect(() => setSql(searchParams.sql), [searchParams.sql]); const repoQuery = useQuery({ queryKey: ['oss-insight-repo', slug], @@ -64,6 +71,20 @@ export default function QueryOssInsight() { mutationFn: () => runQuery(repositoryId!, sql), }); + const handleRun = () => { + // Persist the current input to the URL so a reload or the address bar + // reflects what was run, then execute. + setSearchParams({ slug, sql }); + mutation.mutate(); + }; + + // Build an absolute, shareable link from the *current* (possibly unsaved) + // input. `origin` is read after mount to avoid an SSR/client mismatch. + const pathname = usePathname(); + const [origin, setOrigin] = React.useState(''); + React.useEffect(() => setOrigin(window.location.origin), []); + const shareUrl = `${origin}${pathname}?${new URLSearchParams({ slug, sql })}`; + const rows = React.useMemo(() => mutation.data?.rows ?? [], [mutation.data]); const fields = React.useMemo(() => mutation.data?.fields ?? [], [mutation.data]); @@ -120,7 +141,7 @@ export default function QueryOssInsight() { size="small" label="Repository slug" value={slug} - onChange={(event) => setSearchParams({ slug: event.target.value })} + onChange={(event) => setSlug(event.target.value)} sx={{ flex: 1 }} /> @@ -144,15 +165,16 @@ export default function QueryOssInsight() { '& .MuiInputBase-inputMultiline': { height: '100% !important' }, }} /> - + + {mutation.isError ? ( From d1f327e55f8a9592e165afaf98a55d6dea286b78 Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:41:34 +0200 Subject: [PATCH 5/8] Don't rewrite the URL on run in Query OSS Insight Running a query no longer mutates the address bar. The URL is only read to seed the inputs; the share-link button is the explicit way to capture the current input into a URL. --- .../src/views/QueryOssInsight.tsx | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx index 1a677d404..ff4bd09d2 100644 --- a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx +++ b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx @@ -44,13 +44,13 @@ async function runQuery(repositoryId: number, sql: string): Promise } export default function QueryOssInsight() { - // Search params are the source of truth: they seed the inputs (so a link can - // pre-fill the editor) and survive reloads. Local drafts keep typing snappy - // without writing to the URL on every keystroke; they are committed on Run. - const [searchParams, setSearchParams] = useSearchParamsState( - { slug: { defaultValue: 'mui/material-ui' }, sql: { defaultValue: '' } }, - { replace: true }, - ); + // Search params seed the inputs so a link can pre-fill the editor. They are + // only read here — the share link below is the explicit way to capture the + // current input into a URL. + const [searchParams] = useSearchParamsState({ + slug: { defaultValue: 'mui/material-ui' }, + sql: { defaultValue: '' }, + }); const [slug, setSlug] = React.useState(searchParams.slug); const [sql, setSql] = React.useState(searchParams.sql); @@ -71,15 +71,8 @@ export default function QueryOssInsight() { mutationFn: () => runQuery(repositoryId!, sql), }); - const handleRun = () => { - // Persist the current input to the URL so a reload or the address bar - // reflects what was run, then execute. - setSearchParams({ slug, sql }); - mutation.mutate(); - }; - - // Build an absolute, shareable link from the *current* (possibly unsaved) - // input. `origin` is read after mount to avoid an SSR/client mismatch. + // Build an absolute, shareable link from the current input. `origin` is read + // after mount to avoid an SSR/client mismatch. const pathname = usePathname(); const [origin, setOrigin] = React.useState(''); React.useEffect(() => setOrigin(window.location.origin), []); @@ -168,7 +161,7 @@ export default function QueryOssInsight() { - + + Permalink + {mutation.isError ? ( From 0eb25ecc5009cc568029b2fe57f81abad2895d35 Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:53:35 +0200 Subject: [PATCH 7/8] Trim dead style override on the SQL query field The textarea height override was a no-op: the field fills the editor pane via the InputBase root (height/alignItems/overflow), and the inner textarea keeps its autosized height regardless. Drop the dead rule and document the rest. --- apps/code-infra-dashboard/src/views/QueryOssInsight.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx index e7bf28303..e17d6df3a 100644 --- a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx +++ b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx @@ -147,6 +147,9 @@ export default function QueryOssInsight() { onChange={(event) => setSql(event.target.value)} multiline slotProps={{ htmlInput: { style: { fontFamily: 'monospace' } } }} + // A multiline TextField's bordered box hugs its content by default. + // Stretch it to fill the editor pane (height), keep the text pinned + // to the top (alignItems), and scroll once the query overflows. sx={{ flex: 1, minHeight: 0, @@ -155,7 +158,6 @@ export default function QueryOssInsight() { alignItems: 'flex-start', overflow: 'auto', }, - '& .MuiInputBase-inputMultiline': { height: '100% !important' }, }} /> From 37ab3146532b82b15eb7833aeefab2feb2e15732 Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:48:05 +0200 Subject: [PATCH 8/8] Use a native textarea for the SQL query editor MUI's multiline TextField autosizes to its content, which we had to fight with height/overflow overrides that distorted its padding. A styled native textarea takes a fixed height, fills the editor pane, and scrolls natively with a clean, predictable box model. --- .../src/views/QueryOssInsight.tsx | 64 +++++++++++++------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx index e17d6df3a..400cab86f 100644 --- a/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx +++ b/apps/code-infra-dashboard/src/views/QueryOssInsight.tsx @@ -10,12 +10,40 @@ import Link from '@mui/material/Link'; import NoSsr from '@mui/material/NoSsr'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; +import { styled } from '@mui/material/styles'; import { DataGridPremium, type GridColDef } from '@mui/x-data-grid-premium'; import Heading from '../components/Heading'; import ErrorDisplay from '../components/ErrorDisplay'; import { useSearchParamsState } from '../hooks/useSearchParamsState'; import { fetchJson } from '../utils/http'; +// A native textarea, unlike MUI's multiline TextField, takes a fixed height and +// scrolls instead of autosizing to its content — which is what we want for a +// query editor that fills the pane. +const SqlEditor = styled('textarea')(({ theme }) => ({ + flex: 1, + minHeight: 0, + width: '100%', + boxSizing: 'border-box', + resize: 'none', + padding: theme.spacing(1.5), + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', + fontSize: theme.typography.pxToRem(13), + lineHeight: 1.5, + color: theme.palette.text.primary, + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + outline: 'none', + '&:hover': { borderColor: theme.palette.text.primary }, + '&:focus': { + borderColor: theme.palette.primary.main, + borderWidth: 2, + padding: `calc(${theme.spacing(1.5)} - 1px)`, + }, + '&::placeholder': { color: theme.palette.text.disabled }, +})); + interface RepoDetails { id: number; } @@ -141,25 +169,23 @@ export default function QueryOssInsight() { {repositoryId !== null ? `Repository ID: ${repositoryId}` : 'Not found'} - setSql(event.target.value)} - multiline - slotProps={{ htmlInput: { style: { fontFamily: 'monospace' } } }} - // A multiline TextField's bordered box hugs its content by default. - // Stretch it to fill the editor pane (height), keep the text pinned - // to the top (alignItems), and scroll once the query overflows. - sx={{ - flex: 1, - minHeight: 0, - '& .MuiInputBase-root': { - height: '100%', - alignItems: 'flex-start', - overflow: 'auto', - }, - }} - /> + + + SQL query + + setSql(event.target.value)} + placeholder="SELECT ..." + spellCheck={false} + /> +