Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <QueryOssInsight />;
}
59 changes: 59 additions & 0 deletions apps/code-infra-dashboard/app/api/oss-insight/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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();
// `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),
});
}
7 changes: 7 additions & 0 deletions apps/code-infra-dashboard/src/views/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -87,6 +88,12 @@ const tools: Tool[] = [
icon: <PeopleIcon />,
path: '/mui-about',
},
{
name: 'Query OSS Insight',
description: 'Run arbitrary SQL against the OSS Insight playground for a GitHub repository.',
icon: <StorageIcon />,
path: '/query-oss-insight',
},
];

export default function Landing() {
Expand Down
223 changes: 223 additions & 0 deletions apps/code-infra-dashboard/src/views/QueryOssInsight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
'use client';

import * as React from 'react';
import NextLink from 'next/link';
import { usePathname } from 'next/navigation';
import { useMutation, useQuery } from '@tanstack/react-query';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
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;
}

type QueryRow = Record<string, unknown>;

// 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[];
fields: string[];
}

async function runQuery(repositoryId: number, sql: string): Promise<QueryResult> {
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() {
// 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);
React.useEffect(() => setSlug(searchParams.slug), [searchParams.slug]);
React.useEffect(() => setSql(searchParams.sql), [searchParams.sql]);

const repoQuery = useQuery({
queryKey: ['oss-insight-repo', slug],
queryFn: () => fetchJson<RepoDetails>(`/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),
});

// A permalink to the current input. Relative so the browser resolves it; as a
// real anchor it supports cmd/middle-click to open in a new tab and
// right-click to copy the address.
const pathname = usePathname();
const permalink = `${pathname}?${new URLSearchParams({ slug, sql })}`;

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 })),
[rows],
);

const columns = React.useMemo<GridColDef[]>(
() => fields.map((field) => ({ field, flex: 1, minWidth: 120 })),
[fields],
);

return (
<Box
sx={{
mt: 4,
height: 'calc(100dvh - 120px)',
display: 'flex',
flexDirection: 'column',
minHeight: 0,
}}
>
<Heading level={1}>Query OSS Insight</Heading>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, mb: 2 }}>
Run arbitrary SQL against the{' '}
<a href="https://ossinsight.io" target="_blank" rel="noreferrer">
OSS Insight
</a>{' '}
playground for a GitHub repository.
</Typography>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
gap: 2,
flex: 1,
minHeight: 0,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
minHeight: 0,
flexShrink: 0,
width: { xs: '100%', md: 420 },
height: { xs: '45%', md: 'auto' },
}}
>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField
size="small"
label="Repository slug"
value={slug}
onChange={(event) => setSlug(event.target.value)}
sx={{ flex: 1 }}
/>
<Typography variant="body2" color="text.secondary">
{repositoryId !== null ? `Repository ID: ${repositoryId}` : 'Not found'}
</Typography>
</Box>
<Box sx={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Typography
variant="caption"
component="label"
htmlFor="sql-query"
color="text.secondary"
>
SQL query
</Typography>
<SqlEditor
id="sql-query"
value={sql}
onChange={(event) => setSql(event.target.value)}
placeholder="SELECT ..."
spellCheck={false}
/>
</Box>
<Box sx={{ flexShrink: 0, display: 'flex', alignItems: 'center', gap: 1 }}>
<Button
variant="contained"
onClick={() => mutation.mutate()}
loading={mutation.isPending}
disabled={repositoryId === null || !sql.trim()}
>
Run query
</Button>
<Link component={NextLink} href={permalink} variant="body2">
Permalink
</Link>
</Box>
{mutation.isError ? (
<ErrorDisplay title="Query failed" error={mutation.error as Error} />
) : null}
</Box>
<Box sx={{ flex: 1, minWidth: 0, minHeight: 0 }}>
{/* Remove <NoSsr> once https://github.com/mui/mui-x/issues/17077 is fixed */}
<NoSsr>
<DataGridPremium
rows={gridRows}
columns={columns}
getRowId={(row) => row[ROW_ID] as number}
loading={mutation.isPending}
density="compact"
disableRowSelectionOnClick
sx={{ height: '100%' }}
/>
</NoSsr>
</Box>
</Box>
</Box>
);
}
63 changes: 5 additions & 58 deletions apps/tools-public/toolpad/pages/auditClosedIssues/page.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading