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
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@
"description": "Enable dbt docs collaboration.",
"default": true
},
"dbt.showChangelogOnUpdate": {
"type": "boolean",
"description": "Show the What's New page automatically after the extension updates to a new minor or major version.",
"default": true
},
"dbt.whatsNewManifestUrl": {
"type": "string",
"description": "Override the What's New manifest URL (advanced). Leave empty to use the default endpoint. '{version}' is replaced with the extension version.",
"default": ""
},
"dbt.disableQueryHistory": {
"type": "boolean",
"description": "Disable Query history and bookmarks",
Expand Down Expand Up @@ -423,6 +433,11 @@
"title": "Query Results Debug Info",
"category": "dbt Power User"
},
{
"command": "dbtPowerUser.showWhatsNew",
"title": "Show What's New",
"category": "dbt Power User"
},
{
"command": "dbtPowerUser.resolveConversation",
"title": "Resolve",
Expand Down
5 changes: 5 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
getFormattedDateTime,
} from "../utils";
import { SQLLineagePanel } from "../webview_provider/sqlLineagePanel";
import { WhatsNewPanel } from "../webview_provider/whatsNewPanel";
import { AltimateScan } from "./altimateScan";
import { BigQueryCostEstimate } from "./bigQueryCostEstimate";
import { RunModel } from "./runModel";
Expand Down Expand Up @@ -106,6 +107,7 @@ export class VSCodeCommands implements Disposable {
private cteProfilerDecorationProvider: CteProfilerDecorationProvider,
private cteCodeLensProvider: CteCodeLensProvider,
private telemetry: TelemetryService,
private whatsNewPanel: WhatsNewPanel,
) {
this.disposables.push(
this.cteProfilerService,
Expand All @@ -120,6 +122,9 @@ export class VSCodeCommands implements Disposable {
commands.registerCommand("dbtPowerUser.installDbt", () =>
this.walkthroughCommands.installDbt(),
),
commands.registerCommand("dbtPowerUser.showWhatsNew", () =>
this.whatsNewPanel.show("manual"),
),
commands.registerCommand("dbtPowerUser.runCurrentModel", () => {
// `dbt run` on a singular test file is never meaningful; route it
// to `dbt test --select <test>` instead. See #1720.
Expand Down
3 changes: 3 additions & 0 deletions src/dbtPowerUserExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { TelemetryEvents } from "./telemetry/events";
import { TreeviewProviders } from "./treeview_provider";
import { ValidationProvider } from "./validation_provider";
import { WebviewViewProviders } from "./webview_provider";
import { WhatsNewPanel } from "./webview_provider/whatsNewPanel";

enum PromptAnswer {
YES = "Yes",
Expand Down Expand Up @@ -87,6 +88,7 @@ export class DBTPowerUserExtension implements Disposable {
private commentProviders: CommentProviders,
private notebookProviders: NotebookProviders,
private mcpServer: DbtPowerUserMcpServer,
private whatsNewPanel: WhatsNewPanel,
) {
this.disposables.push(
this.dbtProjectContainer,
Expand Down Expand Up @@ -177,6 +179,7 @@ export class DBTPowerUserExtension implements Disposable {
await this.mcpServer.updateMcpExtensionApi();
this.dbtProjectContainer.setContext(context);
this.dbtProjectContainer.initializeWalkthrough();
this.whatsNewPanel.checkAndShowOnActivation();
await this.dbtProjectContainer.detectDBT();
await this.dbtProjectContainer.initializeDBTProjects();
await this.statusBars.initialize();
Expand Down
20 changes: 20 additions & 0 deletions src/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ import { NewLineagePanel } from "./webview_provider/newLineagePanel";
import { OnboardingPanel } from "./webview_provider/onboardingPanel";
import { QueryResultPanel } from "./webview_provider/queryResultPanel";
import { SQLLineagePanel } from "./webview_provider/sqlLineagePanel";
import { WhatsNewPanel } from "./webview_provider/whatsNewPanel";

export const container = new Container();

Expand Down Expand Up @@ -1626,6 +1627,7 @@ container
context.container.get(CteProfilerDecorationProvider),
context.container.get(CteCodeLensProvider),
context.container.get(TelemetryService),
context.container.get(WhatsNewPanel),
);
})
.inSingletonScope();
Expand Down Expand Up @@ -1856,6 +1858,23 @@ container
})
.inSingletonScope();

container
.bind(WhatsNewPanel)
.toDynamicValue((context) => {
return new WhatsNewPanel(
context.container.get(DBTProjectContainer),
context.container.get(AltimateRequest),
context.container.get(TelemetryService),
context.container.get(SharedStateService),
context.container.get("DBTTerminal"),
context.container.get(QueryManifestService),
context.container.get(UsersService),
context.container.get(AltimateAuthService),
context.container.get(AltimateCodeChatService),
);
})
.inSingletonScope();

// Bind DbtPowerUserActionsCenter
container
.bind(DbtPowerUserActionsCenter)
Expand Down Expand Up @@ -1901,6 +1920,7 @@ container
context.container.get(CommentProviders),
context.container.get("NotebookProviders"),
context.container.get(DbtPowerUserMcpServer),
context.container.get(WhatsNewPanel),
);
})
.inSingletonScope();
3 changes: 3 additions & 0 deletions src/telemetry/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,7 @@ export enum TelemetryEvents {
"AltimateCode/RunHistoryFixClick" = "AltimateCode/RunHistoryFixClick",
"AltimateCode/TroubleshootCodeActionClick" = "AltimateCode/TroubleshootCodeActionClick",
"DocumentationEditor/UnitTestGenerateClick" = "DocumentationEditor/UnitTestGenerateClick",
"WhatsNew/PageOpened" = "whats_new_page_opened",
"WhatsNew/ChangelogItemClicked" = "changelog_item_clicked",
"WhatsNew/FullChangelogClicked" = "full_changelog_clicked",
}
242 changes: 242 additions & 0 deletions src/webview_provider/whatsNewPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { DBTTerminal } from "@altimateai/dbt-integration";
import { inject } from "inversify";
import fetch from "node-fetch";
import { ViewColumn, WebviewPanel, window, workspace } from "vscode";
import { AltimateRequest } from "../altimate";
import { DBTProjectContainer } from "../dbt_client/dbtProjectContainer";
import { AltimateAuthService } from "../services/altimateAuthService";
import { AltimateCodeChatService } from "../services/altimateCodeChatService";
import { QueryManifestService } from "../services/queryManifestService";
import { SharedStateService } from "../services/sharedStateService";
import { UsersService } from "../services/usersService";
import { TelemetryService } from "../telemetry";
import { TelemetryEvents } from "../telemetry/events";
import {
AltimateWebviewProvider,
HandleCommandProps,
} from "./altimateWebviewProvider";

// A single curated changelog file is published by the Comm Center as an
// IDE-specific manifest, filtered to dbt-power-user entries. The webview
// renders directly from this shape; entry links resolve to `base_url + anchor`
// on the website changelog.
interface WhatsNewItem {
title: string;
tag: "new" | "improved" | "fix";
summary: string;
anchor: string;
}

interface WhatsNewManifest {
product: string;
version: string;
generated_at: string;
base_url: string;
items: WhatsNewItem[];
}

// TODO: point at the Comm Center IDE-manifest endpoint once its URL and keying
// (per-version file vs single feed) are finalized. `{version}` is replaced with
// the running extension version. Until then, `dbt.whatsNewManifestUrl` can be
// set to a reachable manifest for local/staging verification.
const WHATS_NEW_MANIFEST_URL =
"https://www.altimate.ai/changelog/ide-manifest/dbt-power-user/{version}.json";

// globalState keys
const LAST_SEEN_VERSION_KEY = "whatsNew.lastSeenVersion";
const MANIFEST_CACHE_KEY = "whatsNew.cachedManifest";

type WhatsNewTrigger = "auto" | "manual";

export class WhatsNewPanel extends AltimateWebviewProvider {
public static readonly viewType = "dbtPowerUser.WhatsNew";
protected viewPath = "/whats-new";
protected panelDescription = "What's New in dbt Power User";
private currentTrigger: WhatsNewTrigger = "manual";

public constructor(
protected dbtProjectContainer: DBTProjectContainer,
protected altimateRequest: AltimateRequest,
protected telemetry: TelemetryService,
protected emitterService: SharedStateService,
@inject("DBTTerminal")
protected dbtTerminal: DBTTerminal,
protected queryManifestService: QueryManifestService,
protected usersService: UsersService,
protected altimateAuthService: AltimateAuthService,
altimateCodeChatService: AltimateCodeChatService,
) {
super(
dbtProjectContainer,
altimateRequest,
telemetry,
emitterService,
dbtTerminal,
queryManifestService,
usersService,
altimateAuthService,
altimateCodeChatService,
);
}

/**
* Opens (or focuses) the What's New panel in the editor area. Mirrors VS
* Code's own Release Notes: a one-off webview in the center, not a sidebar.
*/
public show(trigger: WhatsNewTrigger): void {
this.currentTrigger = trigger;

if (this._panel) {
(this._panel as WebviewPanel).reveal?.();
return;
}

const webviewPanel = window.createWebviewPanel(
WhatsNewPanel.viewType,
"What's New · Power user for dbt",
{ viewColumn: ViewColumn.Active },
{ enableScripts: true, retainContextWhenHidden: true },
);
this._panel = webviewPanel;

webviewPanel.onDidDispose(() => {
this._panel = undefined;
this._webview = undefined;
this.isWebviewReady = false;
});

this._webview = webviewPanel.webview;
this.renderWebviewView();
}

protected renderWebviewView(): void {
if (!this._webview) {
return;
}
this._webview.onDidReceiveMessage(this.handleCommand, this, []);
this._webview.html = this.getHtml(
this._webview,
this.dbtProjectContainer.extensionUri,
);
}

/**
* Auto-open decision, run once on activation (mirrors initializeWalkthrough).
* Opens only on a minor/major version bump; patch releases stay silent.
* The current version is always persisted so users are not re-prompted.
*/
public checkAndShowOnActivation(): void {
const current = this.dbtProjectContainer.extensionVersion;
const lastSeen = this.dbtProjectContainer.getFromGlobalState(
LAST_SEEN_VERSION_KEY,
) as string | undefined;

this.dbtProjectContainer.setToGlobalState(LAST_SEEN_VERSION_KEY, current);

const enabled = workspace
.getConfiguration("dbt")
.get<boolean>("showChangelogOnUpdate", true);
if (!enabled) {
return;
}

// Fresh install: defer to the setup walkthrough rather than the changelog.
if (!lastSeen) {
return;
}

if (WhatsNewPanel.isMinorOrMajorUpgrade(lastSeen, current)) {
this.show("auto");
}
}

private static parseVersion(
version: string,
): [number, number, number] | undefined {
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
if (!match) {
return undefined;
}
return [Number(match[1]), Number(match[2]), Number(match[3])];
}

static isMinorOrMajorUpgrade(prev: string, curr: string): boolean {
const p = WhatsNewPanel.parseVersion(prev);
const c = WhatsNewPanel.parseVersion(curr);
if (!p || !c) {
return false;
}
if (c[0] !== p[0]) {
return c[0] > p[0];
}
if (c[1] !== p[1]) {
return c[1] > p[1];
}
return false; // same or patch-only change
}

async handleCommand(message: HandleCommandProps): Promise<void> {
const { command, syncRequestId } = message;
switch (command) {
case "getWhatsNewManifest":
this.handleSyncRequestFromWebview(
syncRequestId,
() => this.getManifestForWebview(),
command,
);
break;
default:
super.handleCommand(message);
break;
}
}

private async getManifestForWebview(): Promise<WhatsNewManifest> {
const manifest = await this.fetchManifest();
this.telemetry.sendTelemetryEvent(
TelemetryEvents["WhatsNew/PageOpened"],
{ version: manifest.version, trigger: this.currentTrigger },
{ items_shown: manifest.items.length },
);
return manifest;
}

private async fetchManifest(): Promise<WhatsNewManifest> {
const version = this.dbtProjectContainer.extensionVersion;
const override = workspace
.getConfiguration("dbt")
.get<string>("whatsNewManifestUrl", "");
const url = (override || WHATS_NEW_MANIFEST_URL).replace(
"{version}",
version,
);

try {
const response = await fetch(url, {
headers: { Accept: "application/json" },
});
if (!response.ok) {
throw new Error(`Manifest request failed with ${response.status}`);
}
const manifest = (await response.json()) as WhatsNewManifest;
this.dbtProjectContainer.setToGlobalState(MANIFEST_CACHE_KEY, manifest);
return manifest;
} catch (error) {
// Offline / endpoint down: fall back to the last successfully fetched
// manifest so the panel still renders something useful.
const cached = this.dbtProjectContainer.getFromGlobalState(
MANIFEST_CACHE_KEY,
) as WhatsNewManifest | undefined;
if (cached) {
this.dbtTerminal.warn(
"whatsNew:fetchManifest",
`Failed to fetch What's New manifest; serving cached copy: ${
error instanceof Error ? error.message : String(error)
}`,
);
return cached;
}
throw error;
}
}
}
2 changes: 1 addition & 1 deletion webview_panels/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@altimateai/ui-components": "0.0.86",
"@altimateai/ui-components": "0.0.87",
"@ant-design/pro-chat": "^1.15.2",
"@finos/perspective": "^2.10.0",
"@finos/perspective-viewer": "^2.10.0",
Expand Down
Loading
Loading