@a9s/cli 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/seed.js +202 -4
- package/dist/src/App.js +35 -3
- package/dist/src/components/AdvancedTextInput.js +3 -1
- package/dist/src/components/AutocompleteInput.js +3 -1
- package/dist/src/components/DetailPanel.js +3 -1
- package/dist/src/components/DiffViewer.js +3 -1
- package/dist/src/components/ErrorStatePanel.js +3 -1
- package/dist/src/components/HUD.js +3 -1
- package/dist/src/components/HelpPanel.js +6 -4
- package/dist/src/components/ModeBar.js +5 -8
- package/dist/src/components/Table/index.js +19 -26
- package/dist/src/components/TableSkeleton.js +3 -1
- package/dist/src/components/YankHelpPanel.js +3 -1
- package/dist/src/constants/commands.js +2 -1
- package/dist/src/constants/theme.js +608 -0
- package/dist/src/contexts/ThemeContext.js +13 -0
- package/dist/src/features/AppMainView.integration.test.js +1 -0
- package/dist/src/features/AppMainView.js +6 -4
- package/dist/src/hooks/useCommandRouter.js +5 -0
- package/dist/src/hooks/usePickerManager.js +35 -1
- package/dist/src/index.js +2 -1
- package/dist/src/services.js +2 -2
- package/dist/src/state/atoms.js +3 -0
- package/dist/src/utils/config.js +36 -0
- package/dist/src/views/dynamodb/adapter.js +313 -9
- package/dist/src/views/dynamodb/capabilities/detailCapability.js +94 -0
- package/dist/src/views/dynamodb/capabilities/yankCapability.js +6 -0
- package/dist/src/views/dynamodb/capabilities/yankOptions.js +69 -0
- package/dist/src/views/dynamodb/schema.js +18 -0
- package/dist/src/views/dynamodb/types.js +1 -0
- package/dist/src/views/dynamodb/utils.js +175 -0
- package/dist/src/views/iam/adapter.js +2 -1
- package/dist/src/views/route53/adapter.js +166 -9
- package/dist/src/views/route53/capabilities/detailCapability.js +63 -0
- package/dist/src/views/route53/capabilities/yankCapability.js +6 -0
- package/dist/src/views/route53/capabilities/yankOptions.js +58 -0
- package/dist/src/views/route53/schema.js +18 -0
- package/dist/src/views/route53/types.js +1 -0
- package/dist/src/views/s3/adapter.js +2 -1
- package/dist/src/views/secretsmanager/adapter.js +2 -1
- package/package.json +2 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { runAwsJsonAsync } from "../../../utils/aws.js";
|
|
2
|
+
import { formatBillingMode, formatKeySchema } from "../utils.js";
|
|
3
|
+
export function createDynamoDBDetailCapability(region, getLevel) {
|
|
4
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
5
|
+
const getDetails = async (row) => {
|
|
6
|
+
const meta = row.meta;
|
|
7
|
+
if (!meta) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
const level = getLevel?.();
|
|
11
|
+
// Table level
|
|
12
|
+
if (level?.kind === "tables" && meta.type === "table") {
|
|
13
|
+
try {
|
|
14
|
+
const data = await runAwsJsonAsync([
|
|
15
|
+
"dynamodb",
|
|
16
|
+
"describe-table",
|
|
17
|
+
"--table-name",
|
|
18
|
+
meta.tableName,
|
|
19
|
+
...regionArgs,
|
|
20
|
+
]);
|
|
21
|
+
const table = data.Table;
|
|
22
|
+
const fields = [
|
|
23
|
+
{ label: "Table Name", value: table.TableName },
|
|
24
|
+
{ label: "Table ARN", value: table.TableArn },
|
|
25
|
+
{ label: "Status", value: table.TableStatus },
|
|
26
|
+
{ label: "Billing Mode", value: formatBillingMode(table) },
|
|
27
|
+
{ label: "Item Count", value: String(table.ItemCount ?? 0) },
|
|
28
|
+
{ label: "Table Size", value: `${table.TableSizeBytes ?? 0} bytes` },
|
|
29
|
+
{ label: "Key Schema", value: formatKeySchema(table) },
|
|
30
|
+
];
|
|
31
|
+
// Add attributes
|
|
32
|
+
if (table.AttributeDefinitions && table.AttributeDefinitions.length > 0) {
|
|
33
|
+
const attrs = table.AttributeDefinitions.map((a) => `${a.AttributeName} (${a.AttributeType})`).join(", ");
|
|
34
|
+
fields.push({ label: "Attributes", value: attrs });
|
|
35
|
+
}
|
|
36
|
+
fields.push({
|
|
37
|
+
label: "GSI Count",
|
|
38
|
+
value: String(table.GlobalSecondaryIndexes?.length ?? 0),
|
|
39
|
+
});
|
|
40
|
+
fields.push({
|
|
41
|
+
label: "LSI Count",
|
|
42
|
+
value: String(table.LocalSecondaryIndexes?.length ?? 0),
|
|
43
|
+
});
|
|
44
|
+
if (table.CreationDateTime) {
|
|
45
|
+
fields.push({
|
|
46
|
+
label: "Created",
|
|
47
|
+
value: new Date(table.CreationDateTime).toISOString(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return fields;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Item level
|
|
57
|
+
if (level?.kind === "items" && meta.type === "item") {
|
|
58
|
+
try {
|
|
59
|
+
const itemJson = meta.itemJson ? JSON.parse(meta.itemJson) : {};
|
|
60
|
+
const prettyJson = JSON.stringify(itemJson, null, 2);
|
|
61
|
+
const fields = [
|
|
62
|
+
{
|
|
63
|
+
label: "Item (JSON)",
|
|
64
|
+
value: prettyJson,
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
if (meta.itemSize) {
|
|
68
|
+
fields.push({
|
|
69
|
+
label: "Size",
|
|
70
|
+
value: `${meta.itemSize} bytes`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return fields;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Item-field level
|
|
80
|
+
if (level?.kind === "item-fields" && meta.type === "item-field") {
|
|
81
|
+
const fields = [
|
|
82
|
+
{ label: "Attribute", value: meta.fieldName ?? "-" },
|
|
83
|
+
{ label: "Type", value: meta.fieldType ?? "-" },
|
|
84
|
+
{
|
|
85
|
+
label: "Value",
|
|
86
|
+
value: String(meta.fieldRawValue ?? "-"),
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
return fields;
|
|
90
|
+
}
|
|
91
|
+
return [];
|
|
92
|
+
};
|
|
93
|
+
return { getDetails };
|
|
94
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createYankCapability } from "../../../adapters/capabilities/YankCapability.js";
|
|
2
|
+
import { DynamoDBRowMetaSchema } from "../schema.js";
|
|
3
|
+
import { DynamoDBYankOptions } from "./yankOptions.js";
|
|
4
|
+
export function createDynamoDBYankCapability(_region, _getLevel) {
|
|
5
|
+
return createYankCapability(DynamoDBYankOptions, DynamoDBRowMetaSchema, {});
|
|
6
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const DynamoDBYankOptions = [
|
|
2
|
+
// Table options
|
|
3
|
+
{
|
|
4
|
+
trigger: { type: "key", char: "n" },
|
|
5
|
+
label: "Copy table name",
|
|
6
|
+
feedback: "Copied table name",
|
|
7
|
+
isRelevant: (row) => row.meta.type === "table",
|
|
8
|
+
resolve: async (row) => {
|
|
9
|
+
return row.meta.tableName ?? null;
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
trigger: { type: "key", char: "a" },
|
|
14
|
+
label: "Copy table ARN",
|
|
15
|
+
feedback: "Copied table ARN",
|
|
16
|
+
isRelevant: (row) => row.meta.type === "table",
|
|
17
|
+
resolve: async (row) => {
|
|
18
|
+
return row.meta.tableArn ?? null;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
// Item options
|
|
22
|
+
{
|
|
23
|
+
trigger: { type: "key", char: "k" },
|
|
24
|
+
label: "Copy primary key (JSON)",
|
|
25
|
+
feedback: "Copied primary key",
|
|
26
|
+
isRelevant: (row) => row.meta.type === "item",
|
|
27
|
+
resolve: async (row) => {
|
|
28
|
+
if (!row.meta.itemPkValue && !row.meta.itemSkValue) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const obj = {};
|
|
32
|
+
if (row.meta.itemPkValue) {
|
|
33
|
+
obj.pk = row.meta.itemPkValue;
|
|
34
|
+
}
|
|
35
|
+
if (row.meta.itemSkValue) {
|
|
36
|
+
obj.sk = row.meta.itemSkValue;
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify(obj);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
trigger: { type: "key", char: "j" },
|
|
43
|
+
label: "Copy entire item (JSON)",
|
|
44
|
+
feedback: "Copied item",
|
|
45
|
+
isRelevant: (row) => row.meta.type === "item" && !!row.meta.itemJson,
|
|
46
|
+
resolve: async (row) => {
|
|
47
|
+
return row.meta.itemJson ?? null;
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
// Field options
|
|
51
|
+
{
|
|
52
|
+
trigger: { type: "key", char: "v" },
|
|
53
|
+
label: "Copy attribute value",
|
|
54
|
+
feedback: "Copied attribute value",
|
|
55
|
+
isRelevant: (row) => row.meta.type === "item-field",
|
|
56
|
+
resolve: async (row) => {
|
|
57
|
+
return row.meta.fieldValue ?? null;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
trigger: { type: "key", char: "k" },
|
|
62
|
+
label: "Copy attribute name",
|
|
63
|
+
feedback: "Copied attribute name",
|
|
64
|
+
isRelevant: (row) => row.meta.type === "item-field",
|
|
65
|
+
resolve: async (row) => {
|
|
66
|
+
return row.meta.fieldName ?? null;
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const DynamoDBRowMetaSchema = z.object({
|
|
3
|
+
type: z.enum(["table", "item", "item-field"]),
|
|
4
|
+
tableName: z.string().optional(),
|
|
5
|
+
tableStatus: z.string().optional(),
|
|
6
|
+
tableArn: z.string().optional(),
|
|
7
|
+
billing: z.string().optional(),
|
|
8
|
+
gsiCount: z.number().optional(),
|
|
9
|
+
itemIndex: z.number().optional(),
|
|
10
|
+
itemPkValue: z.string().optional(),
|
|
11
|
+
itemSkValue: z.string().optional(),
|
|
12
|
+
itemSize: z.number().optional(),
|
|
13
|
+
itemJson: z.string().optional(),
|
|
14
|
+
fieldName: z.string().optional(),
|
|
15
|
+
fieldValue: z.string().optional(),
|
|
16
|
+
fieldType: z.string().optional(),
|
|
17
|
+
fieldRawValue: z.unknown().optional(),
|
|
18
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unwrap a DynamoDB AttributeValue to its native JS value.
|
|
3
|
+
* E.g., { S: "hello" } → "hello", { N: "42" } → "42", { BOOL: true } → true
|
|
4
|
+
*/
|
|
5
|
+
export function unwrapDynamoValue(attr) {
|
|
6
|
+
const key = Object.keys(attr)[0];
|
|
7
|
+
const value = attr[key];
|
|
8
|
+
switch (key) {
|
|
9
|
+
case "S":
|
|
10
|
+
return value;
|
|
11
|
+
case "N":
|
|
12
|
+
return value;
|
|
13
|
+
case "B":
|
|
14
|
+
return value;
|
|
15
|
+
case "SS":
|
|
16
|
+
return value;
|
|
17
|
+
case "NS":
|
|
18
|
+
return value;
|
|
19
|
+
case "BS":
|
|
20
|
+
return value;
|
|
21
|
+
case "M": {
|
|
22
|
+
const mapVal = value;
|
|
23
|
+
const obj = {};
|
|
24
|
+
for (const [k, v] of Object.entries(mapVal)) {
|
|
25
|
+
obj[k] = unwrapDynamoValue(v);
|
|
26
|
+
}
|
|
27
|
+
return obj;
|
|
28
|
+
}
|
|
29
|
+
case "L": {
|
|
30
|
+
const listVal = value;
|
|
31
|
+
return listVal.map((v) => unwrapDynamoValue(v));
|
|
32
|
+
}
|
|
33
|
+
case "NULL":
|
|
34
|
+
return null;
|
|
35
|
+
case "BOOL":
|
|
36
|
+
return value;
|
|
37
|
+
default:
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get the DynamoDB type descriptor for an attribute value.
|
|
43
|
+
* Returns the single-letter type: S, N, B, SS, NS, BS, M, L, NULL, BOOL
|
|
44
|
+
*/
|
|
45
|
+
export function getDynamoType(attr) {
|
|
46
|
+
const key = Object.keys(attr)[0];
|
|
47
|
+
return key || "?";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Format a DynamoDB value for display.
|
|
51
|
+
* - For simple types (S, N, B, BOOL, NULL), return the value
|
|
52
|
+
* - For collections (SS, NS, BS), return comma-separated or wrapped in []
|
|
53
|
+
* - For maps/lists, return pretty JSON
|
|
54
|
+
* - For large values (>3 lines), truncate to 2 header + ... + 1 footer
|
|
55
|
+
* - For single/few line values, truncate at 50 chars with "..." suffix
|
|
56
|
+
*/
|
|
57
|
+
export function formatDynamoValue(attr) {
|
|
58
|
+
const type = getDynamoType(attr);
|
|
59
|
+
const raw = unwrapDynamoValue(attr);
|
|
60
|
+
const key = Object.keys(attr)[0];
|
|
61
|
+
const value = attr[key];
|
|
62
|
+
if (type === "NULL")
|
|
63
|
+
return "null";
|
|
64
|
+
if (type === "BOOL")
|
|
65
|
+
return String(value);
|
|
66
|
+
if (type === "N")
|
|
67
|
+
return String(value);
|
|
68
|
+
if (type === "S") {
|
|
69
|
+
const str = String(value);
|
|
70
|
+
// Escape newlines for display
|
|
71
|
+
const escaped = str.replace(/\n/g, "\\n");
|
|
72
|
+
if (escaped.length > 50)
|
|
73
|
+
return escaped.slice(0, 47) + "...";
|
|
74
|
+
return escaped;
|
|
75
|
+
}
|
|
76
|
+
if (type === "B")
|
|
77
|
+
return `<binary: ${value?.length ?? 0} bytes>`;
|
|
78
|
+
if (type === "SS") {
|
|
79
|
+
return (value ?? []).join(", ");
|
|
80
|
+
}
|
|
81
|
+
if (type === "NS") {
|
|
82
|
+
return (value ?? []).join(", ");
|
|
83
|
+
}
|
|
84
|
+
if (type === "BS") {
|
|
85
|
+
return `<${(value ?? []).length} binary values>`;
|
|
86
|
+
}
|
|
87
|
+
if (type === "M") {
|
|
88
|
+
const json = JSON.stringify(raw, null, 2);
|
|
89
|
+
return truncateLargeValue(json);
|
|
90
|
+
}
|
|
91
|
+
if (type === "L") {
|
|
92
|
+
const json = JSON.stringify(raw, null, 2);
|
|
93
|
+
return truncateLargeValue(json);
|
|
94
|
+
}
|
|
95
|
+
return String(raw);
|
|
96
|
+
}
|
|
97
|
+
function truncateLargeValue(str) {
|
|
98
|
+
const lines = str.split("\n");
|
|
99
|
+
if (lines.length > 3) {
|
|
100
|
+
return `${lines[0]}\n${lines[1]}\n...\n${lines[lines.length - 1]}`;
|
|
101
|
+
}
|
|
102
|
+
// Single or few lines
|
|
103
|
+
const compact = str.replace(/\n/g, " ");
|
|
104
|
+
if (compact.length > 50)
|
|
105
|
+
return compact.slice(0, 47) + "...";
|
|
106
|
+
return compact;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Format billing mode: "On-Demand" or "Provisioned (R:X W:Y)"
|
|
110
|
+
*/
|
|
111
|
+
export function formatBillingMode(table) {
|
|
112
|
+
const billing = table.BillingModeSummary?.BillingMode;
|
|
113
|
+
if (billing === "PAY_PER_REQUEST") {
|
|
114
|
+
return "On-Demand";
|
|
115
|
+
}
|
|
116
|
+
const r = table.ProvisionedThroughput?.ReadCapacityUnits ?? 0;
|
|
117
|
+
const w = table.ProvisionedThroughput?.WriteCapacityUnits ?? 0;
|
|
118
|
+
return `Provisioned (R:${r} W:${w})`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Format key schema: "PK: userId, SK: timestamp" or just "PK: id"
|
|
122
|
+
*/
|
|
123
|
+
export function formatKeySchema(table) {
|
|
124
|
+
const keys = table.KeySchema ?? [];
|
|
125
|
+
const hash = keys.find((k) => k.KeyType === "HASH");
|
|
126
|
+
const range = keys.find((k) => k.KeyType === "RANGE");
|
|
127
|
+
let result = "";
|
|
128
|
+
if (hash)
|
|
129
|
+
result += `PK: ${hash.AttributeName}`;
|
|
130
|
+
if (range)
|
|
131
|
+
result += (result ? ", " : "") + `SK: ${range.AttributeName}`;
|
|
132
|
+
return result || "-";
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Extract primary key value from an item.
|
|
136
|
+
* Returns a JSON string like '{"userId": "abc123"}' or '{"id": "x", "sk": "y"}'
|
|
137
|
+
*/
|
|
138
|
+
export function extractPrimaryKeyJson(item, table) {
|
|
139
|
+
const keys = table.KeySchema ?? [];
|
|
140
|
+
const pkObj = {};
|
|
141
|
+
for (const key of keys) {
|
|
142
|
+
const attrName = key.AttributeName;
|
|
143
|
+
const attr = item[attrName];
|
|
144
|
+
if (attr) {
|
|
145
|
+
pkObj[attrName] = unwrapDynamoValue(attr);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return JSON.stringify(pkObj);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the sort key value from an item for display.
|
|
152
|
+
* Returns unwrapped value or undefined.
|
|
153
|
+
*/
|
|
154
|
+
export function extractSkValue(item, table) {
|
|
155
|
+
const rangeKey = table.KeySchema?.find((k) => k.KeyType === "RANGE");
|
|
156
|
+
if (!rangeKey)
|
|
157
|
+
return undefined;
|
|
158
|
+
const attr = item[rangeKey.AttributeName];
|
|
159
|
+
if (!attr)
|
|
160
|
+
return undefined;
|
|
161
|
+
return String(unwrapDynamoValue(attr));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the partition key value from an item for display.
|
|
165
|
+
* Returns unwrapped value or undefined.
|
|
166
|
+
*/
|
|
167
|
+
export function extractPkValue(item, table) {
|
|
168
|
+
const hashKey = table.KeySchema?.find((k) => k.KeyType === "HASH");
|
|
169
|
+
if (!hashKey)
|
|
170
|
+
return undefined;
|
|
171
|
+
const attr = item[hashKey.AttributeName];
|
|
172
|
+
if (!attr)
|
|
173
|
+
return undefined;
|
|
174
|
+
return String(unwrapDynamoValue(attr));
|
|
175
|
+
}
|
|
@@ -4,6 +4,7 @@ import { formatDate } from "./utils.js";
|
|
|
4
4
|
import { createIamEditCapability } from "./capabilities/editCapability.js";
|
|
5
5
|
import { createIamDetailCapability } from "./capabilities/detailCapability.js";
|
|
6
6
|
import { createIamYankCapability } from "./capabilities/yankCapability.js";
|
|
7
|
+
import { SERVICE_COLORS } from "../../constants/theme.js";
|
|
7
8
|
function getIamMeta(row) {
|
|
8
9
|
return row.meta;
|
|
9
10
|
}
|
|
@@ -241,7 +242,7 @@ export function createIamServiceAdapter() {
|
|
|
241
242
|
return {
|
|
242
243
|
id: "iam",
|
|
243
244
|
label: "IAM",
|
|
244
|
-
hudColor:
|
|
245
|
+
hudColor: SERVICE_COLORS.iam,
|
|
245
246
|
getColumns,
|
|
246
247
|
getRows,
|
|
247
248
|
onSelect,
|
|
@@ -1,22 +1,179 @@
|
|
|
1
1
|
import { textCell } from "../../types.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { runAwsJsonAsync } from "../../utils/aws.js";
|
|
3
|
+
import { atom } from "jotai";
|
|
4
|
+
import { getDefaultStore } from "jotai";
|
|
5
|
+
import { createRoute53DetailCapability } from "./capabilities/detailCapability.js";
|
|
6
|
+
import { createRoute53YankCapability } from "./capabilities/yankCapability.js";
|
|
7
|
+
import { SERVICE_COLORS } from "../../constants/theme.js";
|
|
8
|
+
export const route53LevelAtom = atom({ kind: "zones" });
|
|
9
|
+
export const route53BackStackAtom = atom([]);
|
|
10
|
+
export function createRoute53ServiceAdapter(endpointUrl, region) {
|
|
11
|
+
const store = getDefaultStore();
|
|
12
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
13
|
+
const getLevel = () => store.get(route53LevelAtom);
|
|
14
|
+
const setLevel = (level) => store.set(route53LevelAtom, level);
|
|
15
|
+
const getBackStack = () => store.get(route53BackStackAtom);
|
|
16
|
+
const setBackStack = (stack) => store.set(route53BackStackAtom, stack);
|
|
17
|
+
const getColumns = () => {
|
|
18
|
+
const level = getLevel();
|
|
19
|
+
if (level.kind === "zones") {
|
|
20
|
+
return [
|
|
21
|
+
{ key: "name", label: "Name" },
|
|
22
|
+
{ key: "zoneId", label: "Zone ID" },
|
|
23
|
+
{ key: "recordCount", label: "Records" },
|
|
24
|
+
{ key: "type", label: "Type", width: 10 },
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
// records level
|
|
28
|
+
return [
|
|
29
|
+
{ key: "name", label: "Name" },
|
|
30
|
+
{ key: "type", label: "Type", width: 10 },
|
|
31
|
+
{ key: "ttl", label: "TTL", width: 10 },
|
|
32
|
+
{ key: "values", label: "Value(s)", width: 50 },
|
|
33
|
+
];
|
|
34
|
+
};
|
|
4
35
|
const getRows = async () => {
|
|
5
|
-
|
|
36
|
+
const level = getLevel();
|
|
37
|
+
if (level.kind === "zones") {
|
|
38
|
+
const data = await runAwsJsonAsync([
|
|
39
|
+
"route53",
|
|
40
|
+
"list-hosted-zones",
|
|
41
|
+
...regionArgs,
|
|
42
|
+
]);
|
|
43
|
+
return (data.HostedZones ?? []).map((zone) => {
|
|
44
|
+
const isPrivate = zone.Config?.PrivateZone ?? zone.HostedZoneConfig?.PrivateZone ?? false;
|
|
45
|
+
const shortZoneId = zone.Id.replace(/^\/hostedzone\//, "");
|
|
46
|
+
return {
|
|
47
|
+
id: zone.Id,
|
|
48
|
+
cells: {
|
|
49
|
+
name: textCell(zone.Name),
|
|
50
|
+
zoneId: textCell(shortZoneId),
|
|
51
|
+
recordCount: textCell(String(zone.ResourceRecordSetCount ?? 0)),
|
|
52
|
+
type: textCell(isPrivate ? "Private" : "Public"),
|
|
53
|
+
},
|
|
54
|
+
meta: {
|
|
55
|
+
type: "zone",
|
|
56
|
+
zoneId: shortZoneId,
|
|
57
|
+
zoneName: zone.Name,
|
|
58
|
+
isPrivate,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// records level
|
|
64
|
+
const { zoneId, zoneName } = level;
|
|
65
|
+
try {
|
|
66
|
+
const fullZoneId = zoneId.startsWith("/hostedzone/") ? zoneId : `/hostedzone/${zoneId}`;
|
|
67
|
+
const data = await runAwsJsonAsync([
|
|
68
|
+
"route53",
|
|
69
|
+
"list-resource-record-sets",
|
|
70
|
+
"--hosted-zone-id",
|
|
71
|
+
fullZoneId,
|
|
72
|
+
...regionArgs,
|
|
73
|
+
]);
|
|
74
|
+
return (data.ResourceRecordSets ?? []).map((record) => {
|
|
75
|
+
const values = [];
|
|
76
|
+
let valuesDisplay = "";
|
|
77
|
+
if (record.AliasTarget) {
|
|
78
|
+
valuesDisplay = `ALIAS → ${record.AliasTarget.DNSName}`;
|
|
79
|
+
}
|
|
80
|
+
else if (record.ResourceRecords && record.ResourceRecords.length > 0) {
|
|
81
|
+
record.ResourceRecords.forEach((r) => values.push(r.Value));
|
|
82
|
+
if (values.length <= 3) {
|
|
83
|
+
valuesDisplay = values.join(", ");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
valuesDisplay = `<${values.length} values>`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
id: `${record.Name}-${record.Type}`,
|
|
91
|
+
cells: {
|
|
92
|
+
name: textCell(record.Name),
|
|
93
|
+
type: textCell(record.Type),
|
|
94
|
+
ttl: textCell(record.AliasTarget ? "-" : String(record.TTL ?? "-")),
|
|
95
|
+
values: textCell(valuesDisplay),
|
|
96
|
+
},
|
|
97
|
+
meta: {
|
|
98
|
+
type: "record",
|
|
99
|
+
zoneId,
|
|
100
|
+
zoneName,
|
|
101
|
+
recordName: record.Name,
|
|
102
|
+
recordType: record.Type,
|
|
103
|
+
recordTtl: record.TTL,
|
|
104
|
+
recordValues: values,
|
|
105
|
+
recordAliasTarget: record.AliasTarget,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
6
113
|
};
|
|
7
|
-
const onSelect = async (
|
|
114
|
+
const onSelect = async (row) => {
|
|
115
|
+
const level = getLevel();
|
|
116
|
+
const backStack = getBackStack();
|
|
117
|
+
const meta = row.meta;
|
|
118
|
+
if (level.kind === "zones") {
|
|
119
|
+
if (!meta || meta.type !== "zone") {
|
|
120
|
+
return { action: "none" };
|
|
121
|
+
}
|
|
122
|
+
const newStack = [...backStack, { level: level, selectedIndex: 0 }];
|
|
123
|
+
setBackStack(newStack);
|
|
124
|
+
setLevel({
|
|
125
|
+
kind: "records",
|
|
126
|
+
zoneId: meta.zoneId,
|
|
127
|
+
zoneName: meta.zoneName,
|
|
128
|
+
});
|
|
129
|
+
return { action: "navigate" };
|
|
130
|
+
}
|
|
131
|
+
// records level: leaf, no drill-down
|
|
8
132
|
return { action: "none" };
|
|
9
133
|
};
|
|
134
|
+
const canGoBack = () => getBackStack().length > 0;
|
|
135
|
+
const goBack = () => {
|
|
136
|
+
const backStack = getBackStack();
|
|
137
|
+
if (backStack.length > 0) {
|
|
138
|
+
const newStack = backStack.slice(0, -1);
|
|
139
|
+
const frame = backStack[backStack.length - 1];
|
|
140
|
+
setBackStack(newStack);
|
|
141
|
+
setLevel(frame.level);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const getPath = () => {
|
|
145
|
+
const level = getLevel();
|
|
146
|
+
if (level.kind === "zones")
|
|
147
|
+
return "route53://";
|
|
148
|
+
return `route53://${level.zoneName}`;
|
|
149
|
+
};
|
|
150
|
+
const getContextLabel = () => {
|
|
151
|
+
const level = getLevel();
|
|
152
|
+
if (level.kind === "zones")
|
|
153
|
+
return "🌐 Hosted Zones";
|
|
154
|
+
return `🌐 ${level.zoneName}`;
|
|
155
|
+
};
|
|
156
|
+
// Compose capabilities
|
|
157
|
+
const detailCapability = createRoute53DetailCapability(region, getLevel);
|
|
158
|
+
const yankCapability = createRoute53YankCapability(region, getLevel);
|
|
10
159
|
return {
|
|
11
160
|
id: "route53",
|
|
12
161
|
label: "Route53",
|
|
13
|
-
hudColor:
|
|
162
|
+
hudColor: SERVICE_COLORS.route53,
|
|
14
163
|
getColumns,
|
|
15
164
|
getRows,
|
|
16
165
|
onSelect,
|
|
17
|
-
canGoBack
|
|
18
|
-
goBack
|
|
19
|
-
getPath
|
|
20
|
-
getContextLabel
|
|
166
|
+
canGoBack,
|
|
167
|
+
goBack,
|
|
168
|
+
getPath,
|
|
169
|
+
getContextLabel,
|
|
170
|
+
reset() {
|
|
171
|
+
setLevel({ kind: "zones" });
|
|
172
|
+
setBackStack([]);
|
|
173
|
+
},
|
|
174
|
+
capabilities: {
|
|
175
|
+
detail: detailCapability,
|
|
176
|
+
yank: yankCapability,
|
|
177
|
+
},
|
|
21
178
|
};
|
|
22
179
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { runAwsJsonAsync } from "../../../utils/aws.js";
|
|
2
|
+
export function createRoute53DetailCapability(region, getLevel) {
|
|
3
|
+
const regionArgs = region ? ["--region", region] : [];
|
|
4
|
+
const getDetails = async (row) => {
|
|
5
|
+
const meta = row.meta;
|
|
6
|
+
if (!meta) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const level = getLevel?.();
|
|
10
|
+
// Zone level
|
|
11
|
+
if (level?.kind === "zones" && meta.type === "zone") {
|
|
12
|
+
const fullZoneId = meta.zoneId.startsWith("/hostedzone/")
|
|
13
|
+
? meta.zoneId
|
|
14
|
+
: `/hostedzone/${meta.zoneId}`;
|
|
15
|
+
const data = await runAwsJsonAsync([
|
|
16
|
+
"route53",
|
|
17
|
+
"get-hosted-zone",
|
|
18
|
+
"--id",
|
|
19
|
+
fullZoneId,
|
|
20
|
+
...regionArgs,
|
|
21
|
+
]);
|
|
22
|
+
const fields = [
|
|
23
|
+
{ label: "Zone Name", value: data.Name ?? "-" },
|
|
24
|
+
{ label: "Zone ID (Full)", value: data.Id ?? "-" },
|
|
25
|
+
{ label: "Zone ID (Short)", value: meta.zoneId },
|
|
26
|
+
{ label: "Record Count", value: String(data.ResourceRecordSetCount ?? 0) },
|
|
27
|
+
{
|
|
28
|
+
label: "Type",
|
|
29
|
+
value: meta.isPrivate ? "Private" : "Public",
|
|
30
|
+
},
|
|
31
|
+
{ label: "Caller Reference", value: data.CallerReference ?? "-" },
|
|
32
|
+
];
|
|
33
|
+
return fields;
|
|
34
|
+
}
|
|
35
|
+
// Record level
|
|
36
|
+
if (level?.kind === "records" && meta.type === "record") {
|
|
37
|
+
const fields = [
|
|
38
|
+
{ label: "Record Name", value: meta.recordName ?? "-" },
|
|
39
|
+
{ label: "Record Type", value: meta.recordType ?? "-" },
|
|
40
|
+
{ label: "TTL", value: meta.recordAliasTarget ? "(Alias - no TTL)" : String(meta.recordTtl ?? "-") },
|
|
41
|
+
];
|
|
42
|
+
if (meta.recordValues && meta.recordValues.length > 0) {
|
|
43
|
+
fields.push({
|
|
44
|
+
label: "Values",
|
|
45
|
+
value: meta.recordValues.join("\n"),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (meta.recordAliasTarget) {
|
|
49
|
+
fields.push({
|
|
50
|
+
label: "Alias Target",
|
|
51
|
+
value: meta.recordAliasTarget.DNSName,
|
|
52
|
+
});
|
|
53
|
+
fields.push({
|
|
54
|
+
label: "Hosted Zone ID",
|
|
55
|
+
value: meta.recordAliasTarget.HostedZoneId,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return fields;
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
};
|
|
62
|
+
return { getDetails };
|
|
63
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createYankCapability } from "../../../adapters/capabilities/YankCapability.js";
|
|
2
|
+
import { Route53RowMetaSchema } from "../schema.js";
|
|
3
|
+
import { Route53YankOptions } from "./yankOptions.js";
|
|
4
|
+
export function createRoute53YankCapability(_region, _getLevel) {
|
|
5
|
+
return createYankCapability(Route53YankOptions, Route53RowMetaSchema, {});
|
|
6
|
+
}
|