Skip to content
Merged
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
35 changes: 35 additions & 0 deletions src/keboola_agent_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,41 @@ def get_config_detail(self, component_id: str, config_id: str) -> dict[str, Any]
)
return response.json()

def list_config_folder_metadata(self, branch_id: int) -> dict[str, str]:
"""Fetch folder names for all configurations via metadata search.

Uses the search/component-configurations endpoint to find configs
with ``KBC.configuration.folderName`` metadata.

Note: This endpoint requires a branch ID (branch-only route).

Args:
branch_id: Branch ID (required — use default branch for production).

Returns:
Dict mapping ``"{component_id}/{config_id}"`` to folder name.
"""
prefix = f"/v2/storage/branch/{branch_id}"
resp = self._request(
"GET",
f"{prefix}/search/component-configurations",
params={
"metadataKeys[]": "KBC.configuration.folderName",
"include": "filteredMetadata",
},
)
folder_map: dict[str, str] = {}
for item in resp.json():
comp_id = item.get("idComponent", "")
config_id = str(item.get("configurationId", ""))
meta = next(
(m for m in item.get("metadata", []) if m["key"] == "KBC.configuration.folderName"),
None,
)
if meta:
folder_map[f"{comp_id}/{config_id}"] = meta["value"]
return folder_map

def create_config(
self,
component_id: str,
Expand Down
2 changes: 2 additions & 0 deletions src/keboola_agent_cli/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def format_configs_table(console: Console, data: dict[str, Any]) -> None:
table.add_column("Type", style="dim")
table.add_column("Config ID", justify="right")
table.add_column("Config Name")
table.add_column("Folder", style="dim")
table.add_column("Last Modified", style="dim")
table.add_column("Modified By", style="dim")

Expand All @@ -177,6 +178,7 @@ def format_configs_table(console: Console, data: dict[str, Any]) -> None:
cfg["component_type"],
cfg["config_id"],
cfg["config_name"],
cfg.get("folder", ""),
last_mod,
cfg.get("last_modified_by", ""),
)
Expand Down
22 changes: 21 additions & 1 deletion src/keboola_agent_cli/services/config_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ def _fetch_project_configs(
client = self._client_factory(project.stack_url, project.token)
try:
components = client.list_components(component_type=component_type)

# Fetch folder metadata (requires branch ID — search endpoint is branch-only)
folder_map: dict[str, str] = {}
try:
# Use active branch or find the default branch ID
branch_id = project.active_branch_id
if not branch_id:
# Fetch default branch ID from dev-branches endpoint
branches = client.list_dev_branches()
default = next((b for b in branches if b.get("isDefault")), None)
if default:
branch_id = default["id"]
if branch_id:
result = client.list_config_folder_metadata(branch_id=branch_id)
folder_map = result if isinstance(result, dict) else {}
except Exception:
pass # graceful fallback if search endpoint unavailable

configs: list[dict[str, Any]] = []
for component in components:
comp_id = component.get("id", "")
Expand All @@ -79,19 +97,21 @@ def _fetch_project_configs(
# Extract last-modified info from currentVersion
current_version = cfg.get("currentVersion", {})
creator_token = current_version.get("creatorToken", {})
cfg_id = str(cfg.get("id", ""))

configs.append(
{
"project_alias": alias,
"component_id": comp_id,
"component_name": comp_name,
"component_type": comp_type,
"config_id": str(cfg.get("id", "")),
"config_id": cfg_id,
"config_name": cfg.get("name", ""),
"config_description": cfg.get("description", ""),
"last_modified": current_version.get("created", ""),
"last_modified_by": creator_token.get("description", ""),
"last_change_description": current_version.get("changeDescription", ""),
"folder": folder_map.get(f"{comp_id}/{cfg_id}", ""),
}
)
return (alias, configs, True)
Expand Down
10 changes: 5 additions & 5 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,9 +727,9 @@ def test_config_list_human_output(self, tmp_path: Path) -> None:
# Should show project-grouped table
assert "prod" in result.output
assert "Configurations" in result.output
# Rich may truncate long names in narrow terminals, check for prefix
assert "Production" in result.output
assert "keboola.ex" in result.output
# Rich may truncate long names in narrow terminals
assert "Product" in result.output
assert "keboola" in result.output

def test_config_list_project_filter(self, tmp_path: Path) -> None:
"""config list --project X returns configs only from that project."""
Expand Down Expand Up @@ -1065,8 +1065,8 @@ def factory(url, token):
assert result.exit_code == 0
# Should show configs from good project
assert "Configurations" in result.output
# Rich may truncate long names in narrow terminals, check for prefix
assert "Production" in result.output
# Rich may truncate long names in narrow terminals
assert "Product" in result.output
# Should show warning about bad project
assert "bad" in result.output
assert "Token expired" in result.output
Expand Down
6 changes: 3 additions & 3 deletions tests/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,9 +891,9 @@ def test_configs_table_grouped_by_project(self) -> None:

assert "Configurations" in output
assert "prod" in output
# Rich may word-wrap long names across lines in narrow console
assert "Production" in output
assert "Write to" in output
# Rich may truncate long names in narrow console (7 columns)
assert "Product" in output
assert "Write" in output

def test_configs_table_empty(self) -> None:
"""format_configs_table shows helpful message when no configs found."""
Expand Down
Loading