From e5053ef3eeef47c192a86f10d8bcf21ef9796901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 13:57:33 +0200 Subject: [PATCH 01/17] [CDF-27845] Apply ToolkitPanel to deploy command output Replace bare rich.panel.Panel usage with ToolkitPanel from ui.py, consistent with the build v2 styling improvements. Also consolidates three separate clean-mode panels into one with dynamic flag text. Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/commands/deploy.py | 27 ++++++++-------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy.py b/cognite_toolkit/_cdf_tk/commands/deploy.py index 0cdf5a0d59..490cbdb2b7 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy.py @@ -6,7 +6,6 @@ from cognite.client.exceptions import CogniteAPIError, CogniteDuplicatedError from rich import print from rich.markup import escape -from rich.panel import Panel from cognite_toolkit._cdf_tk.client import ToolkitClient from cognite_toolkit._cdf_tk.client._resource_base import T_Identifier, T_RequestResource, T_ResponseResource @@ -56,6 +55,7 @@ LowSeverityWarning, ToolkitDependenciesIncludedWarning, ) +from cognite_toolkit._cdf_tk.ui import ToolkitPanel from cognite_toolkit._cdf_tk.utils import humanize_collection, read_yaml_file from cognite_toolkit._cdf_tk.utils.auth import EnvironmentVariables @@ -168,16 +168,11 @@ def _order_loaders( @staticmethod def _start_message(build_dir: Path, dry_run: bool, env_vars: EnvironmentVariables) -> None: - environment_vars = "" - if not _RUNNING_IN_BROWSER: - environment_vars = f"\n\nConnected to {env_vars.as_string()}" verb = "Checking" if dry_run else "Deploying" - print( - Panel( - f"[bold]{verb}[/]resource files from {build_dir} directory.{environment_vars}", - expand=False, - ) - ) + content = f"[bold]{verb}[/] resource files from {build_dir} directory." + if not _RUNNING_IN_BROWSER: + content += f"\n\nConnected to {env_vars.as_string()}" + print(ToolkitPanel(content, title="[bold]Deploy[/]", expand=False)) def clean_all_resources( self, @@ -193,14 +188,10 @@ def clean_all_resources( verbose: bool, ) -> None: # Drop has to be done in the reverse order of deploy. - if drop and drop_data: - print(Panel("[bold] Cleaning resources as --drop and --drop-data are passed[/]")) - elif drop: - print(Panel("[bold] Cleaning resources as --drop is passed[/]")) - elif drop_data: - print(Panel("[bold] Cleaning resources as --drop-data is passed[/]")) - else: + if not (drop or drop_data): return None + flags = "--drop and --drop-data" if (drop and drop_data) else ("--drop" if drop else "--drop-data") + print(ToolkitPanel(f"Cleaning resources as {flags} is passed", title="[bold]Clean[/]", expand=False)) for loader_cls in reversed(ordered_loaders): if not issubclass(loader_cls, ResourceIO): @@ -289,7 +280,7 @@ def deploy_all_resources( """ if verbose: - print(Panel("[bold]DEPLOYING resources...[/]")) + print(ToolkitPanel("[bold]Deploying resources...[/]", title="[bold]Deploy[/]", expand=False)) if ordered_loaders is None: selected_loaders = self._clean_command.get_selected_loaders(build_dir, set(), None) From c1a620291ba7de4cc108d7900ae6265c84323661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 20:54:48 +0200 Subject: [PATCH 02/17] [CDF-27845] Remove expand=False and adopt AuraColor in deploy output - Remove all expand=False (ToolkitPanel expands by default, consistent with build output) - Replace bare string border colors ("green"/"yellow") with AuraColor constants (GREEN, AMBER) in both DeployCommand and DeployV2Command - Add AuraColor.AMBER border to the clean-mode panel in DeployCommand Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/commands/deploy.py | 12 +- .../_cdf_tk/commands/deploy_v2/command.py | 183 ++++++++++-------- 2 files changed, 110 insertions(+), 85 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy.py b/cognite_toolkit/_cdf_tk/commands/deploy.py index 490cbdb2b7..5bad606f95 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy.py @@ -55,7 +55,7 @@ LowSeverityWarning, ToolkitDependenciesIncludedWarning, ) -from cognite_toolkit._cdf_tk.ui import ToolkitPanel +from cognite_toolkit._cdf_tk.ui import AuraColor, ToolkitPanel from cognite_toolkit._cdf_tk.utils import humanize_collection, read_yaml_file from cognite_toolkit._cdf_tk.utils.auth import EnvironmentVariables @@ -172,7 +172,7 @@ def _start_message(build_dir: Path, dry_run: bool, env_vars: EnvironmentVariable content = f"[bold]{verb}[/] resource files from {build_dir} directory." if not _RUNNING_IN_BROWSER: content += f"\n\nConnected to {env_vars.as_string()}" - print(ToolkitPanel(content, title="[bold]Deploy[/]", expand=False)) + print(ToolkitPanel(content, title="[bold]Deploy[/]")) def clean_all_resources( self, @@ -191,7 +191,11 @@ def clean_all_resources( if not (drop or drop_data): return None flags = "--drop and --drop-data" if (drop and drop_data) else ("--drop" if drop else "--drop-data") - print(ToolkitPanel(f"Cleaning resources as {flags} is passed", title="[bold]Clean[/]", expand=False)) + print( + ToolkitPanel( + f"Cleaning resources as {flags} is passed", title="[bold]Clean[/]", border_style=AuraColor.AMBER.rich + ) + ) for loader_cls in reversed(ordered_loaders): if not issubclass(loader_cls, ResourceIO): @@ -280,7 +284,7 @@ def deploy_all_resources( """ if verbose: - print(ToolkitPanel("[bold]Deploying resources...[/]", title="[bold]Deploy[/]", expand=False)) + print(ToolkitPanel("[bold]Deploying resources...[/]", title="[bold]Deploy[/]")) if ordered_loaders is None: selected_loaders = self._clean_command.get_selected_loaders(build_dir, set(), None) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index 10f15371a2..c87465f080 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -10,11 +10,9 @@ import questionary from pydantic import ValidationError -from rich.console import Console +from rich.console import Console, Group, RenderableType from rich.markup import escape -from rich.panel import Panel from rich.progress import Progress -from rich.table import Table from yaml import YAMLError from cognite_toolkit._cdf_tk.client import ToolkitClient @@ -52,6 +50,7 @@ catch_warnings, ) from cognite_toolkit._cdf_tk.tracker import Tracker +from cognite_toolkit._cdf_tk.ui import AuraColor, ToolkitPanel, ToolkitPanelSection, ToolkitTable from cognite_toolkit._cdf_tk.utils import humanize_collection, sanitize_filename, to_diff from cognite_toolkit._cdf_tk.utils.auth import EnvironmentVariables from cognite_toolkit._version import __version__ @@ -326,10 +325,15 @@ def read_build_directory( def _display_startup(self, operation: str, build_dir: Path, cdf_project: str, console: Console) -> None: console.print( - Panel( - f"{operation.title()}ing {build_dir.as_posix()} directory:\n - Toolkit Version '{__version__!s}'\n" - f" - CDF project {cdf_project!r}", - expand=False, + ToolkitPanel( + ToolkitPanelSection( + content=[ + f"[bold]{operation.title()}ing[/] {build_dir.as_posix()}", + f"Toolkit version: {__version__!s}", + f"CDF project: {cdf_project!r}", + ] + ), + title=f"[bold]{operation.title()}[/]", ) ) @@ -359,43 +363,45 @@ def _display_read_dir(self, build_dir: ReadBuildDirectory, console: Console, ver if invalid_yaml_file_count: summary_lines.append(f"[red]✗[/] [bold]{invalid_yaml_file_count}[/] invalid yaml files") - console.print( - Panel( - "\n".join(summary_lines), - title=f"[bold]Build directory ({build_dir.path.as_posix()})[/]", - border_style="yellow" if has_issues else "green", - expand=False, - ) - ) + sections: list[RenderableType] = [ToolkitPanelSection(content=summary_lines)] - if warnings: - for warning in warnings: - self.warn(warning, console=console) - - if not verbose and (skipped_dir_count or invalid_dir_count or invalid_yaml_file_count): - console.print( - f"{HINT_LEAD_TEXT} Use --verbose flag to get more details about the skipped and invalid directories and files." - ) if verbose: if build_dir.skipped_directories: - table = Table(title="Skipped Directories", expand=False, show_edge=False) - table.add_column("Directory", style="dim") + t = ToolkitTable("Directory") for dir_ in build_dir.skipped_directories: - table.add_row(dir_.directory.as_posix()) - console.print(table) + t.add_row(dir_.directory.as_posix()) + sections.append(ToolkitPanelSection(title="Skipped directories", content=[t.as_panel_detail()])) if build_dir.invalid_directories: - table = Table(title="Invalid Directories", expand=False, show_edge=False) - table.add_column("Directory", style="red") + t = ToolkitTable("Directory") + t.columns[0].style = "red" for inv_dir in build_dir.invalid_directories: - table.add_row(inv_dir.as_posix()) - console.print(table) + t.add_row(inv_dir.as_posix()) + sections.append(ToolkitPanelSection(title="Invalid directories", content=[t.as_panel_detail()])) if invalid_yaml_file_count: - table = Table(title="Invalid YAML Files", expand=False, show_edge=False) - table.add_column("File", style="red") + t = ToolkitTable("File") + t.columns[0].style = "red" for dir_ in build_dir.resource_directories: for file in dir_.invalid_files: - table.add_row(file.as_posix()) - console.print(table) + t.add_row(file.as_posix()) + sections.append(ToolkitPanelSection(title="Invalid YAML files", content=[t.as_panel_detail()])) + elif skipped_dir_count or invalid_dir_count or invalid_yaml_file_count: + sections.append( + ToolkitPanelSection( + description=f"{HINT_LEAD_TEXT} Use --verbose to see details about skipped and invalid directories and files." + ) + ) + + console.print( + ToolkitPanel( + Group(*sections), + title=f"[bold]Build directory ({build_dir.path.as_posix()})[/]", + border_style=AuraColor.AMBER.rich if has_issues else AuraColor.GREEN.rich, + ) + ) + + if warnings: + for warning in warnings: + self.warn(warning, console=console) def _validate_cdf_project( self, @@ -468,22 +474,28 @@ def create_deployment_plan(cls, read_dir: ReadBuildDirectory) -> list[Deployment @classmethod def _display_plan(cls, plan: list[DeploymentStep], operation: str, operation_noun: str, console: Console) -> None: if not plan: - console.print(f"[bold yellow]No resources to {operation}.[/]") + console.print( + ToolkitPanel( + f"[bold yellow]No resources to {operation}.[/]", + title=f"[bold]{operation_noun.title()} plan[/]", + border_style=AuraColor.AMBER.rich, + ) + ) return step_count = len(plan) total_files = sum(len(step.files) for step in plan) - summary_lines = [ - f"[green]✓[/] [bold]{step_count}[/] resource types to {operation}", - f"[green]✓[/] [bold]{total_files}[/] resources to {operation}", - ] console.print( - Panel( - "\n".join(summary_lines), + ToolkitPanel( + ToolkitPanelSection( + content=[ + f"[green]✓[/] [bold]{step_count}[/] resource types to {operation}", + f"[green]✓[/] [bold]{total_files}[/] resource files to {operation}", + ] + ), title=f"[bold]{operation_noun.title()} plan[/]", - border_style="green", - expand=False, + border_style=AuraColor.GREEN.rich, ) ) @@ -705,10 +717,9 @@ def _categorize_resources( for sensitive in crud.sensitive_strings(resource.request): diff_str = diff_str.replace(sensitive, "********") console.print( - Panel( + ToolkitPanel( diff_str, - title=f"{crud.display_name}: {identifier}", - expand=False, + title=f"[bold]{crud.display_name}:[/] {identifier}", ) ) return resources @@ -887,14 +898,20 @@ def _display_results( cls, results: Sequence[DeploymentResult], operation: str, operation_noun: str, console: Console, verbose: bool ) -> None: if not results: - console.print(f"No resources were {operation}ed.") + console.print( + ToolkitPanel( + f"No resources were {operation}ed.", + title=f"[bold]{operation_noun.title()} summary[/]", + ) + ) return is_dry_run = results[0].is_dry_run - title = f"{operation_noun.title()} Summary" + panel_title = f"[bold]{operation_noun.title()} summary[/]" if is_dry_run: - title += " (dry run)" - table = Table(title=title, show_lines=False) + panel_title += " [dim](dry run)[/]" + + table = ToolkitTable() table.add_column("Resource", style="cyan") if is_dry_run: table.add_column("Would create", justify="right", style="green") @@ -904,7 +921,6 @@ def _display_results( table.add_column("Created", justify="right", style="green") table.add_column("Updated", justify="right", style="yellow") table.add_column("Deleted", justify="right", style="red") - table.add_column("Unchanged", justify="right", style="dim") table.add_column("Skipped", justify="right", style="yellow") table.add_column("Total", justify="right", style="cyan") @@ -932,11 +948,7 @@ def _display_results( str(result.total_count), ] if is_dry_run: - if result.is_missing_write_acl: - row.append("[red]No[/]") - else: - row.append("[green]Yes[/]") - + row.append("[red]No[/]" if result.is_missing_write_acl else "[green]Yes[/]") table.add_row(*row) total += result @@ -952,28 +964,29 @@ def _display_results( f"[bold]{total.total_count}[/]", ] if is_dry_run: - if total.is_missing_write_acl: - last_row.append("[red]No[/]") - else: - last_row.append("[green]Yes[/]") - + last_row.append("[red]No[/]" if total.is_missing_write_acl else "[green]Yes[/]") table.add_row(*last_row) - console.print(table) + sections: list[RenderableType] = [ToolkitPanelSection(content=[table.as_panel_detail()])] if total.skipped and not verbose: most_common = Counter(skip.code for skip in total.skipped).most_common(n=3) - console.print( - f"{HINT_LEAD_TEXT}A total of {total.skipped_count} resources were skipped during {operation_noun}. " - f"The most common reasons were: {', '.join(f'{code} ({count} occurrences)' for code, count in most_common)}. " - f"Use --verbose flag to get details about all skipped resources." + sections.append( + ToolkitPanelSection( + description=( + f"{HINT_LEAD_TEXT}A total of {total.skipped_count} resources were skipped during {operation_noun}. " + f"The most common reasons were: {', '.join(f'{code} ({count} occurrences)' for code, count in most_common)}. " + f"Use --verbose to see all skipped resources." + ) + ) ) - if verbose and total.skipped: - skipped_str = [ - f"{skip.id!s} in file {skip.source_file.as_posix()} | {skip.code} | {skip.reason}" - for skip in total.skipped - ] - console.print(Panel("\n".join(skipped_str), title="Skipped resources", expand=False)) + elif verbose and total.skipped: + skipped_table = ToolkitTable("Resource", "File", "Code", "Reason") + for skip in total.skipped: + skipped_table.add_row(str(skip.id), skip.source_file.as_posix(), skip.code, skip.reason) + sections.append(ToolkitPanelSection(title="Skipped resources", content=[skipped_table.as_panel_detail()])) + + console.print(ToolkitPanel(Group(*sections), title=panel_title)) @classmethod def _track_deployment_result( @@ -1054,14 +1067,22 @@ def _find_raw_tables(cls, build_lineage: BuildLineage) -> Mapping[RawTableSelect def _display_deprecation_warning(cls, raw_files: Mapping[RawTableSelector, list[Path]], console: Console) -> None: raw_table_count = len(raw_files) file_count = sum(len(files) for files in raw_files.values()) + plural_t = "" if raw_table_count == 1 else "s" + plural_f = "" if file_count == 1 else "s" console.print( - Panel( - f"[yellow]Deprecation Warning[/]\n\n" - f"You are deploying {raw_table_count} raw table{'' if raw_table_count == 1 else 's'} based on {file_count} file{'' if file_count == 1 else 's'}.\n\n" - f"Support for deploying raw tables through the deploy command will be removed in a future release. " - f"Please migrate your raw tables to use the new data plugin. See the documentation for more details.", - title="Deprecation Warning", - border_style="yellow", - expand=False, + ToolkitPanel( + Group( + ToolkitPanelSection( + description=f"[bold]{raw_table_count}[/] raw table{plural_t} from [bold]{file_count}[/] file{plural_f}." + ), + ToolkitPanelSection( + description=( + "Support for deploying raw tables through the deploy command will be removed in a future release. " + "Please migrate your raw tables to use the new data plugin. See the documentation for more details." + ) + ), + ), + title="[bold]Deprecation warning[/]", + border_style=AuraColor.AMBER.rich, ) ) From bcda9334a3b92ad5989bb8977881333ca0ff2b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 21:21:53 +0200 Subject: [PATCH 03/17] [CDF-27845] Combine setup panels, drop operation_noun, use AuraColor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge _display_startup, _display_read_dir, and _display_plan into a single _display_setup method that prints one 'Setting up deploy' ToolkitPanel. Wraps create_deployment_plan in try/except so failures render an amber panel showing what was collected before re-raising. Removes the operation_noun property from DeployOptions — all display strings now derive directly from the operation str ('deploy'/'clean'). Co-Authored-By: Claude Sonnet 4.6 --- .../_cdf_tk/commands/deploy_v2/command.py | 164 +++++++++--------- 1 file changed, 84 insertions(+), 80 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index c87465f080..167fe9a8f9 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -71,10 +71,6 @@ class DeployOptions: environment_variables: dict[str, str | None] | None = None deployment_dir: Path | None = None - @property - def operation_noun(self) -> str: - return {"deploy": "deployment", "clean": "deletion"}[self.operation] - @dataclass class ResourceDirectory: @@ -212,12 +208,7 @@ def deploy( client = env_vars.get_client(is_strict_validation=build_dir.is_strict_validation) self._validate_cdf_project(build_dir, options.operation, options.cdf_project, env_vars.CDF_PROJECT) - self._display_startup(options.operation, build_dir.path, client.config.project, client.console) - self._display_read_dir(build_dir, client.console, options.verbose) - - plan = self.create_deployment_plan(build_dir) - - self._display_plan(plan, options.operation, options.operation_noun, client.console) + plan = self._display_setup(options.operation, build_dir, client.config.project, client.console, options.verbose) clean_result: Sequence[DeploymentResult] | None = None if options.drop and (options.operation == "clean" or not options.dry_run): @@ -232,7 +223,7 @@ def deploy( if clean_result is not None: self._merge_clean_results(results, clean_result) - self._display_results(results, options.operation, options.operation_noun, client.console, options.verbose) + self._display_results(results, options.operation, client.console, options.verbose) self._track_deployment_result(self.tracker, client, results, options.operation) if build_lineage and (raw_files := self._find_raw_tables(build_lineage)): @@ -323,85 +314,126 @@ def read_build_directory( cdf_project=cdf_project, ) - def _display_startup(self, operation: str, build_dir: Path, cdf_project: str, console: Console) -> None: - console.print( - ToolkitPanel( - ToolkitPanelSection( - content=[ - f"[bold]{operation.title()}ing[/] {build_dir.as_posix()}", - f"Toolkit version: {__version__!s}", - f"CDF project: {cdf_project!r}", - ] - ), - title=f"[bold]{operation.title()}[/]", - ) + def _display_setup( + self, + operation: str, + build_dir: ReadBuildDirectory, + cdf_project: str, + console: Console, + verbose: bool, + ) -> list[DeploymentStep]: + startup_section = ToolkitPanelSection( + content=[ + f"Build path: {build_dir.path.as_posix()}", + f"CDF project: {cdf_project!r}", + f"Toolkit version: {__version__!s}", + ], ) - def _display_read_dir(self, build_dir: ReadBuildDirectory, console: Console, verbose: bool) -> None: - warnings = list(build_dir.create_warnings()) + read_dir_warnings = list(build_dir.create_warnings()) resource_dir_count = len(build_dir.resource_directories) skipped_dir_count = len(build_dir.skipped_directories) invalid_dir_count = len(build_dir.invalid_directories) - resource_file_count = sum( len(files) for dir_ in build_dir.resource_directories for files in dir_.files_by_crud.values() ) invalid_yaml_file_count = sum(len(dir_.invalid_files) for dir_ in build_dir.resource_directories) + has_issues = bool(read_dir_warnings or invalid_dir_count or invalid_yaml_file_count) - has_issues = bool(warnings or invalid_dir_count or invalid_yaml_file_count) - - summary_lines = [ + read_dir_summary = [ f"[green]✓[/] [bold]{resource_dir_count}[/] resource directories", f"[green]✓[/] [bold]{resource_file_count:,}[/] resource files", ] - if warnings: - summary_lines.append(f"[yellow]![/] [bold]{len(warnings)}[/] warnings during reading") + if read_dir_warnings: + read_dir_summary.append(f"[yellow]![/] [bold]{len(read_dir_warnings)}[/] warnings during reading") if skipped_dir_count: - summary_lines.append(f"[dim]○[/] [bold]{skipped_dir_count}[/] skipped directories") + read_dir_summary.append(f"[dim]○[/] [bold]{skipped_dir_count}[/] skipped directories") if invalid_dir_count: - summary_lines.append(f"[red]✗[/] [bold]{invalid_dir_count}[/] invalid directories") + read_dir_summary.append(f"[red]✗[/] [bold]{invalid_dir_count}[/] invalid directories") if invalid_yaml_file_count: - summary_lines.append(f"[red]✗[/] [bold]{invalid_yaml_file_count}[/] invalid yaml files") - - sections: list[RenderableType] = [ToolkitPanelSection(content=summary_lines)] + read_dir_summary.append(f"[red]✗[/] [bold]{invalid_yaml_file_count}[/] invalid yaml files") + read_dir_subsections: list[RenderableType] = [ToolkitPanelSection(content=read_dir_summary)] if verbose: if build_dir.skipped_directories: t = ToolkitTable("Directory") for dir_ in build_dir.skipped_directories: t.add_row(dir_.directory.as_posix()) - sections.append(ToolkitPanelSection(title="Skipped directories", content=[t.as_panel_detail()])) + read_dir_subsections.append( + ToolkitPanelSection(title="Skipped directories", content=[t.as_panel_detail()]) + ) if build_dir.invalid_directories: t = ToolkitTable("Directory") t.columns[0].style = "red" for inv_dir in build_dir.invalid_directories: t.add_row(inv_dir.as_posix()) - sections.append(ToolkitPanelSection(title="Invalid directories", content=[t.as_panel_detail()])) + read_dir_subsections.append( + ToolkitPanelSection(title="Invalid directories", content=[t.as_panel_detail()]) + ) if invalid_yaml_file_count: t = ToolkitTable("File") t.columns[0].style = "red" for dir_ in build_dir.resource_directories: for file in dir_.invalid_files: t.add_row(file.as_posix()) - sections.append(ToolkitPanelSection(title="Invalid YAML files", content=[t.as_panel_detail()])) + read_dir_subsections.append( + ToolkitPanelSection(title="Invalid YAML files", content=[t.as_panel_detail()]) + ) elif skipped_dir_count or invalid_dir_count or invalid_yaml_file_count: - sections.append( + read_dir_subsections.append( ToolkitPanelSection( description=f"{HINT_LEAD_TEXT} Use --verbose to see details about skipped and invalid directories and files." ) ) + read_dir_section = ToolkitPanelSection( + title=f"Build directory ({build_dir.path.as_posix()})", + content=read_dir_subsections, + ) + try: + plan = self.create_deployment_plan(build_dir) + except Exception as e: + console.print( + ToolkitPanel( + Group( + startup_section, + read_dir_section, + ToolkitPanelSection( + description=f"[bold]Failed to create plan for {operation}:[/] {escape(str(e))}" + ), + ), + title=operation, + border_style=AuraColor.AMBER.rich, + ) + ) + for warning in read_dir_warnings: + self.warn(warning, console=console) + raise + + if not plan: + plan_section = ToolkitPanelSection(description=f"[bold]No resources to {operation}.[/]") + else: + step_count = len(plan) + total_files = sum(len(step.files) for step in plan) + plan_section = ToolkitPanelSection( + title=f"{operation.title()} plan", + content=[ + f"[green]✓[/] [bold]{step_count}[/] resource types to {operation}", + f"[green]✓[/] [bold]{total_files}[/] resource files to {operation}", + ], + ) + + border_style = AuraColor.AMBER.rich if (has_issues or not plan) else AuraColor.GREEN.rich console.print( ToolkitPanel( - Group(*sections), - title=f"[bold]Build directory ({build_dir.path.as_posix()})[/]", - border_style=AuraColor.AMBER.rich if has_issues else AuraColor.GREEN.rich, + Group(startup_section, read_dir_section, plan_section), + title="[bold]Setting up deploy[/]", + border_style=border_style, ) ) - - if warnings: - for warning in warnings: - self.warn(warning, console=console) + for warning in read_dir_warnings: + self.warn(warning, console=console) + return plan def _validate_cdf_project( self, @@ -471,34 +503,6 @@ def create_deployment_plan(cls, read_dir: ReadBuildDirectory) -> list[Deployment ) return plan - @classmethod - def _display_plan(cls, plan: list[DeploymentStep], operation: str, operation_noun: str, console: Console) -> None: - if not plan: - console.print( - ToolkitPanel( - f"[bold yellow]No resources to {operation}.[/]", - title=f"[bold]{operation_noun.title()} plan[/]", - border_style=AuraColor.AMBER.rich, - ) - ) - return - - step_count = len(plan) - total_files = sum(len(step.files) for step in plan) - - console.print( - ToolkitPanel( - ToolkitPanelSection( - content=[ - f"[green]✓[/] [bold]{step_count}[/] resource types to {operation}", - f"[green]✓[/] [bold]{total_files}[/] resource files to {operation}", - ] - ), - title=f"[bold]{operation_noun.title()} plan[/]", - border_style=AuraColor.GREEN.rich, - ) - ) - @classmethod def apply_plan( cls, client: ToolkitClient, plan: list[DeploymentStep], options: DeployOptions, is_delete: bool = False @@ -518,7 +522,7 @@ def apply_plan( console = client.console with Progress(console=console) as progress: total_files = sum(len(step.files) for step in plan) - task_id = progress.add_task(f"Starting {options.operation_noun}", total=total_files) + task_id = progress.add_task(f"Starting {options.operation}", total=total_files) for step in plan: crud = step.crud_cls.create_loader(client) resource_name = crud.display_name @@ -895,19 +899,19 @@ def _merge_clean_results( @classmethod def _display_results( - cls, results: Sequence[DeploymentResult], operation: str, operation_noun: str, console: Console, verbose: bool + cls, results: Sequence[DeploymentResult], operation: str, console: Console, verbose: bool ) -> None: if not results: console.print( ToolkitPanel( f"No resources were {operation}ed.", - title=f"[bold]{operation_noun.title()} summary[/]", + title=f"[bold]{operation.title()} summary[/]", ) ) return is_dry_run = results[0].is_dry_run - panel_title = f"[bold]{operation_noun.title()} summary[/]" + panel_title = f"[bold]{operation.title()} summary[/]" if is_dry_run: panel_title += " [dim](dry run)[/]" @@ -974,7 +978,7 @@ def _display_results( sections.append( ToolkitPanelSection( description=( - f"{HINT_LEAD_TEXT}A total of {total.skipped_count} resources were skipped during {operation_noun}. " + f"{HINT_LEAD_TEXT}A total of {total.skipped_count} resources were skipped during {operation}. " f"The most common reasons were: {', '.join(f'{code} ({count} occurrences)' for code, count in most_common)}. " f"Use --verbose to see all skipped resources." ) From eb132521aa83d412d7fddc82b878960fc59e8291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 21:28:16 +0200 Subject: [PATCH 04/17] [CDF-27845] Make ToolkitPanel title always bold via title_style Auto-wrap plain string titles in a bold Text object inside ToolkitPanel.__init__ so callers don't need [bold]...[/] markup. Strip those wrappers from all existing call sites in build_v2, deploy, and deploy_v2. Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/commands/build_v2/build_v2.py | 8 +++----- cognite_toolkit/_cdf_tk/commands/deploy.py | 8 +++----- cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py | 10 +++++----- cognite_toolkit/_cdf_tk/ui.py | 4 ++++ 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/build_v2/build_v2.py b/cognite_toolkit/_cdf_tk/commands/build_v2/build_v2.py index acebd4d72a..ce9319d7c2 100644 --- a/cognite_toolkit/_cdf_tk/commands/build_v2/build_v2.py +++ b/cognite_toolkit/_cdf_tk/commands/build_v2/build_v2.py @@ -371,9 +371,7 @@ def _display_module_sources( summary_sections.append(ToolkitPanelSection(title="Issue details", content=issue_details_section_content)) border_style = {0: AuraColor.GREEN.rich, 1: AuraColor.AMBER.rich, 2: AuraColor.RED.rich}[border_color] - console.print( - ToolkitPanel(Group(*summary_sections), title="[bold]Loading modules[/]", border_style=border_style) - ) + console.print(ToolkitPanel(Group(*summary_sections), title="Loading modules", border_style=border_style)) if errors: console.print("\n") @@ -797,7 +795,7 @@ def _display_validation_plan(self, plan: list[ValidationStep], console: Console) console.print( ToolkitPanel( Group(*validation_sections), - title="[bold]Planning validation[/]", + title="Planning validation", border_style=border_style, ) ) @@ -971,7 +969,7 @@ def _display_build_summary( console.print( ToolkitPanel( "\n".join(summary_lines), - title=f"[bold]Built to directory {build_dir_display}[/]", + title=f"Built to directory {build_dir_display}", border_style=border_color, ) ) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy.py b/cognite_toolkit/_cdf_tk/commands/deploy.py index 5bad606f95..c5a7e4d13f 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy.py @@ -172,7 +172,7 @@ def _start_message(build_dir: Path, dry_run: bool, env_vars: EnvironmentVariable content = f"[bold]{verb}[/] resource files from {build_dir} directory." if not _RUNNING_IN_BROWSER: content += f"\n\nConnected to {env_vars.as_string()}" - print(ToolkitPanel(content, title="[bold]Deploy[/]")) + print(ToolkitPanel(content, title="Deploy")) def clean_all_resources( self, @@ -192,9 +192,7 @@ def clean_all_resources( return None flags = "--drop and --drop-data" if (drop and drop_data) else ("--drop" if drop else "--drop-data") print( - ToolkitPanel( - f"Cleaning resources as {flags} is passed", title="[bold]Clean[/]", border_style=AuraColor.AMBER.rich - ) + ToolkitPanel(f"Cleaning resources as {flags} is passed", title="Clean", border_style=AuraColor.AMBER.rich) ) for loader_cls in reversed(ordered_loaders): @@ -284,7 +282,7 @@ def deploy_all_resources( """ if verbose: - print(ToolkitPanel("[bold]Deploying resources...[/]", title="[bold]Deploy[/]")) + print(ToolkitPanel("[bold]Deploying resources...[/]", title="Deploy")) if ordered_loaders is None: selected_loaders = self._clean_command.get_selected_loaders(build_dir, set(), None) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index 167fe9a8f9..9c12f35475 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -427,7 +427,7 @@ def _display_setup( console.print( ToolkitPanel( Group(startup_section, read_dir_section, plan_section), - title="[bold]Setting up deploy[/]", + title=f"Setting up {operation}", border_style=border_style, ) ) @@ -723,7 +723,7 @@ def _categorize_resources( console.print( ToolkitPanel( diff_str, - title=f"[bold]{crud.display_name}:[/] {identifier}", + title=f"{crud.display_name}: {identifier}", ) ) return resources @@ -905,13 +905,13 @@ def _display_results( console.print( ToolkitPanel( f"No resources were {operation}ed.", - title=f"[bold]{operation.title()} summary[/]", + title=f"{operation.title()} summary", ) ) return is_dry_run = results[0].is_dry_run - panel_title = f"[bold]{operation.title()} summary[/]" + panel_title = f"{operation.title()} summary" if is_dry_run: panel_title += " [dim](dry run)[/]" @@ -1086,7 +1086,7 @@ def _display_deprecation_warning(cls, raw_files: Mapping[RawTableSelector, list[ ) ), ), - title="[bold]Deprecation warning[/]", + title="Deprecation warning", border_style=AuraColor.AMBER.rich, ) ) diff --git a/cognite_toolkit/_cdf_tk/ui.py b/cognite_toolkit/_cdf_tk/ui.py index 282d71edd9..1b49374529 100644 --- a/cognite_toolkit/_cdf_tk/ui.py +++ b/cognite_toolkit/_cdf_tk/ui.py @@ -41,14 +41,18 @@ def __init__( renderable: RenderableType, box: rich_box.Box = rich_box.ROUNDED, *, + title: str | Text | None = None, title_align: Literal["left", "center", "right"] = "left", subtitle_align: Literal["left", "center", "right"] = "left", padding: int | tuple[int] | tuple[int, int] | tuple[int, int, int, int] = (1, 2), **kwargs: Any, ) -> None: + if isinstance(title, str): + title = Text(title, style="bold") super().__init__( renderable, box, + title=title, title_align=title_align, subtitle_align=subtitle_align, padding=padding, From 1b57b9d6d35808aa373cec554e8b1206c7391d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 21:50:59 +0200 Subject: [PATCH 05/17] [CDF-27845] Add vertical space between startup and build-dir sections Wrap startup_section in Padding((0,0,1,0)) so there is a blank line before the 'Processed build directory' section, matching the spacing already present between that section and the Plan section. Co-Authored-By: Claude Sonnet 4.6 --- .../_cdf_tk/commands/deploy_v2/command.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index 9c12f35475..24881c385c 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -12,6 +12,7 @@ from pydantic import ValidationError from rich.console import Console, Group, RenderableType from rich.markup import escape +from rich.padding import Padding from rich.progress import Progress from yaml import YAMLError @@ -324,9 +325,9 @@ def _display_setup( ) -> list[DeploymentStep]: startup_section = ToolkitPanelSection( content=[ - f"Build path: {build_dir.path.as_posix()}", - f"CDF project: {cdf_project!r}", + f"Target project: {cdf_project!r}", f"Toolkit version: {__version__!s}", + f"Build path: {build_dir.path.as_posix()}/", ], ) @@ -386,7 +387,7 @@ def _display_setup( ) ) read_dir_section = ToolkitPanelSection( - title=f"Build directory ({build_dir.path.as_posix()})", + title="Processed build directory", content=read_dir_subsections, ) @@ -396,7 +397,7 @@ def _display_setup( console.print( ToolkitPanel( Group( - startup_section, + Padding(startup_section, (0, 0, 1, 0)), read_dir_section, ToolkitPanelSection( description=f"[bold]Failed to create plan for {operation}:[/] {escape(str(e))}" @@ -416,7 +417,7 @@ def _display_setup( step_count = len(plan) total_files = sum(len(step.files) for step in plan) plan_section = ToolkitPanelSection( - title=f"{operation.title()} plan", + title="Plan", content=[ f"[green]✓[/] [bold]{step_count}[/] resource types to {operation}", f"[green]✓[/] [bold]{total_files}[/] resource files to {operation}", @@ -426,8 +427,8 @@ def _display_setup( border_style = AuraColor.AMBER.rich if (has_issues or not plan) else AuraColor.GREEN.rich console.print( ToolkitPanel( - Group(startup_section, read_dir_section, plan_section), - title=f"Setting up {operation}", + Group(Padding(startup_section, (0, 0, 1, 0)), read_dir_section, plan_section), + title=f"Setting up {operation} operation", border_style=border_style, ) ) From a9b1d490f716177b0bcedaa224de3a49b27c4a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 21:59:48 +0200 Subject: [PATCH 06/17] [CDF-27845] Replace detail tables with hanging_indent nested lists Use the same pattern as build error output: hanging_indent with icon and marker_style instead of ToolkitTable for skipped directories, invalid directories, invalid YAML files, and skipped resources. Co-Authored-By: Claude Sonnet 4.6 --- .../_cdf_tk/commands/deploy_v2/command.py | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index 24881c385c..bbeef874f5 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -51,7 +51,7 @@ catch_warnings, ) from cognite_toolkit._cdf_tk.tracker import Tracker -from cognite_toolkit._cdf_tk.ui import AuraColor, ToolkitPanel, ToolkitPanelSection, ToolkitTable +from cognite_toolkit._cdf_tk.ui import AuraColor, ToolkitPanel, ToolkitPanelSection, ToolkitTable, hanging_indent from cognite_toolkit._cdf_tk.utils import humanize_collection, sanitize_filename, to_diff from cognite_toolkit._cdf_tk.utils.auth import EnvironmentVariables from cognite_toolkit._version import __version__ @@ -357,28 +357,35 @@ def _display_setup( read_dir_subsections: list[RenderableType] = [ToolkitPanelSection(content=read_dir_summary)] if verbose: if build_dir.skipped_directories: - t = ToolkitTable("Directory") - for dir_ in build_dir.skipped_directories: - t.add_row(dir_.directory.as_posix()) read_dir_subsections.append( - ToolkitPanelSection(title="Skipped directories", content=[t.as_panel_detail()]) + ToolkitPanelSection( + title="Skipped directories", + content=[ + hanging_indent("○", dir_.directory.as_posix(), marker_style="dim") + for dir_ in build_dir.skipped_directories + ], + ) ) if build_dir.invalid_directories: - t = ToolkitTable("Directory") - t.columns[0].style = "red" - for inv_dir in build_dir.invalid_directories: - t.add_row(inv_dir.as_posix()) read_dir_subsections.append( - ToolkitPanelSection(title="Invalid directories", content=[t.as_panel_detail()]) + ToolkitPanelSection( + title="Invalid directories", + content=[ + hanging_indent("✗", inv_dir.as_posix(), marker_style=AuraColor.RED.rich) + for inv_dir in build_dir.invalid_directories + ], + ) ) if invalid_yaml_file_count: - t = ToolkitTable("File") - t.columns[0].style = "red" - for dir_ in build_dir.resource_directories: - for file in dir_.invalid_files: - t.add_row(file.as_posix()) read_dir_subsections.append( - ToolkitPanelSection(title="Invalid YAML files", content=[t.as_panel_detail()]) + ToolkitPanelSection( + title="Invalid YAML files", + content=[ + hanging_indent("✗", file.as_posix(), marker_style=AuraColor.RED.rich) + for dir_ in build_dir.resource_directories + for file in dir_.invalid_files + ], + ) ) elif skipped_dir_count or invalid_dir_count or invalid_yaml_file_count: read_dir_subsections.append( @@ -986,10 +993,19 @@ def _display_results( ) ) elif verbose and total.skipped: - skipped_table = ToolkitTable("Resource", "File", "Code", "Reason") - for skip in total.skipped: - skipped_table.add_row(str(skip.id), skip.source_file.as_posix(), skip.code, skip.reason) - sections.append(ToolkitPanelSection(title="Skipped resources", content=[skipped_table.as_panel_detail()])) + sections.append( + ToolkitPanelSection( + title="Skipped resources", + content=[ + hanging_indent( + "○", + f"[bold]{skip.id}[/] {skip.source_file.as_posix()} [{skip.code}] {skip.reason}", + marker_style="dim", + ) + for skip in total.skipped + ], + ) + ) console.print(ToolkitPanel(Group(*sections), title=panel_title)) From 0a15332dca7084a17c4ba2d8ef20a852f17721d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:08:49 +0200 Subject: [PATCH 07/17] [CDF-27845] Drop redundant warn() calls after setup panel The panel already shows warning count and amber border when issues are detected. Printing them again via self.warn() after the panel prints them twice. Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index bbeef874f5..8cad76a5e6 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -414,8 +414,6 @@ def _display_setup( border_style=AuraColor.AMBER.rich, ) ) - for warning in read_dir_warnings: - self.warn(warning, console=console) raise if not plan: @@ -439,8 +437,6 @@ def _display_setup( border_style=border_style, ) ) - for warning in read_dir_warnings: - self.warn(warning, console=console) return plan def _validate_cdf_project( From 0548b09b107e9a2b0f4f11ef138eb6a47fe62a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:12:31 +0200 Subject: [PATCH 08/17] [CDF-27845] Add consequence to verbose detail section titles Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index 8cad76a5e6..ccf96a3795 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -359,7 +359,7 @@ def _display_setup( if build_dir.skipped_directories: read_dir_subsections.append( ToolkitPanelSection( - title="Skipped directories", + title="Skipped directories (excluded by --include)", content=[ hanging_indent("○", dir_.directory.as_posix(), marker_style="dim") for dir_ in build_dir.skipped_directories @@ -369,7 +369,7 @@ def _display_setup( if build_dir.invalid_directories: read_dir_subsections.append( ToolkitPanelSection( - title="Invalid directories", + title="Invalid directories (will be skipped)", content=[ hanging_indent("✗", inv_dir.as_posix(), marker_style=AuraColor.RED.rich) for inv_dir in build_dir.invalid_directories @@ -379,7 +379,7 @@ def _display_setup( if invalid_yaml_file_count: read_dir_subsections.append( ToolkitPanelSection( - title="Invalid YAML files", + title="Invalid YAML files (will be skipped)", content=[ hanging_indent("✗", file.as_posix(), marker_style=AuraColor.RED.rich) for dir_ in build_dir.resource_directories From b1eeccc64dd4d10ff4bf29cedd1dbb34b73ab194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:14:25 +0200 Subject: [PATCH 09/17] [CDF-27845] Show 'Finished dry-run' in progress bar for dry runs Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index ccf96a3795..7fdf466de9 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -576,7 +576,8 @@ def apply_plan( results.append(result) progress.update(task_id, advance=len(step.files)) - progress.update(task_id, description=f"Finished {options.operation}ing.") + finished = "Finished dry-run." if options.dry_run else f"Finished {options.operation}ing." + progress.update(task_id, description=finished) return results @classmethod From fc38f259bc5d9024e7876e117381783a36d50580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:16:27 +0200 Subject: [PATCH 10/17] [CDF-27845] Use Text.from_markup so title markup renders correctly Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cognite_toolkit/_cdf_tk/ui.py b/cognite_toolkit/_cdf_tk/ui.py index 1b49374529..38fc927878 100644 --- a/cognite_toolkit/_cdf_tk/ui.py +++ b/cognite_toolkit/_cdf_tk/ui.py @@ -48,7 +48,7 @@ def __init__( **kwargs: Any, ) -> None: if isinstance(title, str): - title = Text(title, style="bold") + title = Text.from_markup(title, style="bold") super().__init__( renderable, box, From 5a3ac76e1500d7932fdb39b93926bdb4ece7f074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:17:16 +0200 Subject: [PATCH 11/17] [CDF-27845] Rename 'Can deploy' column to 'Able to deploy' Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index 7fdf466de9..def8a76ba0 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -934,7 +934,7 @@ def _display_results( table.add_column("Skipped", justify="right", style="yellow") table.add_column("Total", justify="right", style="cyan") if is_dry_run: - table.add_column(f"Can {operation}", justify="right") + table.add_column("Able to deploy", justify="right") total = DeploymentResult( "All", From 7666b06c3edb0e16961964d043fc7d2471dfc0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:29:14 +0200 Subject: [PATCH 12/17] [CDF-27845] Replace unified diff with side-by-side diff_table component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add diff_table(old_lines, new_lines) to ui.py: a two-column Rich table showing CDF (left) vs Local (right), with deleted lines in red, inserted in green, equal lines dimmed and collapsed to '…' for large unchanged blocks (context=2). Use it in _categorize_resources verbose output. Co-Authored-By: Claude Sonnet 4.6 --- .../_cdf_tk/commands/deploy_v2/command.py | 20 +++++-- cognite_toolkit/_cdf_tk/ui.py | 60 ++++++++++++++++++- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index def8a76ba0..d197b983a4 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -51,9 +51,17 @@ catch_warnings, ) from cognite_toolkit._cdf_tk.tracker import Tracker -from cognite_toolkit._cdf_tk.ui import AuraColor, ToolkitPanel, ToolkitPanelSection, ToolkitTable, hanging_indent -from cognite_toolkit._cdf_tk.utils import humanize_collection, sanitize_filename, to_diff +from cognite_toolkit._cdf_tk.ui import ( + AuraColor, + ToolkitPanel, + ToolkitPanelSection, + ToolkitTable, + diff_table, + hanging_indent, +) +from cognite_toolkit._cdf_tk.utils import humanize_collection, sanitize_filename from cognite_toolkit._cdf_tk.utils.auth import EnvironmentVariables +from cognite_toolkit._cdf_tk.utils.file import yaml_safe_dump from cognite_toolkit._version import __version__ Operation: TypeAlias = Literal["deploy", "clean"] @@ -722,12 +730,14 @@ def _categorize_resources( resources.to_delete.append(identifier) resources.to_create.append(resource.request) if options.verbose: - diff_str = "\n".join(to_diff(cdf_dict, resource.raw_dict)) + old_lines = yaml_safe_dump(cdf_dict, sort_keys=True).splitlines() + new_lines = yaml_safe_dump(resource.raw_dict, sort_keys=True).splitlines() for sensitive in crud.sensitive_strings(resource.request): - diff_str = diff_str.replace(sensitive, "********") + old_lines = [line.replace(sensitive, "********") for line in old_lines] + new_lines = [line.replace(sensitive, "********") for line in new_lines] console.print( ToolkitPanel( - diff_str, + diff_table(old_lines, new_lines), title=f"{crud.display_name}: {identifier}", ) ) diff --git a/cognite_toolkit/_cdf_tk/ui.py b/cognite_toolkit/_cdf_tk/ui.py index 38fc927878..77015766db 100644 --- a/cognite_toolkit/_cdf_tk/ui.py +++ b/cognite_toolkit/_cdf_tk/ui.py @@ -1,5 +1,7 @@ from collections.abc import Sequence +from difflib import SequenceMatcher from enum import Enum +from itertools import zip_longest from typing import Any, ClassVar, Literal import questionary @@ -11,7 +13,15 @@ from rich.table import Table from rich.text import Text -__all__ = ["QUESTIONARY_STYLE", "AuraColor", "ToolkitPanel", "ToolkitPanelSection", "ToolkitTable", "hanging_indent"] +__all__ = [ + "QUESTIONARY_STYLE", + "AuraColor", + "ToolkitPanel", + "ToolkitPanelSection", + "ToolkitTable", + "diff_table", + "hanging_indent", +] # https://cognitedata.github.io/aura/primitives/colors @@ -124,6 +134,54 @@ def as_panel_detail(self) -> RenderableType: return Padding(self, (1, 0, 1, 2)) +def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> RenderableType: + """Side-by-side diff table comparing old (left) and new (right) lines. + + Equal regions larger than 2*context+1 lines are collapsed to a '…' separator. + Deleted lines are shown in red on the left, inserted lines in green on the right. + """ + matcher = SequenceMatcher(None, old_lines, new_lines, autojunk=False) + + table = Table( + box=rich_box.SIMPLE, + show_edge=False, + padding=(0, 1), + expand=True, + ) + table.add_column("CDF", overflow="fold", ratio=1) + table.add_column("Local", overflow="fold", ratio=1) + + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == "equal": + block = old_lines[i1:i2] + if len(block) <= 2 * context + 1: + for line in block: + table.add_row(f"[dim]{line}[/]", f"[dim]{line}[/]") + else: + for line in block[:context]: + table.add_row(f"[dim]{line}[/]", f"[dim]{line}[/]") + table.add_row( + f"[{AuraColor.MOUNTAIN.rich}]…[/]", + f"[{AuraColor.MOUNTAIN.rich}]…[/]", + ) + for line in block[-context:]: + table.add_row(f"[dim]{line}[/]", f"[dim]{line}[/]") + elif tag == "delete": + for line in old_lines[i1:i2]: + table.add_row(f"[{AuraColor.RED.rich}]{line}[/]", "") + elif tag == "insert": + for line in new_lines[j1:j2]: + table.add_row("", f"[{AuraColor.GREEN.rich}]{line}[/]") + elif tag == "replace": + for old, new in zip_longest(old_lines[i1:i2], new_lines[j1:j2], fillvalue=""): + table.add_row( + f"[{AuraColor.RED.rich}]{old}[/]" if old else "", + f"[{AuraColor.GREEN.rich}]{new}[/]" if new else "", + ) + + return table + + QUESTIONARY_STYLE = questionary.Style( [ ("qmark", AuraColor.FJORD.fg), # token in front of the question From e5bbbdd0a1ee5f90375f05b0086bb6b5d2dc810e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:32:26 +0200 Subject: [PATCH 13/17] [CDF-27845] Fix diff_table: disable highlight, label columns with colors Set highlight=False to stop Rich converting file paths to hyperlinks. Color the column headers red/green to make the CDF vs Local distinction immediately obvious. Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/ui.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/ui.py b/cognite_toolkit/_cdf_tk/ui.py index 77015766db..ffa503715f 100644 --- a/cognite_toolkit/_cdf_tk/ui.py +++ b/cognite_toolkit/_cdf_tk/ui.py @@ -147,9 +147,11 @@ def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> show_edge=False, padding=(0, 1), expand=True, + highlight=False, + show_header=True, ) - table.add_column("CDF", overflow="fold", ratio=1) - table.add_column("Local", overflow="fold", ratio=1) + table.add_column(f"[{AuraColor.RED.rich}]CDF (current)[/]", overflow="fold", ratio=1, no_wrap=False) + table.add_column(f"[{AuraColor.GREEN.rich}]Local (desired)[/]", overflow="fold", ratio=1, no_wrap=False) for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == "equal": From 8dab222c832d232f5a7fb88702b8fbd1884ba19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:39:29 +0200 Subject: [PATCH 14/17] [CDF-27845] Fix replace blocks in diff_table: stack deletions then insertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairing unrelated lines side-by-side with zip_longest was misleading — e.g. 'status: Failed' appeared next to 'functionPath: handler.py'. Show all old lines as a delete block first, then all new lines as an insert block, so each line gets its own row. Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/ui.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/ui.py b/cognite_toolkit/_cdf_tk/ui.py index ffa503715f..c657f98734 100644 --- a/cognite_toolkit/_cdf_tk/ui.py +++ b/cognite_toolkit/_cdf_tk/ui.py @@ -1,7 +1,6 @@ from collections.abc import Sequence from difflib import SequenceMatcher from enum import Enum -from itertools import zip_longest from typing import Any, ClassVar, Literal import questionary @@ -146,12 +145,12 @@ def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> box=rich_box.SIMPLE, show_edge=False, padding=(0, 1), - expand=True, + expand=False, highlight=False, show_header=True, ) table.add_column(f"[{AuraColor.RED.rich}]CDF (current)[/]", overflow="fold", ratio=1, no_wrap=False) - table.add_column(f"[{AuraColor.GREEN.rich}]Local (desired)[/]", overflow="fold", ratio=1, no_wrap=False) + table.add_column("[Local (desired)", overflow="fold", ratio=1, no_wrap=False) for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == "equal": @@ -175,11 +174,10 @@ def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> for line in new_lines[j1:j2]: table.add_row("", f"[{AuraColor.GREEN.rich}]{line}[/]") elif tag == "replace": - for old, new in zip_longest(old_lines[i1:i2], new_lines[j1:j2], fillvalue=""): - table.add_row( - f"[{AuraColor.RED.rich}]{old}[/]" if old else "", - f"[{AuraColor.GREEN.rich}]{new}[/]" if new else "", - ) + for line in old_lines[i1:i2]: + table.add_row(f"[{AuraColor.RED.rich}]{line}[/]", "") + for line in new_lines[j1:j2]: + table.add_row("", f"[{AuraColor.GREEN.rich}]{line}[/]") return table From ca7c81bf2e200639bbb87a5d8205d7976cce599c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Thu, 30 Apr 2026 22:41:58 +0200 Subject: [PATCH 15/17] [CDF-27845] Put local config left in diff_table, preserve key order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap columns so Local (desired) is on the left and CDF (current) on the right, matching the mental model of "what I want → what's there". Remove sort_keys=True so YAML keys appear in their natural order. Co-Authored-By: Claude Sonnet 4.6 --- .../_cdf_tk/commands/deploy_v2/command.py | 4 ++-- cognite_toolkit/_cdf_tk/ui.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py index d197b983a4..ad35195785 100644 --- a/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py +++ b/cognite_toolkit/_cdf_tk/commands/deploy_v2/command.py @@ -730,8 +730,8 @@ def _categorize_resources( resources.to_delete.append(identifier) resources.to_create.append(resource.request) if options.verbose: - old_lines = yaml_safe_dump(cdf_dict, sort_keys=True).splitlines() - new_lines = yaml_safe_dump(resource.raw_dict, sort_keys=True).splitlines() + old_lines = yaml_safe_dump(cdf_dict).splitlines() + new_lines = yaml_safe_dump(resource.raw_dict).splitlines() for sensitive in crud.sensitive_strings(resource.request): old_lines = [line.replace(sensitive, "********") for line in old_lines] new_lines = [line.replace(sensitive, "********") for line in new_lines] diff --git a/cognite_toolkit/_cdf_tk/ui.py b/cognite_toolkit/_cdf_tk/ui.py index c657f98734..e3b3e65922 100644 --- a/cognite_toolkit/_cdf_tk/ui.py +++ b/cognite_toolkit/_cdf_tk/ui.py @@ -149,8 +149,8 @@ def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> highlight=False, show_header=True, ) + table.add_column(f"[{AuraColor.GREEN.rich}]Local (desired)[/]", overflow="fold", ratio=1, no_wrap=False) table.add_column(f"[{AuraColor.RED.rich}]CDF (current)[/]", overflow="fold", ratio=1, no_wrap=False) - table.add_column("[Local (desired)", overflow="fold", ratio=1, no_wrap=False) for tag, i1, i2, j1, j2 in matcher.get_opcodes(): if tag == "equal": @@ -169,15 +169,15 @@ def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> table.add_row(f"[dim]{line}[/]", f"[dim]{line}[/]") elif tag == "delete": for line in old_lines[i1:i2]: - table.add_row(f"[{AuraColor.RED.rich}]{line}[/]", "") + table.add_row("", f"[{AuraColor.RED.rich}]{line}[/]") elif tag == "insert": for line in new_lines[j1:j2]: - table.add_row("", f"[{AuraColor.GREEN.rich}]{line}[/]") + table.add_row(f"[{AuraColor.GREEN.rich}]{line}[/]", "") elif tag == "replace": - for line in old_lines[i1:i2]: - table.add_row(f"[{AuraColor.RED.rich}]{line}[/]", "") for line in new_lines[j1:j2]: - table.add_row("", f"[{AuraColor.GREEN.rich}]{line}[/]") + table.add_row(f"[{AuraColor.GREEN.rich}]{line}[/]", "") + for line in old_lines[i1:i2]: + table.add_row("", f"[{AuraColor.RED.rich}]{line}[/]") return table From c3cae041ef107a81e7cdcf6557f1cc725007b6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Fri, 1 May 2026 08:15:05 +0200 Subject: [PATCH 16/17] [CDF-27845] Match replace lines by YAML key in diff_table Extract the key from each YAML line in a replace block and pair lines with the same key side-by-side (local left, CDF right). Lines with no matching key on the other side still get their own row. Adds visual unit tests runnable with pytest -s. Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/ui.py | 27 +++++++-- .../test_deployv2/test_diff_table.py | 57 +++++++++++++++++++ 2 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 tests/test_unit/test_cdf_tk/test_commands/test_deployv2/test_diff_table.py diff --git a/cognite_toolkit/_cdf_tk/ui.py b/cognite_toolkit/_cdf_tk/ui.py index e3b3e65922..a1b2a2b16d 100644 --- a/cognite_toolkit/_cdf_tk/ui.py +++ b/cognite_toolkit/_cdf_tk/ui.py @@ -133,6 +133,14 @@ def as_panel_detail(self) -> RenderableType: return Padding(self, (1, 0, 1, 2)) +def _yaml_key(line: str) -> str | None: + stripped = line.lstrip() + if ":" in stripped: + key = stripped.split(":")[0].strip() + return key or None + return None + + def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> RenderableType: """Side-by-side diff table comparing old (left) and new (right) lines. @@ -174,10 +182,21 @@ def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> for line in new_lines[j1:j2]: table.add_row(f"[{AuraColor.GREEN.rich}]{line}[/]", "") elif tag == "replace": - for line in new_lines[j1:j2]: - table.add_row(f"[{AuraColor.GREEN.rich}]{line}[/]", "") - for line in old_lines[i1:i2]: - table.add_row("", f"[{AuraColor.RED.rich}]{line}[/]") + old_block = old_lines[i1:i2] + new_block = new_lines[j1:j2] + old_by_key = {_yaml_key(line): line for line in old_block if _yaml_key(line)} + seen_old_keys: set[str] = set() + for line in new_block: + key = _yaml_key(line) + if key and key in old_by_key: + seen_old_keys.add(key) + table.add_row(f"[{AuraColor.GREEN.rich}]{line}[/]", f"[{AuraColor.RED.rich}]{old_by_key[key]}[/]") + else: + table.add_row(f"[{AuraColor.GREEN.rich}]{line}[/]", "") + for line in old_block: + key = _yaml_key(line) + if key not in seen_old_keys: + table.add_row("", f"[{AuraColor.RED.rich}]{line}[/]") return table diff --git a/tests/test_unit/test_cdf_tk/test_commands/test_deployv2/test_diff_table.py b/tests/test_unit/test_cdf_tk/test_commands/test_deployv2/test_diff_table.py new file mode 100644 index 0000000000..ce9aa4839e --- /dev/null +++ b/tests/test_unit/test_cdf_tk/test_commands/test_deployv2/test_diff_table.py @@ -0,0 +1,57 @@ +"""Visual tests for diff_table — run with `pytest -s` to see rendered output.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml +from rich.console import Console + +from cognite_toolkit._cdf_tk.ui import ToolkitPanel, diff_table +from cognite_toolkit._cdf_tk.utils.file import yaml_safe_dump + +_FUNCTION_YAML = ( + Path(__file__).resolve().parents[5] / "cognite_toolkit/modules/operator/functions/toolkit.Function.yaml" +) + + +def _load() -> dict: + return yaml.safe_load(_FUNCTION_YAML.read_text()) + + +def _render(title: str, old: dict, new: dict, console: Console) -> None: + old_lines = yaml_safe_dump(old).splitlines() + new_lines = yaml_safe_dump(new).splitlines() + console.print(ToolkitPanel(diff_table(old_lines, new_lines), title=title)) + + +@pytest.fixture +def console() -> Console: + return Console(width=120) + + +class TestDiffTableVariations: + def test_field_value_changed(self, console: Console) -> None: + local = _load() + cdf = {**local, "runtime": "py311", "cpu": 1.0} + _render("Field value changed (runtime + cpu)", local, cdf, console) + + def test_field_added_locally(self, console: Console) -> None: + local = _load() + cdf = {k: v for k, v in local.items() if k not in {"envVars", "memory"}} + _render("Fields added locally (envVars + memory missing in CDF)", local, cdf, console) + + def test_field_removed_locally(self, console: Console) -> None: + local = {k: v for k, v in _load().items() if k != "description"} + cdf = _load() + _render("Field removed locally (description in CDF only)", local, cdf, console) + + def test_minimal_cdf_state(self, console: Console) -> None: + local = _load() + cdf = {"externalId": local["externalId"], "status": "Failed"} + _render("Minimal CDF state (failed function, full local definition)", local, cdf, console) + + def test_no_diff(self, console: Console) -> None: + resource = _load() + _render("No diff (identical)", resource, resource, console) From f22ab015de30ff0c875cbca2ba327b752c1c8888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20R=C3=B8nning?= Date: Fri, 1 May 2026 08:24:18 +0200 Subject: [PATCH 17/17] [CDF-27845] Add intra-line character-level highlighting to diff_table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _highlight_diff runs a second SequenceMatcher pass at character granularity on key-matched YAML line pairs, wrapping changed spans in [bold] so readers can spot the exact value difference (e.g. py311 → py312) without scanning the full line. Co-Authored-By: Claude Sonnet 4.6 --- cognite_toolkit/_cdf_tk/ui.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/cognite_toolkit/_cdf_tk/ui.py b/cognite_toolkit/_cdf_tk/ui.py index a1b2a2b16d..e298138f91 100644 --- a/cognite_toolkit/_cdf_tk/ui.py +++ b/cognite_toolkit/_cdf_tk/ui.py @@ -141,6 +141,25 @@ def _yaml_key(line: str) -> str | None: return None +def _highlight_diff(old_str: str, new_str: str) -> tuple[str, str]: + """Return (new_highlighted, old_highlighted) with changed character spans wrapped in [bold].""" + matcher = SequenceMatcher(None, old_str, new_str, autojunk=False) + old_parts: list[str] = [] + new_parts: list[str] = [] + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + if tag == "equal": + old_parts.append(old_str[i1:i2]) + new_parts.append(new_str[j1:j2]) + elif tag == "delete": + old_parts.append(f"[bold]{old_str[i1:i2]}[/bold]") + elif tag == "insert": + new_parts.append(f"[bold]{new_str[j1:j2]}[/bold]") + elif tag == "replace": + old_parts.append(f"[bold]{old_str[i1:i2]}[/bold]") + new_parts.append(f"[bold]{new_str[j1:j2]}[/bold]") + return "".join(new_parts), "".join(old_parts) + + def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> RenderableType: """Side-by-side diff table comparing old (left) and new (right) lines. @@ -190,7 +209,8 @@ def diff_table(old_lines: list[str], new_lines: list[str], context: int = 2) -> key = _yaml_key(line) if key and key in old_by_key: seen_old_keys.add(key) - table.add_row(f"[{AuraColor.GREEN.rich}]{line}[/]", f"[{AuraColor.RED.rich}]{old_by_key[key]}[/]") + local_hl, cdf_hl = _highlight_diff(old_by_key[key], line) + table.add_row(f"[{AuraColor.GREEN.rich}]{local_hl}[/]", f"[{AuraColor.RED.rich}]{cdf_hl}[/]") else: table.add_row(f"[{AuraColor.GREEN.rich}]{line}[/]", "") for line in old_block: