Skip to content

Commit e222758

Browse files
Render stargazers line chart with Observable Plot querying Seafowl
1 parent e297d17 commit e222758

7 files changed

Lines changed: 337 additions & 61 deletions

File tree

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,102 @@
11
import style from "./Charts.module.css";
2+
import { useEffect, useRef } from "react";
23

34
import type { ImportedRepository } from "../../types";
5+
import { SqlProvider, makeSeafowlHTTPContext, useSql } from "@madatdata/react";
6+
7+
import * as Plot from "@observablehq/plot";
8+
import { useMemo } from "react";
9+
10+
import {
11+
stargazersLineChartQuery,
12+
type StargazersLineChartRow,
13+
} from "./sql-queries";
414

515
export interface ChartsProps {
616
importedRepository: ImportedRepository;
717
}
818

9-
export const Charts = ({
10-
importedRepository: {
11-
githubNamespace,
12-
githubRepository,
13-
splitgraphNamespace,
14-
splitgraphRepository,
15-
},
16-
}: ChartsProps) => {
19+
// Assume meta namespace contains both the meta tables, and all imported repositories and tables
20+
const META_NAMESPACE =
21+
process.env.NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE;
22+
23+
export const Charts = ({ importedRepository }: ChartsProps) => {
24+
const seafowlDataContext = useMemo(
25+
() =>
26+
makeSeafowlHTTPContext("https://demo.seafowl.cloud", {
27+
dbname: META_NAMESPACE,
28+
}),
29+
[]
30+
);
31+
1732
return (
1833
<div className={style.charts}>
19-
Chart for{" "}
20-
<a href={`https://github.com/${githubNamespace}/${githubRepository}`}>
21-
github.com/{githubNamespace}/{githubRepository}
22-
</a>
23-
, based on{" "}
24-
<a
25-
href={`https://www.splitgraph.com/${splitgraphNamespace}/${splitgraphRepository}`}
26-
>
27-
splitgraph.com/{splitgraphNamespace}/{splitgraphRepository}
28-
</a>
34+
<SqlProvider dataContext={seafowlDataContext}>
35+
<StargazersChart {...importedRepository} />
36+
</SqlProvider>
2937
</div>
3038
);
3139
};
40+
41+
const StargazersChart = ({
42+
splitgraphNamespace,
43+
splitgraphRepository,
44+
}: ImportedRepository) => {
45+
const containerRef = useRef<HTMLDivElement>();
46+
47+
const { response, error } = useSql<StargazersLineChartRow>(
48+
stargazersLineChartQuery({ splitgraphNamespace, splitgraphRepository })
49+
);
50+
51+
const stargazers = useMemo(() => {
52+
return !response || error
53+
? []
54+
: (response.rows ?? []).map((r) => ({
55+
...r,
56+
starred_at: new Date(r.starred_at),
57+
}));
58+
}, [response, error]);
59+
60+
useEffect(() => {
61+
if (stargazers === undefined) {
62+
return;
63+
}
64+
65+
const plot = Plot.plot({
66+
y: { grid: true },
67+
color: { scheme: "burd" },
68+
marks: [
69+
Plot.lineY(stargazers, {
70+
x: "starred_at",
71+
y: "cumulative_stars",
72+
}),
73+
// NOTE: We don't have username when querying Seafowl because it's within a JSON object,
74+
// and seafowl doesn't support querying inside JSON objects
75+
// Plot.tip(
76+
// stargazers,
77+
// Plot.pointer({
78+
// x: "starred_at",
79+
// y: "cumulative_stars",
80+
// title: (d) => `${d.username} was stargazer #${d.cumulative_stars}`,
81+
// })
82+
// ),
83+
],
84+
});
85+
86+
// There is a bug(?) in useSql where, since we can't give it dependencies, it
87+
// will re-run even with splitgraphNamespace and splitgraphRepository are undefined,
88+
// which results in an error querying Seafowl. So just don't render the chart in that case.
89+
if (splitgraphNamespace && splitgraphRepository) {
90+
containerRef.current.append(plot);
91+
}
92+
93+
return () => plot.remove();
94+
}, [stargazers]);
95+
96+
return (
97+
<>
98+
<h3>Stargazers</h3>
99+
<div ref={containerRef} />
100+
</>
101+
);
102+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.importedRepoMetadata {
2+
background: inherit;
3+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { ImportedRepository } from "../../types";
2+
import style from "./ImportedRepoMetadata.module.css";
3+
4+
import { makeStargazersTableQuery } from "./sql-queries";
5+
6+
// Assume meta namespace contains both the meta tables, and all imported repositories and tables
7+
const META_NAMESPACE =
8+
process.env.NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE;
9+
10+
interface ImportedRepoMetadataProps {
11+
importedRepository: ImportedRepository;
12+
}
13+
14+
export const ImportedRepoMetadata = ({
15+
importedRepository,
16+
}: ImportedRepoMetadataProps) => {
17+
return (
18+
<div className={style.importedRepoMetadata}>
19+
<h1>
20+
<GitHubRepoLink {...importedRepository} />
21+
</h1>
22+
<h2>GitHub Analytics</h2>
23+
24+
<ul>
25+
<li>
26+
Browse the data: <SplitgraphRepoLink {...importedRepository} />
27+
</li>
28+
<li>
29+
<SplitgraphQueryLink
30+
importedRepository={importedRepository}
31+
tableName={"stargazers"}
32+
makeQuery={makeStargazersTableQuery}
33+
/>
34+
</li>
35+
<li>
36+
<SeafowlQueryLink
37+
importedRepository={importedRepository}
38+
tableName={"stargazers"}
39+
makeQuery={makeStargazersTableQuery}
40+
/>
41+
</li>
42+
</ul>
43+
</div>
44+
);
45+
};
46+
47+
const SplitgraphRepoLink = ({
48+
splitgraphNamespace,
49+
splitgraphRepository,
50+
}: ImportedRepository) => {
51+
return (
52+
<a
53+
target="_blank"
54+
href={`https://www.splitgraph.com/${splitgraphNamespace}/${splitgraphRepository}`}
55+
>
56+
{`splitgraph.com/${splitgraphNamespace}/${splitgraphRepository}`}
57+
</a>
58+
);
59+
};
60+
61+
const GitHubRepoLink = ({
62+
githubNamespace,
63+
githubRepository,
64+
}: ImportedRepository) => {
65+
return (
66+
<a
67+
target="_blank"
68+
href={`https://github.com/${githubNamespace}/${githubRepository}`}
69+
>{`github.com/${githubNamespace}/${githubRepository}`}</a>
70+
);
71+
};
72+
73+
const SplitgraphQueryLink = ({
74+
importedRepository,
75+
makeQuery,
76+
tableName,
77+
}: {
78+
importedRepository: ImportedRepository;
79+
makeQuery: (repo: ImportedRepository) => string;
80+
tableName: string;
81+
}) => {
82+
return (
83+
<a
84+
href={makeSplitgraphQueryHref(makeQuery(importedRepository))}
85+
target="_blank"
86+
>
87+
Query {tableName} in the Splitgraph Console
88+
</a>
89+
);
90+
};
91+
92+
const SeafowlQueryLink = ({
93+
importedRepository,
94+
makeQuery,
95+
tableName,
96+
}: {
97+
importedRepository: ImportedRepository;
98+
makeQuery: (repo: ImportedRepository) => string;
99+
tableName: string;
100+
}) => {
101+
return (
102+
<a
103+
href={makeSeafowlQueryHref(makeQuery(importedRepository))}
104+
target="_blank"
105+
>
106+
Query Seafowl table {tableName} using the Splitgraph Console
107+
</a>
108+
);
109+
};
110+
111+
/** Return the URL to Splitgraph Console pointing to Splitgraph DDN */
112+
const makeSplitgraphQueryHref = (sqlQuery: string) => {
113+
const url = `https://www.splitgraph.com/query?${new URLSearchParams({
114+
sqlQuery: sqlQuery,
115+
flavor: "splitgraph",
116+
}).toString()}`;
117+
118+
return url;
119+
};
120+
121+
/** Return the URL to Splitgraph Console pointing to Seafowl db where we export tables */
122+
const makeSeafowlQueryHref = (sqlQuery: string) => {
123+
return `https://www.splitgraph.com/query?${new URLSearchParams({
124+
sqlQuery: sqlQuery,
125+
flavor: "seafowl",
126+
// Splitgraph exports to Seafowl dbname matching the username of the exporting user
127+
"database-name": META_NAMESPACE,
128+
})}`;
129+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { ImportedRepository } from "../../types";
2+
3+
// Assume meta namespace contains both the meta tables, and all imported repositories and tables
4+
const META_NAMESPACE =
5+
process.env.NEXT_PUBLIC_SPLITGRAPH_GITHUB_ANALYTICS_META_NAMESPACE;
6+
7+
type TargetSplitgraphRepo = {
8+
splitgraphNamespace?: string;
9+
splitgraphRepository: string;
10+
};
11+
12+
/**
13+
* Raw query to select all columns in the stargazers table, which can be
14+
* run on both Splitgraph and Seafowl.
15+
*
16+
* This is meant for linking to the query editor, not for rendering charts.
17+
*/
18+
export const makeStargazersTableQuery = ({
19+
splitgraphNamespace = META_NAMESPACE,
20+
splitgraphRepository,
21+
}: TargetSplitgraphRepo) => {
22+
return `SELECT
23+
"repository",
24+
"user_id",
25+
"starred_at",
26+
"user",
27+
"_airbyte_ab_id",
28+
"_airbyte_emitted_at",
29+
"_airbyte_normalized_at",
30+
"_airbyte_stargazers_hashid"
31+
FROM
32+
"${splitgraphNamespace}/${splitgraphRepository}"."stargazers"
33+
LIMIT 100;`;
34+
};
35+
36+
/** Shape of row returned by {@link stargazersLineChartQuery} */
37+
export type StargazersLineChartRow = {
38+
username: string;
39+
cumulative_stars: number;
40+
starred_at: string;
41+
};
42+
43+
/** Time series of GitHub stargazers for the given repository */
44+
export const stargazersLineChartQuery = ({
45+
splitgraphNamespace = META_NAMESPACE,
46+
splitgraphRepository,
47+
}: TargetSplitgraphRepo) => {
48+
return `SELECT
49+
COUNT(*) OVER (ORDER BY starred_at) AS cumulative_stars,
50+
starred_at
51+
FROM
52+
"${splitgraphNamespace}/${splitgraphRepository}"."stargazers"
53+
ORDER BY starred_at;`;
54+
};

examples/nextjs-import-airbyte-github-export-seafowl/components/Sidebar.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useMemo } from "react";
22
import Link from "next/link";
33
import styles from "./Sidebar.module.css";
4+
import { SqlProvider, makeSplitgraphHTTPContext } from "@madatdata/react";
45
import { useSql } from "@madatdata/react";
56

67
import type { ImportedRepository } from "../types";
@@ -46,7 +47,6 @@ const useImportedRepositories = (): ImportedRepository[] => {
4647
}
4748

4849
if (!response) {
49-
console.warn("No response received");
5050
return [];
5151
}
5252

@@ -56,27 +56,40 @@ const useImportedRepositories = (): ImportedRepository[] => {
5656
return repositories;
5757
};
5858

59-
export const Sidebar = () => {
59+
const RepositoriesList = () => {
6060
const repositories = useImportedRepositories();
6161

62+
return (
63+
<ul className={styles.repoList}>
64+
{repositories.map((repo, index) => (
65+
<li key={index}>
66+
<Link
67+
href={`/${repo.githubNamespace}/${repo.githubRepository}?splitgraphNamespace=${repo.splitgraphNamespace}&splitgraphRepository=${repo.splitgraphRepository}`}
68+
>
69+
{repo.githubNamespace}/{repo.githubRepository}
70+
</Link>
71+
</li>
72+
))}
73+
</ul>
74+
);
75+
};
76+
77+
export const Sidebar = () => {
78+
const splitgraphDataContext = useMemo(
79+
() => makeSplitgraphHTTPContext({ credential: null }),
80+
[]
81+
);
82+
6283
return (
6384
<aside className={styles.sidebar}>
6485
<div className={styles.importButtonContainer}>
6586
<Link href="/" className={styles.importButton}>
6687
Import Your Repository
6788
</Link>
6889
</div>
69-
<ul className={styles.repoList}>
70-
{repositories.map((repo, index) => (
71-
<li key={index}>
72-
<Link
73-
href={`/${repo.githubNamespace}/${repo.githubRepository}?splitgraphNamespace=${repo.splitgraphNamespace}&splitgraphRepository=${repo.splitgraphRepository}`}
74-
>
75-
{repo.githubNamespace}/{repo.githubRepository}
76-
</Link>
77-
</li>
78-
))}
79-
</ul>
90+
<SqlProvider dataContext={splitgraphDataContext}>
91+
<RepositoriesList />
92+
</SqlProvider>
8093
</aside>
8194
);
8295
};

0 commit comments

Comments
 (0)