Skip to content

Commit

Permalink
Use sys::database.last_migration to automatically refresh branch gr…
Browse files Browse the repository at this point in the history
…aph data when needed
  • Loading branch information
jaclarke committed Dec 10, 2024
1 parent ec0692b commit b4a9e14
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 75 deletions.
72 changes: 52 additions & 20 deletions shared/common/branchGraph/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,26 +91,52 @@ export const BranchGraph = observer(function BranchGraph({
instanceState,
...props
}: BranchGraphProps) {
const fetching = useRef(false);
const [refreshing, setRefreshing] = useState(true);
const [layoutNodes, setLayoutNodes] = useState<LayoutNode[] | null>(null);

const manualRefresh =
instanceState instanceof InstanceState &&
!!instanceState.databases?.length &&
instanceState.databases[0].last_migration === undefined;

useEffect(() => {
if (
fetching.current ||
!refreshing ||
!instanceId ||
!(instanceState instanceof InstanceState)
) {
return;
}
fetchMigrationsData(instanceId, instanceState).then((data) => {
if (!data) return;

const layoutNodes = joinGraphLayouts(buildBranchGraph(data));
setLayoutNodes(layoutNodes);
setRefreshing(false);
});
fetching.current = true;
instanceState.fetchDatabaseInfo().then(() =>
fetchMigrationsData(instanceId, instanceState).then((data) => {
if (!data) return;

const layoutNodes = joinGraphLayouts(buildBranchGraph(data));
setLayoutNodes(layoutNodes);
setRefreshing(false);
fetching.current = false;
})
);
}, [refreshing, instanceId, instanceState]);

useEffect(() => {
if (!manualRefresh) {
const listener = () => {
if (document.visibilityState === "visible") {
setRefreshing(true);
}
};
document.addEventListener("visibilitychange", listener);

return () => {
document.removeEventListener("visibilitychange", listener);
};
}
}, [manualRefresh]);

const fetchMigrations =
instanceState instanceof Error
? () => {
Expand Down Expand Up @@ -157,19 +183,25 @@ export const BranchGraph = observer(function BranchGraph({
instanceState instanceof Error ? instanceState : layoutNodes
}
{...props}
TopButton={({className}) => (
<button
className={cn(className, {
[styles.refreshing]: refreshing,
})}
onClick={() => {
localStorage.removeItem(`edgedb-branch-graph-${instanceId}`);
setRefreshing(true);
}}
>
<SyncIcon />
</button>
)}
TopButton={
manualRefresh
? ({className}) => (
<button
className={cn(className, {
[styles.refreshing]: refreshing,
})}
onClick={() => {
localStorage.removeItem(
`edgedb-branch-graph-${instanceId}`
);
setRefreshing(true);
}}
>
<SyncIcon />
</button>
)
: undefined
}
/>
</BranchGraphContext.Provider>
);
Expand Down
59 changes: 40 additions & 19 deletions shared/common/branchGraph/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,35 +72,56 @@ export async function fetchMigrationsData(
return null;
}

const databases = new Set(instanceState.databases);
const databases = new Map(
instanceState.databases.map((db) => [db.name, db.last_migration])
);

let migrationsData = _getBranchGraphDataFromCache(instanceId);

if (
!migrationsData ||
migrationsData.length != databases.size ||
!migrationsData.every(({branch}) => databases.has(branch))
migrationsData &&
migrationsData.length == databases.size &&
migrationsData.every(
({branch, migrations}) =>
databases.get(branch) ===
(migrations[migrations.length - 1]?.name ?? null)
)
) {
migrationsData = await Promise.all(
instanceState.databases.map(async (dbName) => {
const conn = instanceState.getConnection(dbName);
return {
branch: dbName,
migrations: sortMigrations(
((
await conn.query(`
// all cached data is still valid
return migrationsData;
}

migrationsData = await Promise.all(
instanceState.databases.map(async ({name, last_migration}) => {
if (last_migration !== undefined) {
const cachedMigration = migrationsData?.find((d) => d.branch === name);
if (
cachedMigration &&
(cachedMigration.migrations[cachedMigration.migrations.length - 1]
?.name ?? null) === last_migration
) {
// return cached data for branch if still valid
return cachedMigration;
}
}

const conn = instanceState.getConnection(name);
return {
branch: name,
migrations: sortMigrations(
((
await conn.query(`
select schema::Migration {
id,
name,
parentId := assert_single(.parents.id)
}`)
).result as Migration[]) ?? []
),
};
})
);
_storeBranchGraphDataInCache(instanceId, migrationsData);
}
).result as Migration[]) ?? []
),
};
})
);
_storeBranchGraphDataInCache(instanceId, migrationsData);

return migrationsData;
}
Expand Down
4 changes: 2 additions & 2 deletions shared/studio/components/databasePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ export default observer(function DatabasePageLoadingWrapper(
const instanceState = useInstanceState();
const {gotoInstancePage} = useDBRouter();

if (!instanceState.databases) {
if (!instanceState.databaseNames) {
return (
<div className={cn(styles.card, styles.loadingState)}>
Fetching instance info...
</div>
);
}

if (!instanceState.databases.includes(props.databaseName)) {
if (!instanceState.databaseNames.includes(props.databaseName)) {
return (
<ErrorPage
title="Database doesn't exist"
Expand Down
6 changes: 3 additions & 3 deletions shared/studio/components/modals/createBranch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ export default function CreateBranchModal({
label="From branch"
items={[
{id: null, label: <i>Empty</i>},
...(instanceState.databases ?? []).map((db) => ({
id: db,
label: db,
...(instanceState.databases ?? []).map(({name}) => ({
id: name,
label: name,
})),
]}
selectedItemId={watch("fromBranch")}
Expand Down
2 changes: 1 addition & 1 deletion shared/studio/state/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class DatabaseState extends Model({
statuses.includes("DROP BRANCH") ||
statuses.includes("ALTER BRANCH")
) {
instanceCtx.get(this)!.fetchInstanceInfo();
instanceCtx.get(this)!.fetchDatabaseInfo();
} else {
const dbState = dbCtx.get(this)!;
dbState.fetchSchemaData();
Expand Down
95 changes: 70 additions & 25 deletions shared/studio/state/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,47 +57,71 @@ export class InstanceState extends Model({
}) {
@observable instanceName: string | null = null;
@observable.ref serverVersion: ServerVersion | null = null;
@observable databases: string[] | null = null;
@observable.ref databases:
| {name: string; last_migration?: string | null}[]
| null = null;
@observable roles: string[] | null = null;

@computed
get instanceId() {
return this._instanceId ?? this.instanceName;
}

@computed
get databaseNames() {
return this.databases?.map((d) => d.name) ?? null;
}

defaultConnection: Connection | null = null;

_refreshAuthToken: (() => void) | null = null;

private async _sysConnFetch(query: string, cardinality: Cardinality) {
return (
await AdminUIFetchConnection.create(
createAuthenticatedFetch({
serverUrl: this.serverUrl,
database: "__edgedbsys__",
user: this.authUsername ?? "edgedb",
authToken: this.authToken!,
}),
codecsRegistry,
this.serverVersion
? [this.serverVersion.major, this.serverVersion.minor]
: undefined
).fetch(
query,
null,
OutputFormat.BINARY,
cardinality,
Session.defaults()
)
).result;
}

private get databasesQuery() {
return `sys::Database {
name,
${
!this.serverVersion || this.serverVersion?.major >= 6
? `last_migration,`
: ""
}
}`;
}

async fetchInstanceInfo() {
const client = AdminUIFetchConnection.create(
createAuthenticatedFetch({
serverUrl: this.serverUrl,
database: "__edgedbsys__",
user: this.authUsername ?? "edgedb",
authToken: this.authToken!,
}),
codecsRegistry,
this.serverVersion
? [this.serverVersion.major, this.serverVersion.minor]
: undefined
);
try {
const data = (
await client.fetch(
`
const data = await this._sysConnFetch(
`
select {
instanceName := sys::get_instance_name(),
version := sys::get_version(),
databases := sys::Database.name,
databases := ${this.databasesQuery},
roles := sys::Role.name,
}`,
null,
OutputFormat.BINARY,
Cardinality.ONE,
Session.defaults()
)
).result;
Cardinality.ONE
);

runInAction(() => {
this.instanceName = data.instanceName ?? "_localdev";
Expand All @@ -106,7 +130,28 @@ export class InstanceState extends Model({
this.roles = data.roles;
});

cleanupOldSchemaDataForInstance(this.instanceId!, this.databases!);
cleanupOldSchemaDataForInstance(this.instanceId!, this.databaseNames!);
} catch (err) {
if (err instanceof AuthenticationError) {
this._refreshAuthToken?.();
} else {
throw err;
}
}
}

async fetchDatabaseInfo() {
try {
const data = await this._sysConnFetch(
`select ${this.databasesQuery}`,
Cardinality.MANY
);

runInAction(() => {
this.databases = data;
});

cleanupOldSchemaDataForInstance(this.instanceId!, this.databaseNames!);
} catch (err) {
if (err instanceof AuthenticationError) {
this._refreshAuthToken?.();
Expand All @@ -129,7 +174,7 @@ export class InstanceState extends Model({
config: frozen({
serverUrl: this.serverUrl,
authToken: this.authToken!,
database: this.databases![0],
database: this.databases![0].name,
user: this.authUsername ?? this.roles![0],
}),
serverVersion: frozen(this.serverVersion),
Expand Down
2 changes: 1 addition & 1 deletion shared/studio/tabs/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ const FirstRunDashboard = observer(function FirstRunDashboard() {
const dbState = useDatabaseState();
const {navigate} = useDBRouter();

const exampleDBExists = instanceState.databases?.includes("_example");
const exampleDBExists = instanceState.databaseNames?.includes("_example");
const dbOrBranch = useMemo(() => {
for (const func of dbState.schemaData?.functions.values() ?? []) {
if (func.name === "sys::get_current_branch") return "branch";
Expand Down
6 changes: 3 additions & 3 deletions shared/studio/tabs/repl/state/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ export async function handleSlashCommand(
case "connect": {
const dbName = args.join(" ");
const instanceState = instanceCtx.get(repl)!;
await instanceState.fetchInstanceInfo();
await instanceState.fetchDatabaseInfo();

if (instanceState.databases!.includes(dbName)) {
if (instanceState.databaseNames!.includes(dbName)) {
item.setCommandResult({kind: CommandOutputKind.none});
repl.navigation?.(`${encodeURIComponent(dbName)}/repl`);
} else {
Expand Down Expand Up @@ -182,7 +182,7 @@ async function handleListCommand(
switch (type) {
case "databases":
{
await instanceState.fetchInstanceInfo();
await instanceState.fetchDatabaseInfo();

item.setCommandResult({
kind: CommandOutputKind.text,
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/databasePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const DatabasePage = observer(function DatabasePage() {
<HeaderTab headerKey="database">
<HeaderNavMenu
currentDB={params.databaseName}
databases={appState.instanceState.databases}
databases={appState.instanceState.databaseNames}
instanceState={appState.instanceState}
/>
</HeaderTab>
Expand Down

0 comments on commit b4a9e14

Please sign in to comment.