mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Refactor components for improved readability and consistency
- Added missing newlines at the end of files in MarkdownContent.tsx and RootCauseCard.tsx - Reformatted shadowColor and color properties in NotesSection.tsx, SegmentedControl.tsx, MainTabNavigator.tsx, HomeScreen.tsx for better readability - Enhanced code formatting in SectionHeader.tsx and OnCallStackNavigator.tsx for consistency - Improved readability of getEntityId function in useAllProjectOnCallPolicies.ts - Refactored conditional rendering in AlertDetailScreen.tsx, AlertEpisodeDetailScreen.tsx, IncidentDetailScreen.tsx, and IncidentEpisodeDetailScreen.tsx for better clarity
This commit is contained in:
@@ -11,14 +11,17 @@ export function registerConfigCommands(program: Command): void {
|
||||
.command("login")
|
||||
.description("Authenticate with a OneUptime instance")
|
||||
.argument("<api-key>", "API key for authentication")
|
||||
.argument("<instance-url>", "OneUptime instance URL (e.g. https://oneuptime.com)")
|
||||
.option(
|
||||
"--context-name <name>",
|
||||
"Name for this context",
|
||||
"default",
|
||||
.argument(
|
||||
"<instance-url>",
|
||||
"OneUptime instance URL (e.g. https://oneuptime.com)",
|
||||
)
|
||||
.option("--context-name <name>", "Name for this context", "default")
|
||||
.action(
|
||||
(apiKey: string, instanceUrl: string, options: { contextName: string }) => {
|
||||
(
|
||||
apiKey: string,
|
||||
instanceUrl: string,
|
||||
options: { contextName: string },
|
||||
) => {
|
||||
try {
|
||||
const context: CLIContext = {
|
||||
name: options.contextName,
|
||||
@@ -68,20 +71,17 @@ export function registerConfigCommands(program: Command): void {
|
||||
process.argv.includes("--no-color");
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
head: ["", "Name", "URL"].map((h: string) =>
|
||||
noColor ? h : chalk.cyan(h),
|
||||
),
|
||||
head: ["", "Name", "URL"].map((h: string) => {
|
||||
return noColor ? h : chalk.cyan(h);
|
||||
}),
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const ctx of contexts) {
|
||||
table.push([
|
||||
ctx.isCurrent ? "*" : "",
|
||||
ctx.name,
|
||||
ctx.apiUrl,
|
||||
]);
|
||||
table.push([ctx.isCurrent ? "*" : "", ctx.name, ctx.apiUrl]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(table.toString());
|
||||
});
|
||||
|
||||
@@ -93,9 +93,7 @@ export function registerConfigCommands(program: Command): void {
|
||||
ConfigManager.setCurrentContext(name);
|
||||
printSuccess(`Switched to context "${name}".`);
|
||||
} catch (error) {
|
||||
printError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
printError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -119,8 +117,11 @@ export function registerConfigCommands(program: Command): void {
|
||||
ctx.apiKey.substring(ctx.apiKey.length - 4)
|
||||
: "****";
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Context: ${ctx.name}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`URL: ${ctx.apiUrl}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`API Key: ${maskedKey}`);
|
||||
});
|
||||
|
||||
@@ -132,9 +133,7 @@ export function registerConfigCommands(program: Command): void {
|
||||
ConfigManager.removeContext(name);
|
||||
printSuccess(`Context "${name}" deleted.`);
|
||||
} catch (error) {
|
||||
printError(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
printError(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,10 +3,9 @@ import DatabaseModels from "Common/Models/DatabaseModels/Index";
|
||||
import AnalyticsModels from "Common/Models/AnalyticsModels/Index";
|
||||
import BaseModel from "Common/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
||||
import AnalyticsBaseModel from "Common/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
||||
import { ResourceInfo } from "../Types/CLITypes";
|
||||
import { ResourceInfo, ResolvedCredentials } from "../Types/CLITypes";
|
||||
import { executeApiRequest, ApiOperation } from "../Core/ApiClient";
|
||||
import { CLIOptions, getResolvedCredentials } from "../Core/ConfigManager";
|
||||
import { ResolvedCredentials } from "../Types/CLITypes";
|
||||
import { formatOutput, printSuccess } from "../Core/OutputFormatter";
|
||||
import { handleError } from "../Core/ErrorHandler";
|
||||
import { generateAllFieldsSelect } from "../Utils/SelectFieldGenerator";
|
||||
@@ -118,8 +117,7 @@ function registerListCommand(
|
||||
}) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials =
|
||||
getResolvedCredentials(parentOpts);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
resource.tableName,
|
||||
resource.modelType,
|
||||
@@ -130,15 +128,11 @@ function registerListCommand(
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "list" as ApiOperation,
|
||||
query: options.query
|
||||
? parseJsonArg(options.query)
|
||||
: undefined,
|
||||
query: options.query ? parseJsonArg(options.query) : undefined,
|
||||
select,
|
||||
skip: parseInt(options.skip, 10),
|
||||
limit: parseInt(options.limit, 10),
|
||||
sort: options.sort
|
||||
? parseJsonArg(options.sort)
|
||||
: undefined,
|
||||
sort: options.sort ? parseJsonArg(options.sort) : undefined,
|
||||
});
|
||||
|
||||
// Extract data array from response
|
||||
@@ -147,6 +141,7 @@ function registerListCommand(
|
||||
? ((result as JSONObject)["data"] as JSONValue) || result
|
||||
: result;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(responseData, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
@@ -166,8 +161,7 @@ function registerGetCommand(
|
||||
.action(async (id: string, options: { output?: string }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials =
|
||||
getResolvedCredentials(parentOpts);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
resource.tableName,
|
||||
resource.modelType,
|
||||
@@ -182,6 +176,7 @@ function registerGetCommand(
|
||||
select,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
@@ -200,31 +195,21 @@ function registerCreateCommand(
|
||||
.option("--file <path>", "Read resource data from a JSON file")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(
|
||||
async (options: {
|
||||
data?: string;
|
||||
file?: string;
|
||||
output?: string;
|
||||
}) => {
|
||||
async (options: { data?: string; file?: string; output?: string }) => {
|
||||
try {
|
||||
let data: JSONObject;
|
||||
|
||||
if (options.file) {
|
||||
const fileContent: string = fs.readFileSync(
|
||||
options.file,
|
||||
"utf-8",
|
||||
);
|
||||
const fileContent: string = fs.readFileSync(options.file, "utf-8");
|
||||
data = JSON.parse(fileContent) as JSONObject;
|
||||
} else if (options.data) {
|
||||
data = parseJsonArg(options.data);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Either --data or --file is required for create.",
|
||||
);
|
||||
throw new Error("Either --data or --file is required for create.");
|
||||
}
|
||||
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials =
|
||||
getResolvedCredentials(parentOpts);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
@@ -234,6 +219,7 @@ function registerCreateCommand(
|
||||
data,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
@@ -251,32 +237,27 @@ function registerUpdateCommand(
|
||||
.description(`Update an existing ${resource.singularName}`)
|
||||
.requiredOption("--data <json>", "Fields to update as JSON")
|
||||
.option("-o, --output <format>", "Output format: json, table, wide")
|
||||
.action(
|
||||
async (
|
||||
id: string,
|
||||
options: { data: string; output?: string },
|
||||
) => {
|
||||
try {
|
||||
const data: JSONObject = parseJsonArg(options.data);
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials =
|
||||
getResolvedCredentials(parentOpts);
|
||||
.action(async (id: string, options: { data: string; output?: string }) => {
|
||||
try {
|
||||
const data: JSONObject = parseJsonArg(options.data);
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "update" as ApiOperation,
|
||||
id,
|
||||
data,
|
||||
});
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "update" as ApiOperation,
|
||||
id,
|
||||
data,
|
||||
});
|
||||
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatOutput(result, options.output));
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerDeleteCommand(
|
||||
@@ -290,8 +271,7 @@ function registerDeleteCommand(
|
||||
.action(async (id: string, _options: { force?: boolean }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials =
|
||||
getResolvedCredentials(parentOpts);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
@@ -301,9 +281,7 @@ function registerDeleteCommand(
|
||||
id,
|
||||
});
|
||||
|
||||
printSuccess(
|
||||
`${resource.singularName} ${id} deleted successfully.`,
|
||||
);
|
||||
printSuccess(`${resource.singularName} ${id} deleted successfully.`);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
@@ -321,17 +299,14 @@ function registerCountCommand(
|
||||
.action(async (options: { query?: string }) => {
|
||||
try {
|
||||
const parentOpts: CLIOptions = getParentOptions(resourceCmd);
|
||||
const creds: ResolvedCredentials =
|
||||
getResolvedCredentials(parentOpts);
|
||||
const creds: ResolvedCredentials = getResolvedCredentials(parentOpts);
|
||||
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
apiUrl: creds.apiUrl,
|
||||
apiKey: creds.apiKey,
|
||||
apiPath: resource.apiPath,
|
||||
operation: "count" as ApiOperation,
|
||||
query: options.query
|
||||
? parseJsonArg(options.query)
|
||||
: undefined,
|
||||
query: options.query ? parseJsonArg(options.query) : undefined,
|
||||
});
|
||||
|
||||
// Count response is typically { count: number }
|
||||
@@ -341,8 +316,10 @@ function registerCountCommand(
|
||||
!Array.isArray(result) &&
|
||||
"count" in (result as JSONObject)
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log((result as JSONObject)["count"]);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(result);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -357,9 +334,7 @@ export function registerResourceCommands(program: Command): void {
|
||||
for (const resource of resources) {
|
||||
const resourceCmd: Command = program
|
||||
.command(resource.name)
|
||||
.description(
|
||||
`Manage ${resource.pluralName} (${resource.modelType})`,
|
||||
);
|
||||
.description(`Manage ${resource.pluralName} (${resource.modelType})`);
|
||||
|
||||
// Database models get full CRUD
|
||||
if (resource.modelType === "database") {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { Command } from "commander";
|
||||
import { CLIContext } from "../Types/CLITypes";
|
||||
import { getCurrentContext, CLIOptions, getResolvedCredentials } from "../Core/ConfigManager";
|
||||
import { ResolvedCredentials } from "../Types/CLITypes";
|
||||
import {
|
||||
CLIContext,
|
||||
ResolvedCredentials,
|
||||
ResourceInfo,
|
||||
} from "../Types/CLITypes";
|
||||
import {
|
||||
getCurrentContext,
|
||||
CLIOptions,
|
||||
getResolvedCredentials,
|
||||
} from "../Core/ConfigManager";
|
||||
import { printInfo, printError } from "../Core/OutputFormatter";
|
||||
import { discoverResources } from "./ResourceCommands";
|
||||
import { ResourceInfo } from "../Types/CLITypes";
|
||||
import Table from "cli-table3";
|
||||
import chalk from "chalk";
|
||||
|
||||
@@ -15,11 +21,15 @@ export function registerUtilityCommands(program: Command): void {
|
||||
.description("Print CLI version")
|
||||
.action(() => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const pkg: { version: string } = require("../package.json") as { version: string };
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
||||
const pkg: { version: string } = require("../package.json") as {
|
||||
version: string;
|
||||
};
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(pkg.version);
|
||||
} catch {
|
||||
// Fallback if package.json can't be loaded at runtime
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("1.0.0");
|
||||
}
|
||||
});
|
||||
@@ -42,7 +52,9 @@ export function registerUtilityCommands(program: Command): void {
|
||||
try {
|
||||
creds = getResolvedCredentials(cliOpts);
|
||||
} catch {
|
||||
printInfo("Not authenticated. Run `oneuptime login` to authenticate.");
|
||||
printInfo(
|
||||
"Not authenticated. Run `oneuptime login` to authenticate.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,9 +65,12 @@ export function registerUtilityCommands(program: Command): void {
|
||||
creds.apiKey.substring(creds.apiKey.length - 4)
|
||||
: "****";
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`URL: ${creds.apiUrl}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`API Key: ${maskedKey}`);
|
||||
if (ctx) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Context: ${ctx.name}`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -73,7 +88,9 @@ export function registerUtilityCommands(program: Command): void {
|
||||
const resources: ResourceInfo[] = discoverResources();
|
||||
|
||||
const filtered: ResourceInfo[] = options.type
|
||||
? resources.filter((r: ResourceInfo) => r.modelType === options.type)
|
||||
? resources.filter((r: ResourceInfo) => {
|
||||
return r.modelType === options.type;
|
||||
})
|
||||
: resources;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
@@ -87,16 +104,26 @@ export function registerUtilityCommands(program: Command): void {
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
head: ["Command", "Singular", "Plural", "Type", "API Path"].map(
|
||||
(h: string) => (noColor ? h : chalk.cyan(h)),
|
||||
(h: string) => {
|
||||
return noColor ? h : chalk.cyan(h);
|
||||
},
|
||||
),
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const r of filtered) {
|
||||
table.push([r.name, r.singularName, r.pluralName, r.modelType, r.apiPath]);
|
||||
table.push([
|
||||
r.name,
|
||||
r.singularName,
|
||||
r.pluralName,
|
||||
r.modelType,
|
||||
r.apiPath,
|
||||
]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(table.toString());
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\nTotal: ${filtered.length} resources`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -126,9 +126,7 @@ export async function executeApiRequest(
|
||||
case "count":
|
||||
case "list":
|
||||
case "read":
|
||||
response = await API.post(
|
||||
data ? { ...baseOptions, data } : baseOptions,
|
||||
);
|
||||
response = await API.post(data ? { ...baseOptions, data } : baseOptions);
|
||||
break;
|
||||
case "update":
|
||||
response = await API.put(data ? { ...baseOptions, data } : baseOptions);
|
||||
|
||||
@@ -80,10 +80,12 @@ export function removeContext(name: string): void {
|
||||
export function listContexts(): Array<CLIContext & { isCurrent: boolean }> {
|
||||
const config: CLIConfig = load();
|
||||
return Object.values(config.contexts).map(
|
||||
(ctx: CLIContext): CLIContext & { isCurrent: boolean } => ({
|
||||
...ctx,
|
||||
isCurrent: ctx.name === config.currentContext,
|
||||
}),
|
||||
(ctx: CLIContext): CLIContext & { isCurrent: boolean } => {
|
||||
return {
|
||||
...ctx,
|
||||
isCurrent: ctx.name === config.currentContext,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import chalk from "chalk";
|
||||
|
||||
function isColorDisabled(): boolean {
|
||||
return (
|
||||
process.env["NO_COLOR"] !== undefined ||
|
||||
process.argv.includes("--no-color")
|
||||
process.env["NO_COLOR"] !== undefined || process.argv.includes("--no-color")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,21 +68,21 @@ function formatTable(data: JSONValue, wide: boolean): string {
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
];
|
||||
const prioritized: string[] = priority.filter((col: string) =>
|
||||
columns.includes(col),
|
||||
);
|
||||
const remaining: string[] = columns.filter(
|
||||
(col: string) => !priority.includes(col),
|
||||
);
|
||||
const prioritized: string[] = priority.filter((col: string) => {
|
||||
return columns.includes(col);
|
||||
});
|
||||
const remaining: string[] = columns.filter((col: string) => {
|
||||
return !priority.includes(col);
|
||||
});
|
||||
columns = [...prioritized, ...remaining].slice(0, 6);
|
||||
}
|
||||
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
|
||||
const table: Table.Table = new Table({
|
||||
head: columns.map((col: string) =>
|
||||
useColor ? chalk.cyan(col) : col,
|
||||
),
|
||||
head: columns.map((col: string) => {
|
||||
return useColor ? chalk.cyan(col) : col;
|
||||
}),
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
@@ -154,8 +153,10 @@ export function formatOutput(data: JSONValue, format?: string): string {
|
||||
export function printSuccess(message: string): void {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
if (useColor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.green(message));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
@@ -163,8 +164,10 @@ export function printSuccess(message: string): void {
|
||||
export function printError(message: string): void {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
if (useColor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red(message));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
@@ -172,8 +175,10 @@ export function printError(message: string): void {
|
||||
export function printWarning(message: string): void {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
if (useColor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.yellow(message));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
}
|
||||
}
|
||||
@@ -181,8 +186,10 @@ export function printWarning(message: string): void {
|
||||
export function printInfo(message: string): void {
|
||||
const useColor: boolean = !isColorDisabled();
|
||||
if (useColor) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.blue(message));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ const program: Command = new Command();
|
||||
|
||||
program
|
||||
.name("oneuptime")
|
||||
.description("OneUptime CLI - Manage your OneUptime resources from the command line")
|
||||
.description(
|
||||
"OneUptime CLI - Manage your OneUptime resources from the command line",
|
||||
)
|
||||
.version("1.0.0")
|
||||
.option("--api-key <key>", "API key (overrides config)")
|
||||
.option("--url <url>", "OneUptime instance URL (overrides config)")
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { executeApiRequest, ApiRequestOptions } from "../Core/ApiClient";
|
||||
import API from "Common/Utils/API";
|
||||
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
|
||||
import { JSONValue } from "Common/Types/JSON";
|
||||
|
||||
// Mock the Common/Utils/API module
|
||||
jest.mock("Common/Utils/API", () => {
|
||||
const mockPost = jest.fn();
|
||||
const mockPut = jest.fn();
|
||||
const mockDelete = jest.fn();
|
||||
const mockPost: jest.Mock = jest.fn();
|
||||
const mockPut: jest.Mock = jest.fn();
|
||||
const mockDelete: jest.Mock = jest.fn();
|
||||
|
||||
function MockAPI(protocol, hostname, _route) {
|
||||
function MockAPI(
|
||||
this: { protocol: string; hostname: string },
|
||||
protocol: string,
|
||||
hostname: string,
|
||||
_route: string,
|
||||
): void {
|
||||
this.protocol = protocol;
|
||||
this.hostname = hostname;
|
||||
}
|
||||
@@ -23,36 +29,50 @@ jest.mock("Common/Utils/API", () => {
|
||||
};
|
||||
});
|
||||
|
||||
function createSuccessResponse(data) {
|
||||
function createSuccessResponse(
|
||||
data: Record<string, unknown> | Record<string, unknown>[],
|
||||
): {
|
||||
data: Record<string, unknown> | Record<string, unknown>[];
|
||||
statusCode: number;
|
||||
} {
|
||||
return { data, statusCode: 200 };
|
||||
}
|
||||
|
||||
function createErrorResponse(statusCode, message) {
|
||||
// HTTPErrorResponse computes `message` from `.data` via a getter.
|
||||
// We create a proper prototype chain and set data to contain the message.
|
||||
const resp = Object.create(HTTPErrorResponse.prototype);
|
||||
function createErrorResponse(
|
||||
statusCode: number,
|
||||
message: string,
|
||||
): HTTPErrorResponse {
|
||||
/*
|
||||
* HTTPErrorResponse computes `message` from `.data` via a getter.
|
||||
* We create a proper prototype chain and set data to contain the message.
|
||||
*/
|
||||
const resp: HTTPErrorResponse = Object.create(HTTPErrorResponse.prototype);
|
||||
resp.statusCode = statusCode;
|
||||
// HTTPResponse stores data in _jsonData and exposes it via `data` getter
|
||||
// But since the prototype chain may not have full getters, we define them
|
||||
/*
|
||||
* HTTPResponse stores data in _jsonData and exposes it via `data` getter
|
||||
* But since the prototype chain may not have full getters, we define them
|
||||
*/
|
||||
Object.defineProperty(resp, "data", {
|
||||
get: () => ({ message: message }),
|
||||
get: (): { message: string } => {
|
||||
return { message: message };
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
return resp;
|
||||
}
|
||||
|
||||
describe("ApiClient", () => {
|
||||
let mockPost;
|
||||
let mockPut;
|
||||
let mockDelete;
|
||||
let mockPost: jest.Mock;
|
||||
let mockPut: jest.Mock;
|
||||
let mockDelete: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockPost = API.post;
|
||||
mockPut = API.put;
|
||||
mockDelete = API.delete;
|
||||
(mockPost as any).mockReset();
|
||||
(mockPut as any).mockReset();
|
||||
(mockDelete as any).mockReset();
|
||||
mockPost = API.post as jest.Mock;
|
||||
mockPut = API.put as jest.Mock;
|
||||
mockDelete = API.delete as jest.Mock;
|
||||
(mockPost as jest.Mock).mockReset();
|
||||
(mockPut as jest.Mock).mockReset();
|
||||
(mockDelete as jest.Mock).mockReset();
|
||||
});
|
||||
|
||||
const baseOptions: ApiRequestOptions = {
|
||||
@@ -64,40 +84,46 @@ describe("ApiClient", () => {
|
||||
|
||||
describe("create operation", () => {
|
||||
it("should make a POST request with data wrapped in { data: ... }", async () => {
|
||||
(mockPost as any).mockResolvedValue(createSuccessResponse({ _id: "123" }));
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ _id: "123" }),
|
||||
);
|
||||
|
||||
const result = await executeApiRequest({
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "create",
|
||||
data: { name: "Test Incident" },
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({ data: { name: "Test Incident" } });
|
||||
expect(result).toEqual({ _id: "123" });
|
||||
});
|
||||
|
||||
it("should use empty object when no data provided for create", async () => {
|
||||
(mockPost as any).mockResolvedValue(createSuccessResponse({ _id: "123" }));
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ _id: "123" }),
|
||||
);
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "create",
|
||||
});
|
||||
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({ data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe("read operation", () => {
|
||||
it("should make a POST request with select and id in route", async () => {
|
||||
(mockPost as any).mockResolvedValue(
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ _id: "abc", name: "Test" }),
|
||||
);
|
||||
|
||||
const result = await executeApiRequest({
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "read",
|
||||
id: "abc-123",
|
||||
@@ -105,14 +131,15 @@ describe("ApiClient", () => {
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("abc-123/get-item");
|
||||
expect(callArgs.data).toEqual({ select: { _id: true, name: true } });
|
||||
expect(result).toEqual({ _id: "abc", name: "Test" });
|
||||
});
|
||||
|
||||
it("should use empty select when none provided", async () => {
|
||||
(mockPost as any).mockResolvedValue(createSuccessResponse({}));
|
||||
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
@@ -120,19 +147,21 @@ describe("ApiClient", () => {
|
||||
id: "abc-123",
|
||||
});
|
||||
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({ select: {} });
|
||||
});
|
||||
|
||||
it("should build route without id when no id provided", async () => {
|
||||
(mockPost as any).mockResolvedValue(createSuccessResponse({}));
|
||||
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "read",
|
||||
});
|
||||
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/api/incident");
|
||||
expect(callArgs.url.toString()).not.toContain("/get-item");
|
||||
});
|
||||
@@ -140,7 +169,9 @@ describe("ApiClient", () => {
|
||||
|
||||
describe("list operation", () => {
|
||||
it("should make a POST request with query, select, skip, limit, sort", async () => {
|
||||
(mockPost as any).mockResolvedValue(createSuccessResponse({ data: [] }));
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ data: [] }),
|
||||
);
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
@@ -153,7 +184,8 @@ describe("ApiClient", () => {
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/get-list");
|
||||
expect(callArgs.data).toEqual({
|
||||
query: { status: "active" },
|
||||
@@ -165,14 +197,17 @@ describe("ApiClient", () => {
|
||||
});
|
||||
|
||||
it("should use defaults when no query options provided", async () => {
|
||||
(mockPost as any).mockResolvedValue(createSuccessResponse({ data: [] }));
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ data: [] }),
|
||||
);
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "list",
|
||||
});
|
||||
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({
|
||||
query: {},
|
||||
select: {},
|
||||
@@ -185,16 +220,19 @@ describe("ApiClient", () => {
|
||||
|
||||
describe("count operation", () => {
|
||||
it("should make a POST request to /count path", async () => {
|
||||
(mockPost as any).mockResolvedValue(createSuccessResponse({ count: 42 }));
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ count: 42 }),
|
||||
);
|
||||
|
||||
const result = await executeApiRequest({
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "count",
|
||||
query: { status: "active" },
|
||||
});
|
||||
|
||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/count");
|
||||
expect(result).toEqual({ count: 42 });
|
||||
});
|
||||
@@ -202,9 +240,11 @@ describe("ApiClient", () => {
|
||||
|
||||
describe("update operation", () => {
|
||||
it("should make a PUT request with data", async () => {
|
||||
(mockPut as any).mockResolvedValue(createSuccessResponse({ _id: "abc" }));
|
||||
(mockPut as jest.Mock).mockResolvedValue(
|
||||
createSuccessResponse({ _id: "abc" }),
|
||||
);
|
||||
|
||||
const result = await executeApiRequest({
|
||||
const result: JSONValue = await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "update",
|
||||
id: "abc-123",
|
||||
@@ -212,14 +252,15 @@ describe("ApiClient", () => {
|
||||
});
|
||||
|
||||
expect(mockPut).toHaveBeenCalledTimes(1);
|
||||
const callArgs = (mockPut as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("abc-123");
|
||||
expect(callArgs.data).toEqual({ data: { name: "Updated" } });
|
||||
expect(result).toEqual({ _id: "abc" });
|
||||
});
|
||||
|
||||
it("should use empty object when no data provided for update", async () => {
|
||||
(mockPut as any).mockResolvedValue(createSuccessResponse({}));
|
||||
(mockPut as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
@@ -227,26 +268,28 @@ describe("ApiClient", () => {
|
||||
id: "abc-123",
|
||||
});
|
||||
|
||||
const callArgs = (mockPut as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toEqual({ data: {} });
|
||||
});
|
||||
|
||||
it("should build route without id when no id provided", async () => {
|
||||
(mockPut as any).mockResolvedValue(createSuccessResponse({}));
|
||||
(mockPut as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "update",
|
||||
});
|
||||
|
||||
const callArgs = (mockPut as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPut as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/api/incident");
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete operation", () => {
|
||||
it("should make a DELETE request", async () => {
|
||||
(mockDelete as any).mockResolvedValue(createSuccessResponse({}));
|
||||
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
@@ -255,27 +298,31 @@ describe("ApiClient", () => {
|
||||
});
|
||||
|
||||
expect(mockDelete).toHaveBeenCalledTimes(1);
|
||||
const callArgs = (mockDelete as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("abc-123");
|
||||
expect(callArgs.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should build route without id when no id provided", async () => {
|
||||
(mockDelete as any).mockResolvedValue(createSuccessResponse({}));
|
||||
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
operation: "delete",
|
||||
});
|
||||
|
||||
const callArgs = (mockDelete as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.url.toString()).toContain("/api/incident");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should throw on HTTPErrorResponse", async () => {
|
||||
(mockPost as any).mockResolvedValue(createErrorResponse(500, "Server Error"));
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createErrorResponse(500, "Server Error"),
|
||||
);
|
||||
|
||||
await expect(
|
||||
executeApiRequest({ ...baseOptions, operation: "create", data: {} }),
|
||||
@@ -283,7 +330,9 @@ describe("ApiClient", () => {
|
||||
});
|
||||
|
||||
it("should include status code in error message", async () => {
|
||||
(mockPost as any).mockResolvedValue(createErrorResponse(403, "Forbidden"));
|
||||
(mockPost as jest.Mock).mockResolvedValue(
|
||||
createErrorResponse(403, "Forbidden"),
|
||||
);
|
||||
|
||||
await expect(
|
||||
executeApiRequest({ ...baseOptions, operation: "list" }),
|
||||
@@ -291,7 +340,7 @@ describe("ApiClient", () => {
|
||||
});
|
||||
|
||||
it("should handle error response with no message", async () => {
|
||||
(mockPost as any).mockResolvedValue(createErrorResponse(500, ""));
|
||||
(mockPost as jest.Mock).mockResolvedValue(createErrorResponse(500, ""));
|
||||
|
||||
await expect(
|
||||
executeApiRequest({ ...baseOptions, operation: "list" }),
|
||||
@@ -301,7 +350,7 @@ describe("ApiClient", () => {
|
||||
|
||||
describe("headers", () => {
|
||||
it("should include APIKey, Content-Type, and Accept headers", async () => {
|
||||
(mockPost as any).mockResolvedValue(createSuccessResponse({}));
|
||||
(mockPost as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
@@ -309,7 +358,8 @@ describe("ApiClient", () => {
|
||||
data: { name: "Test" },
|
||||
});
|
||||
|
||||
const callArgs = (mockPost as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockPost as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.headers["APIKey"]).toBe("test-api-key");
|
||||
expect(callArgs.headers["Content-Type"]).toBe("application/json");
|
||||
expect(callArgs.headers["Accept"]).toBe("application/json");
|
||||
@@ -319,7 +369,7 @@ describe("ApiClient", () => {
|
||||
describe("default/unknown operation", () => {
|
||||
it("should handle unknown operation in buildRequestData (falls to default)", async () => {
|
||||
// The "delete" case hits the default branch in buildRequestData returning undefined
|
||||
(mockDelete as any).mockResolvedValue(createSuccessResponse({}));
|
||||
(mockDelete as jest.Mock).mockResolvedValue(createSuccessResponse({}));
|
||||
|
||||
await executeApiRequest({
|
||||
...baseOptions,
|
||||
@@ -328,7 +378,8 @@ describe("ApiClient", () => {
|
||||
});
|
||||
|
||||
// Should not send data for delete
|
||||
const callArgs = (mockDelete as any).mock.calls[0][0];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callArgs: any = (mockDelete as jest.Mock).mock.calls[0][0];
|
||||
expect(callArgs.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
const CONFIG_DIR = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
describe("ConfigCommands", () => {
|
||||
let originalConfigContent: string | null = null;
|
||||
@@ -42,7 +42,7 @@ describe("ConfigCommands", () => {
|
||||
});
|
||||
|
||||
function createProgram(): Command {
|
||||
const program = new Command();
|
||||
const program: Command = new Command();
|
||||
program.exitOverride(); // Prevent commander from calling process.exit
|
||||
program.configureOutput({
|
||||
writeOut: () => {},
|
||||
@@ -54,7 +54,7 @@ describe("ConfigCommands", () => {
|
||||
|
||||
describe("login command", () => {
|
||||
it("should create a context and set it as current", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -63,7 +63,8 @@ describe("ConfigCommands", () => {
|
||||
"https://example.com",
|
||||
]);
|
||||
|
||||
const ctx = ConfigManager.getCurrentContext();
|
||||
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx!.name).toBe("default");
|
||||
expect(ctx!.apiUrl).toBe("https://example.com");
|
||||
@@ -71,7 +72,7 @@ describe("ConfigCommands", () => {
|
||||
});
|
||||
|
||||
it("should use custom context name", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -82,19 +83,20 @@ describe("ConfigCommands", () => {
|
||||
"production",
|
||||
]);
|
||||
|
||||
const ctx = ConfigManager.getCurrentContext();
|
||||
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(ctx!.name).toBe("production");
|
||||
});
|
||||
|
||||
it("should handle login errors gracefully", async () => {
|
||||
// Mock addContext to throw
|
||||
const addCtxSpy = jest
|
||||
const addCtxSpy: jest.SpyInstance = jest
|
||||
.spyOn(ConfigManager, "addContext")
|
||||
.mockImplementation(() => {
|
||||
throw new Error("Permission denied");
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -108,7 +110,7 @@ describe("ConfigCommands", () => {
|
||||
});
|
||||
|
||||
it("should strip trailing slashes from URL", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -117,14 +119,15 @@ describe("ConfigCommands", () => {
|
||||
"https://example.com///",
|
||||
]);
|
||||
|
||||
const ctx = ConfigManager.getCurrentContext();
|
||||
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(ctx!.apiUrl).toBe("https://example.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("context list command", () => {
|
||||
it("should show message when no contexts exist", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "list"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -141,7 +144,7 @@ describe("ConfigCommands", () => {
|
||||
apiKey: "k2",
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "list"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -160,15 +163,16 @@ describe("ConfigCommands", () => {
|
||||
apiKey: "k2",
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "use", "b"]);
|
||||
|
||||
const current = ConfigManager.getCurrentContext();
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current!.name).toBe("b");
|
||||
});
|
||||
|
||||
it("should handle non-existent context", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "use", "nope"]);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
@@ -183,7 +187,7 @@ describe("ConfigCommands", () => {
|
||||
apiKey: "abcdefghijklm",
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "current"]);
|
||||
|
||||
// Check that masked key is shown
|
||||
@@ -196,7 +200,7 @@ describe("ConfigCommands", () => {
|
||||
});
|
||||
|
||||
it("should show message when no current context", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "current"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -208,7 +212,7 @@ describe("ConfigCommands", () => {
|
||||
apiKey: "abc",
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "context", "current"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("API Key: ****");
|
||||
@@ -223,7 +227,7 @@ describe("ConfigCommands", () => {
|
||||
apiKey: "k1",
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -232,12 +236,13 @@ describe("ConfigCommands", () => {
|
||||
"todelete",
|
||||
]);
|
||||
|
||||
const contexts = ConfigManager.listContexts();
|
||||
const contexts: ReturnType<typeof ConfigManager.listContexts> =
|
||||
ConfigManager.listContexts();
|
||||
expect(contexts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should handle deletion of non-existent context", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
|
||||
@@ -2,10 +2,10 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import * as ConfigManager from "../Core/ConfigManager";
|
||||
import { CLIContext, CLIConfig, ResolvedCredentials } from "../Types/CLITypes";
|
||||
import { CLIConfig, ResolvedCredentials } from "../Types/CLITypes";
|
||||
|
||||
const CONFIG_DIR = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
describe("ConfigManager", () => {
|
||||
let originalConfigContent: string | null = null;
|
||||
@@ -39,7 +39,7 @@ describe("ConfigManager", () => {
|
||||
|
||||
describe("load", () => {
|
||||
it("should return default config when no config file exists", () => {
|
||||
const config = ConfigManager.load();
|
||||
const config: CLIConfig = ConfigManager.load();
|
||||
expect(config.currentContext).toBe("");
|
||||
expect(config.contexts).toEqual({});
|
||||
expect(config.defaults.output).toBe("table");
|
||||
@@ -61,7 +61,7 @@ describe("ConfigManager", () => {
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
const config = ConfigManager.load();
|
||||
const config: CLIConfig = ConfigManager.load();
|
||||
expect(config.currentContext).toBe("test");
|
||||
expect(config.contexts["test"]?.apiKey).toBe("key123");
|
||||
});
|
||||
@@ -72,7 +72,7 @@ describe("ConfigManager", () => {
|
||||
}
|
||||
fs.writeFileSync(CONFIG_FILE, "not valid json {{{", { mode: 0o600 });
|
||||
|
||||
const config = ConfigManager.load();
|
||||
const config: CLIConfig = ConfigManager.load();
|
||||
expect(config.currentContext).toBe("");
|
||||
expect(config.contexts).toEqual({});
|
||||
});
|
||||
@@ -81,9 +81,14 @@ describe("ConfigManager", () => {
|
||||
describe("save", () => {
|
||||
it("should create config directory if it does not exist", () => {
|
||||
// Remove the dir if it exists (we'll restore after)
|
||||
const tmpDir = path.join(os.tmpdir(), ".oneuptime-test-" + Date.now());
|
||||
// We can't easily test this with the real path, but we verify save works
|
||||
// when the dir already exists (which it does after beforeAll).
|
||||
const tmpDir: string = path.join(
|
||||
os.tmpdir(),
|
||||
".oneuptime-test-" + Date.now(),
|
||||
);
|
||||
/*
|
||||
* We can't easily test this with the real path, but we verify save works
|
||||
* when the dir already exists (which it does after beforeAll).
|
||||
*/
|
||||
const config: CLIConfig = {
|
||||
currentContext: "",
|
||||
contexts: {},
|
||||
@@ -103,8 +108,8 @@ describe("ConfigManager", () => {
|
||||
defaults: { output: "table", limit: 10 },
|
||||
};
|
||||
ConfigManager.save(config);
|
||||
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
const parsed = JSON.parse(content);
|
||||
const content: string = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
const parsed: CLIConfig = JSON.parse(content);
|
||||
expect(parsed.currentContext).toBe("x");
|
||||
});
|
||||
});
|
||||
@@ -131,7 +136,8 @@ describe("ConfigManager", () => {
|
||||
apiUrl: "https://prod.com",
|
||||
apiKey: "k1",
|
||||
});
|
||||
const ctx = ConfigManager.getCurrentContext();
|
||||
const ctx: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx!.name).toBe("prod");
|
||||
});
|
||||
@@ -145,7 +151,8 @@ describe("ConfigManager", () => {
|
||||
apiKey: "sk-prod-123",
|
||||
});
|
||||
|
||||
const current = ConfigManager.getCurrentContext();
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current).not.toBeNull();
|
||||
expect(current!.name).toBe("prod");
|
||||
});
|
||||
@@ -162,7 +169,8 @@ describe("ConfigManager", () => {
|
||||
apiKey: "key2",
|
||||
});
|
||||
|
||||
const current = ConfigManager.getCurrentContext();
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current!.name).toBe("prod"); // First one remains current
|
||||
});
|
||||
|
||||
@@ -178,7 +186,8 @@ describe("ConfigManager", () => {
|
||||
apiKey: "key2",
|
||||
});
|
||||
|
||||
const contexts = ConfigManager.listContexts();
|
||||
const contexts: ReturnType<typeof ConfigManager.listContexts> =
|
||||
ConfigManager.listContexts();
|
||||
expect(contexts).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -197,14 +206,15 @@ describe("ConfigManager", () => {
|
||||
});
|
||||
|
||||
ConfigManager.setCurrentContext("b");
|
||||
const current = ConfigManager.getCurrentContext();
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current!.name).toBe("b");
|
||||
});
|
||||
|
||||
it("should throw for non-existent context", () => {
|
||||
expect(() => ConfigManager.setCurrentContext("nonexistent")).toThrow(
|
||||
'Context "nonexistent" does not exist',
|
||||
);
|
||||
expect(() => {
|
||||
return ConfigManager.setCurrentContext("nonexistent");
|
||||
}).toThrow('Context "nonexistent" does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,14 +227,15 @@ describe("ConfigManager", () => {
|
||||
});
|
||||
ConfigManager.removeContext("test");
|
||||
|
||||
const contexts = ConfigManager.listContexts();
|
||||
const contexts: ReturnType<typeof ConfigManager.listContexts> =
|
||||
ConfigManager.listContexts();
|
||||
expect(contexts).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should throw for non-existent context", () => {
|
||||
expect(() => ConfigManager.removeContext("nonexistent")).toThrow(
|
||||
'Context "nonexistent" does not exist',
|
||||
);
|
||||
expect(() => {
|
||||
return ConfigManager.removeContext("nonexistent");
|
||||
}).toThrow('Context "nonexistent" does not exist');
|
||||
});
|
||||
|
||||
it("should update current context when removing the current one", () => {
|
||||
@@ -241,7 +252,8 @@ describe("ConfigManager", () => {
|
||||
ConfigManager.setCurrentContext("a");
|
||||
ConfigManager.removeContext("a");
|
||||
|
||||
const current = ConfigManager.getCurrentContext();
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current).not.toBeNull();
|
||||
expect(current!.name).toBe("b");
|
||||
});
|
||||
@@ -255,7 +267,7 @@ describe("ConfigManager", () => {
|
||||
ConfigManager.removeContext("only");
|
||||
|
||||
expect(ConfigManager.getCurrentContext()).toBeNull();
|
||||
const config = ConfigManager.load();
|
||||
const config: CLIConfig = ConfigManager.load();
|
||||
expect(config.currentContext).toBe("");
|
||||
});
|
||||
|
||||
@@ -273,7 +285,8 @@ describe("ConfigManager", () => {
|
||||
ConfigManager.setCurrentContext("a");
|
||||
ConfigManager.removeContext("b");
|
||||
|
||||
const current = ConfigManager.getCurrentContext();
|
||||
const current: ReturnType<typeof ConfigManager.getCurrentContext> =
|
||||
ConfigManager.getCurrentContext();
|
||||
expect(current!.name).toBe("a");
|
||||
});
|
||||
});
|
||||
@@ -296,9 +309,22 @@ describe("ConfigManager", () => {
|
||||
});
|
||||
ConfigManager.setCurrentContext("b");
|
||||
|
||||
const contexts = ConfigManager.listContexts();
|
||||
const a = contexts.find((c) => c.name === "a");
|
||||
const b = contexts.find((c) => c.name === "b");
|
||||
const contexts: ReturnType<typeof ConfigManager.listContexts> =
|
||||
ConfigManager.listContexts();
|
||||
const a:
|
||||
| ReturnType<typeof ConfigManager.listContexts>[number]
|
||||
| undefined = contexts.find(
|
||||
(c: ReturnType<typeof ConfigManager.listContexts>[number]) => {
|
||||
return c.name === "a";
|
||||
},
|
||||
);
|
||||
const b:
|
||||
| ReturnType<typeof ConfigManager.listContexts>[number]
|
||||
| undefined = contexts.find(
|
||||
(c: ReturnType<typeof ConfigManager.listContexts>[number]) => {
|
||||
return c.name === "b";
|
||||
},
|
||||
);
|
||||
expect(a!.isCurrent).toBe(false);
|
||||
expect(b!.isCurrent).toBe(true);
|
||||
});
|
||||
@@ -340,9 +366,9 @@ describe("ConfigManager", () => {
|
||||
});
|
||||
|
||||
it("should throw when --context flag references non-existent context", () => {
|
||||
expect(() =>
|
||||
ConfigManager.getResolvedCredentials({ context: "nope" }),
|
||||
).toThrow('Context "nope" does not exist');
|
||||
expect(() => {
|
||||
return ConfigManager.getResolvedCredentials({ context: "nope" });
|
||||
}).toThrow('Context "nope" does not exist');
|
||||
});
|
||||
|
||||
it("should resolve from current context in config", () => {
|
||||
@@ -390,17 +416,19 @@ describe("ConfigManager", () => {
|
||||
const creds: ResolvedCredentials = ConfigManager.getResolvedCredentials(
|
||||
{},
|
||||
);
|
||||
// env vars take priority: both are set so goes through priority 2
|
||||
// Actually, only ONEUPTIME_API_KEY is set, not ONEUPTIME_URL
|
||||
// So it falls through to priority 4 (current context)
|
||||
/*
|
||||
* env vars take priority: both are set so goes through priority 2
|
||||
* Actually, only ONEUPTIME_API_KEY is set, not ONEUPTIME_URL
|
||||
* So it falls through to priority 4 (current context)
|
||||
*/
|
||||
expect(creds.apiKey).toBe("ctx-key");
|
||||
expect(creds.apiUrl).toBe("https://ctx.com");
|
||||
});
|
||||
|
||||
it("should throw when no credentials available at all", () => {
|
||||
expect(() => ConfigManager.getResolvedCredentials({})).toThrow(
|
||||
"No credentials found",
|
||||
);
|
||||
expect(() => {
|
||||
return ConfigManager.getResolvedCredentials({});
|
||||
}).toThrow("No credentials found");
|
||||
});
|
||||
|
||||
it("should prefer CLI flags over env vars", () => {
|
||||
|
||||
@@ -9,9 +9,11 @@ describe("ErrorHandler", () => {
|
||||
exitSpy = jest.spyOn(process, "exit").mockImplementation((() => {
|
||||
// no-op
|
||||
}) as any);
|
||||
printErrorSpy = jest.spyOn(OutputFormatter, "printError").mockImplementation(() => {
|
||||
// no-op
|
||||
});
|
||||
printErrorSpy = jest
|
||||
.spyOn(OutputFormatter, "printError")
|
||||
.mockImplementation(() => {
|
||||
// no-op
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -44,9 +46,7 @@ describe("ErrorHandler", () => {
|
||||
|
||||
it("should exit with NotFound for 404 errors", () => {
|
||||
handleError(new Error("HTTP 404 response"));
|
||||
expect(printErrorSpy).toHaveBeenCalledWith(
|
||||
"Not found: HTTP 404 response",
|
||||
);
|
||||
expect(printErrorSpy).toHaveBeenCalledWith("Not found: HTTP 404 response");
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.NotFound);
|
||||
});
|
||||
|
||||
@@ -65,9 +65,7 @@ describe("ErrorHandler", () => {
|
||||
|
||||
it("should exit with GeneralError for generic Error objects", () => {
|
||||
handleError(new Error("Something went wrong"));
|
||||
expect(printErrorSpy).toHaveBeenCalledWith(
|
||||
"Error: Something went wrong",
|
||||
);
|
||||
expect(printErrorSpy).toHaveBeenCalledWith("Error: Something went wrong");
|
||||
expect(exitSpy).toHaveBeenCalledWith(ExitCode.GeneralError);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Command } from "commander";
|
||||
import { Command, Option } from "commander";
|
||||
import { registerConfigCommands } from "../Commands/ConfigCommands";
|
||||
import { registerResourceCommands } from "../Commands/ResourceCommands";
|
||||
import { registerUtilityCommands } from "../Commands/UtilityCommands";
|
||||
|
||||
describe("Index (CLI entry point)", () => {
|
||||
it("should create a program with all command groups registered", () => {
|
||||
const program = new Command();
|
||||
const program: Command = new Command();
|
||||
program
|
||||
.name("oneuptime")
|
||||
.description(
|
||||
@@ -23,7 +23,9 @@ describe("Index (CLI entry point)", () => {
|
||||
registerResourceCommands(program);
|
||||
|
||||
// Verify all expected commands are registered
|
||||
const commandNames = program.commands.map((c) => c.name());
|
||||
const commandNames: string[] = program.commands.map((c: Command) => {
|
||||
return c.name();
|
||||
});
|
||||
expect(commandNames).toContain("login");
|
||||
expect(commandNames).toContain("context");
|
||||
expect(commandNames).toContain("version");
|
||||
@@ -35,14 +37,14 @@ describe("Index (CLI entry point)", () => {
|
||||
});
|
||||
|
||||
it("should set correct program name and description", () => {
|
||||
const program = new Command();
|
||||
const program: Command = new Command();
|
||||
program.name("oneuptime").description("OneUptime CLI");
|
||||
|
||||
expect(program.name()).toBe("oneuptime");
|
||||
});
|
||||
|
||||
it("should define global options", () => {
|
||||
const program = new Command();
|
||||
const program: Command = new Command();
|
||||
program
|
||||
.option("--api-key <key>", "API key")
|
||||
.option("--url <url>", "URL")
|
||||
@@ -51,8 +53,10 @@ describe("Index (CLI entry point)", () => {
|
||||
.option("--no-color", "Disable color");
|
||||
|
||||
// Parse with just the program name - verify options are registered
|
||||
const options = program.options;
|
||||
const optionNames = options.map((o) => o.long || o.short);
|
||||
const options: readonly Option[] = program.options;
|
||||
const optionNames: (string | undefined)[] = options.map((o: Option) => {
|
||||
return o.long || o.short;
|
||||
});
|
||||
expect(optionNames).toContain("--api-key");
|
||||
expect(optionNames).toContain("--url");
|
||||
expect(optionNames).toContain("--context");
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
printWarning,
|
||||
printInfo,
|
||||
} from "../Core/OutputFormatter";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
describe("OutputFormatter", () => {
|
||||
let consoleLogSpy: jest.SpyInstance;
|
||||
@@ -32,48 +33,48 @@ describe("OutputFormatter", () => {
|
||||
|
||||
describe("formatOutput with JSON format", () => {
|
||||
it("should format single object as JSON", () => {
|
||||
const data = { id: "123", name: "Test" };
|
||||
const result = formatOutput(data, "json");
|
||||
const data: Record<string, string> = { id: "123", name: "Test" };
|
||||
const result: string = formatOutput(data, "json");
|
||||
expect(JSON.parse(result)).toEqual(data);
|
||||
});
|
||||
|
||||
it("should format array as JSON", () => {
|
||||
const data = [
|
||||
const data: Record<string, string>[] = [
|
||||
{ id: "1", name: "A" },
|
||||
{ id: "2", name: "B" },
|
||||
];
|
||||
const result = formatOutput(data, "json");
|
||||
const result: string = formatOutput(data, "json");
|
||||
expect(JSON.parse(result)).toEqual(data);
|
||||
});
|
||||
|
||||
it("should format null as JSON", () => {
|
||||
const result = formatOutput(null, "json");
|
||||
const result: string = formatOutput(null, "json");
|
||||
expect(result).toBe("null");
|
||||
});
|
||||
|
||||
it("should format number as JSON", () => {
|
||||
const result = formatOutput(42, "json");
|
||||
const result: string = formatOutput(42, "json");
|
||||
expect(result).toBe("42");
|
||||
});
|
||||
|
||||
it("should format string as JSON", () => {
|
||||
const result = formatOutput("hello", "json");
|
||||
const result: string = formatOutput("hello", "json");
|
||||
expect(result).toBe('"hello"');
|
||||
});
|
||||
|
||||
it("should format boolean as JSON", () => {
|
||||
const result = formatOutput(true, "json");
|
||||
const result: string = formatOutput(true, "json");
|
||||
expect(result).toBe("true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatOutput with table format", () => {
|
||||
it("should format array as table", () => {
|
||||
const data = [
|
||||
const data: Record<string, string>[] = [
|
||||
{ _id: "1", name: "A" },
|
||||
{ _id: "2", name: "B" },
|
||||
];
|
||||
const result = formatOutput(data, "table");
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("A");
|
||||
expect(result).toContain("2");
|
||||
@@ -81,76 +82,76 @@ describe("OutputFormatter", () => {
|
||||
});
|
||||
|
||||
it("should handle empty array", () => {
|
||||
const result = formatOutput([], "table");
|
||||
const result: string = formatOutput([], "table");
|
||||
expect(result).toBe("No results found.");
|
||||
});
|
||||
|
||||
it("should handle single object as key-value table", () => {
|
||||
const data = { name: "Test", status: "Active" };
|
||||
const result = formatOutput(data, "table");
|
||||
const data: Record<string, string> = { name: "Test", status: "Active" };
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("Test");
|
||||
expect(result).toContain("Active");
|
||||
});
|
||||
|
||||
it("should return 'No data returned.' for null in table mode", () => {
|
||||
const result = formatOutput(null, "table");
|
||||
const result: string = formatOutput(null, "table");
|
||||
expect(result).toBe("No data returned.");
|
||||
});
|
||||
|
||||
it("should return 'No data returned.' for undefined in table mode", () => {
|
||||
const result = formatOutput(undefined as any, "table");
|
||||
const result: string = formatOutput(undefined as any, "table");
|
||||
expect(result).toBe("No data returned.");
|
||||
});
|
||||
|
||||
it("should return 'No data returned.' for empty string in table mode", () => {
|
||||
const result = formatOutput("" as any, "table");
|
||||
const result: string = formatOutput("" as any, "table");
|
||||
expect(result).toBe("No data returned.");
|
||||
});
|
||||
|
||||
it("should fallback to JSON for array of non-objects", () => {
|
||||
const data = ["a", "b", "c"];
|
||||
const result = formatOutput(data, "table");
|
||||
const data: string[] = ["a", "b", "c"];
|
||||
const result: string = formatOutput(data, "table");
|
||||
// First item is not an object, so should fallback to JSON
|
||||
expect(result).toContain('"a"');
|
||||
});
|
||||
|
||||
it("should truncate long string values", () => {
|
||||
const longValue = "x".repeat(100);
|
||||
const data = [{ _id: "1", field: longValue }];
|
||||
const result = formatOutput(data, "table");
|
||||
const longValue: string = "x".repeat(100);
|
||||
const data: Record<string, string>[] = [{ _id: "1", field: longValue }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("...");
|
||||
});
|
||||
|
||||
it("should truncate long object values", () => {
|
||||
const bigObj = { a: "x".repeat(80) };
|
||||
const data = [{ _id: "1", nested: bigObj }];
|
||||
const result = formatOutput(data, "table");
|
||||
const bigObj: Record<string, string> = { a: "x".repeat(80) };
|
||||
const data: JSONObject[] = [{ _id: "1", nested: bigObj }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("...");
|
||||
});
|
||||
|
||||
it("should show short object values without truncation", () => {
|
||||
const smallObj = { a: 1 };
|
||||
const data = [{ _id: "1", nested: smallObj }];
|
||||
const result = formatOutput(data, "table");
|
||||
const smallObj: Record<string, number> = { a: 1 };
|
||||
const data: JSONObject[] = [{ _id: "1", nested: smallObj }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain('{"a":1}');
|
||||
});
|
||||
|
||||
it("should render null values as empty in table", () => {
|
||||
const data = [{ _id: "1", value: null }];
|
||||
const result = formatOutput(data, "table");
|
||||
const data: JSONObject[] = [{ _id: "1", value: null }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("1");
|
||||
});
|
||||
|
||||
it("should render undefined values as empty in table", () => {
|
||||
const data = [{ _id: "1", value: undefined }];
|
||||
const result = formatOutput(data, "table");
|
||||
const data: JSONObject[] = [{ _id: "1", value: undefined }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
expect(result).toContain("1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatOutput with wide format", () => {
|
||||
it("should show all columns in wide mode", () => {
|
||||
const data = [
|
||||
const data: Record<string, string>[] = [
|
||||
{
|
||||
_id: "1",
|
||||
name: "A",
|
||||
@@ -163,12 +164,12 @@ describe("OutputFormatter", () => {
|
||||
col7: "t",
|
||||
},
|
||||
];
|
||||
const result = formatOutput(data, "wide");
|
||||
const result: string = formatOutput(data, "wide");
|
||||
expect(result).toContain("col7");
|
||||
});
|
||||
|
||||
it("should limit columns in non-wide table mode", () => {
|
||||
const data = [
|
||||
const data: Record<string, string>[] = [
|
||||
{
|
||||
_id: "1",
|
||||
name: "A",
|
||||
@@ -181,13 +182,13 @@ describe("OutputFormatter", () => {
|
||||
col7: "t",
|
||||
},
|
||||
];
|
||||
const result = formatOutput(data, "table");
|
||||
const result: string = formatOutput(data, "table");
|
||||
// Table mode should limit to 6 columns, so col7 should not appear
|
||||
expect(result).not.toContain("col7");
|
||||
});
|
||||
|
||||
it("should prioritize common columns in non-wide mode", () => {
|
||||
const data = [
|
||||
const data: Record<string, string>[] = [
|
||||
{
|
||||
extra1: "a",
|
||||
extra2: "b",
|
||||
@@ -203,7 +204,7 @@ describe("OutputFormatter", () => {
|
||||
updatedAt: "2024-01-02",
|
||||
},
|
||||
];
|
||||
const result = formatOutput(data, "table");
|
||||
const result: string = formatOutput(data, "table");
|
||||
// Priority columns should appear
|
||||
expect(result).toContain("_id");
|
||||
expect(result).toContain("name");
|
||||
@@ -212,16 +213,18 @@ describe("OutputFormatter", () => {
|
||||
|
||||
describe("format auto-detection", () => {
|
||||
it("should default to JSON when not a TTY", () => {
|
||||
const originalIsTTY = process.stdout.isTTY;
|
||||
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: false,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const data = { id: "1" };
|
||||
const result = formatOutput(data);
|
||||
expect(() => JSON.parse(result)).not.toThrow();
|
||||
const data: Record<string, string> = { id: "1" };
|
||||
const result: string = formatOutput(data);
|
||||
expect(() => {
|
||||
return JSON.parse(result);
|
||||
}).not.toThrow();
|
||||
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: originalIsTTY,
|
||||
@@ -231,17 +234,17 @@ describe("OutputFormatter", () => {
|
||||
});
|
||||
|
||||
it("should default to table when TTY", () => {
|
||||
const originalIsTTY = process.stdout.isTTY;
|
||||
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: true,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const data = [{ _id: "1", name: "Test" }];
|
||||
const result = formatOutput(data);
|
||||
const data: Record<string, string>[] = [{ _id: "1", name: "Test" }];
|
||||
const result: string = formatOutput(data);
|
||||
// Table format contains box-drawing characters
|
||||
expect(result).toContain("─");
|
||||
expect(result).toContain("\u2500");
|
||||
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: originalIsTTY,
|
||||
@@ -251,17 +254,17 @@ describe("OutputFormatter", () => {
|
||||
});
|
||||
|
||||
it("should handle unknown format string and default to table via TTY check", () => {
|
||||
const data = [{ _id: "1" }];
|
||||
const data: Record<string, string>[] = [{ _id: "1" }];
|
||||
// "unknown" is not json/table/wide, so cliFormat falls through and TTY detection occurs
|
||||
const originalIsTTY = process.stdout.isTTY;
|
||||
const originalIsTTY: boolean | undefined = process.stdout.isTTY;
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: true,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const result = formatOutput(data, "unknown");
|
||||
expect(result).toContain("─");
|
||||
const result: string = formatOutput(data, "unknown");
|
||||
expect(result).toContain("\u2500");
|
||||
|
||||
Object.defineProperty(process.stdout, "isTTY", {
|
||||
value: originalIsTTY,
|
||||
@@ -274,23 +277,26 @@ describe("OutputFormatter", () => {
|
||||
describe("color handling", () => {
|
||||
it("should respect NO_COLOR env variable in table rendering", () => {
|
||||
process.env["NO_COLOR"] = "1";
|
||||
const data = [{ _id: "1", name: "A" }];
|
||||
const result = formatOutput(data, "table");
|
||||
const data: Record<string, string>[] = [{ _id: "1", name: "A" }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
// Should not contain ANSI color codes
|
||||
// eslint-disable-next-line no-control-regex
|
||||
expect(result).not.toMatch(/\x1b\[/);
|
||||
});
|
||||
|
||||
it("should respect --no-color argv flag in table rendering", () => {
|
||||
process.argv.push("--no-color");
|
||||
const data = [{ _id: "1", name: "A" }];
|
||||
const result = formatOutput(data, "table");
|
||||
const data: Record<string, string>[] = [{ _id: "1", name: "A" }];
|
||||
const result: string = formatOutput(data, "table");
|
||||
// eslint-disable-next-line no-control-regex
|
||||
expect(result).not.toMatch(/\x1b\[/);
|
||||
});
|
||||
|
||||
it("should render single object without color when NO_COLOR set", () => {
|
||||
process.env["NO_COLOR"] = "1";
|
||||
const data = { name: "Test" };
|
||||
const result = formatOutput(data, "table");
|
||||
const data: Record<string, string> = { name: "Test" };
|
||||
const result: string = formatOutput(data, "table");
|
||||
// eslint-disable-next-line no-control-regex
|
||||
expect(result).not.toMatch(/\x1b\[/);
|
||||
expect(result).toContain("name");
|
||||
});
|
||||
@@ -300,7 +306,9 @@ describe("OutputFormatter", () => {
|
||||
it("should log success message with color", () => {
|
||||
delete process.env["NO_COLOR"];
|
||||
// Remove --no-color from argv if present
|
||||
process.argv = process.argv.filter((a) => a !== "--no-color");
|
||||
process.argv = process.argv.filter((a: string) => {
|
||||
return a !== "--no-color";
|
||||
});
|
||||
printSuccess("OK");
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -315,7 +323,9 @@ describe("OutputFormatter", () => {
|
||||
describe("printError", () => {
|
||||
it("should log error message with color", () => {
|
||||
delete process.env["NO_COLOR"];
|
||||
process.argv = process.argv.filter((a) => a !== "--no-color");
|
||||
process.argv = process.argv.filter((a: string) => {
|
||||
return a !== "--no-color";
|
||||
});
|
||||
printError("fail");
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -330,7 +340,9 @@ describe("OutputFormatter", () => {
|
||||
describe("printWarning", () => {
|
||||
it("should log warning message with color", () => {
|
||||
delete process.env["NO_COLOR"];
|
||||
process.argv = process.argv.filter((a) => a !== "--no-color");
|
||||
process.argv = process.argv.filter((a: string) => {
|
||||
return a !== "--no-color";
|
||||
});
|
||||
printWarning("warn");
|
||||
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -345,7 +357,9 @@ describe("OutputFormatter", () => {
|
||||
describe("printInfo", () => {
|
||||
it("should log info message with color", () => {
|
||||
delete process.env["NO_COLOR"];
|
||||
process.argv = process.argv.filter((a) => a !== "--no-color");
|
||||
process.argv = process.argv.filter((a: string) => {
|
||||
return a !== "--no-color";
|
||||
});
|
||||
printInfo("info");
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -6,11 +6,15 @@ import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
// Mock the ApiClient module before it's imported by ResourceCommands
|
||||
const mockExecuteApiRequest = jest.fn();
|
||||
jest.mock("../Core/ApiClient", () => ({
|
||||
...jest.requireActual("../Core/ApiClient"),
|
||||
executeApiRequest: (...args) => mockExecuteApiRequest(...args),
|
||||
}));
|
||||
const mockExecuteApiRequest: jest.Mock = jest.fn();
|
||||
jest.mock("../Core/ApiClient", () => {
|
||||
return {
|
||||
...jest.requireActual("../Core/ApiClient"),
|
||||
executeApiRequest: (...args: unknown[]): unknown => {
|
||||
return mockExecuteApiRequest(...args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mock setup
|
||||
import {
|
||||
@@ -18,8 +22,8 @@ import {
|
||||
registerResourceCommands,
|
||||
} from "../Commands/ResourceCommands";
|
||||
|
||||
const CONFIG_DIR = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
describe("ResourceCommands", () => {
|
||||
let originalConfigContent: string | null = null;
|
||||
@@ -68,20 +72,32 @@ describe("ResourceCommands", () => {
|
||||
});
|
||||
|
||||
it("should discover the Incident resource", () => {
|
||||
const incident = resources.find((r) => r.singularName === "Incident");
|
||||
const incident: ResourceInfo | undefined = resources.find(
|
||||
(r: ResourceInfo) => {
|
||||
return r.singularName === "Incident";
|
||||
},
|
||||
);
|
||||
expect(incident).toBeDefined();
|
||||
expect(incident!.modelType).toBe("database");
|
||||
expect(incident!.apiPath).toBe("/incident");
|
||||
});
|
||||
|
||||
it("should discover the Monitor resource", () => {
|
||||
const monitor = resources.find((r) => r.singularName === "Monitor");
|
||||
const monitor: ResourceInfo | undefined = resources.find(
|
||||
(r: ResourceInfo) => {
|
||||
return r.singularName === "Monitor";
|
||||
},
|
||||
);
|
||||
expect(monitor).toBeDefined();
|
||||
expect(monitor!.modelType).toBe("database");
|
||||
});
|
||||
|
||||
it("should discover the Alert resource", () => {
|
||||
const alert = resources.find((r) => r.singularName === "Alert");
|
||||
const alert: ResourceInfo | undefined = resources.find(
|
||||
(r: ResourceInfo) => {
|
||||
return r.singularName === "Alert";
|
||||
},
|
||||
);
|
||||
expect(alert).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -107,28 +123,34 @@ describe("ResourceCommands", () => {
|
||||
|
||||
describe("registerResourceCommands", () => {
|
||||
it("should register commands for all discovered resources", () => {
|
||||
const program = new Command();
|
||||
const program: Command = new Command();
|
||||
program.exitOverride();
|
||||
registerResourceCommands(program);
|
||||
|
||||
const resources = discoverResources();
|
||||
const resources: ResourceInfo[] = discoverResources();
|
||||
for (const resource of resources) {
|
||||
const cmd = program.commands.find((c) => c.name() === resource.name);
|
||||
const cmd: Command | undefined = program.commands.find((c: Command) => {
|
||||
return c.name() === resource.name;
|
||||
});
|
||||
expect(cmd).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("should register list, get, create, update, delete, count subcommands for database resources", () => {
|
||||
const program = new Command();
|
||||
const program: Command = new Command();
|
||||
program.exitOverride();
|
||||
registerResourceCommands(program);
|
||||
|
||||
const incidentCmd = program.commands.find(
|
||||
(c) => c.name() === "incident",
|
||||
const incidentCmd: Command | undefined = program.commands.find(
|
||||
(c: Command) => {
|
||||
return c.name() === "incident";
|
||||
},
|
||||
);
|
||||
expect(incidentCmd).toBeDefined();
|
||||
|
||||
const subcommands = incidentCmd!.commands.map((c) => c.name());
|
||||
const subcommands: string[] = incidentCmd!.commands.map((c: Command) => {
|
||||
return c.name();
|
||||
});
|
||||
expect(subcommands).toContain("list");
|
||||
expect(subcommands).toContain("get");
|
||||
expect(subcommands).toContain("create");
|
||||
@@ -140,7 +162,7 @@ describe("ResourceCommands", () => {
|
||||
|
||||
describe("resource command actions", () => {
|
||||
function createProgramWithResources(): Command {
|
||||
const program = new Command();
|
||||
const program: Command = new Command();
|
||||
program.exitOverride();
|
||||
program.configureOutput({
|
||||
writeOut: () => {},
|
||||
@@ -165,16 +187,18 @@ describe("ResourceCommands", () => {
|
||||
|
||||
describe("list subcommand", () => {
|
||||
it("should call API with list operation", async () => {
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "list"]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].operation).toBe("list");
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].apiPath).toBe("/incident");
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].apiPath).toBe(
|
||||
"/incident",
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass query, limit, skip, sort options", async () => {
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -191,7 +215,8 @@ describe("ResourceCommands", () => {
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts = mockExecuteApiRequest.mock.calls[0][0];
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.query).toEqual({ status: "active" });
|
||||
expect(opts.limit).toBe(20);
|
||||
expect(opts.skip).toBe(5);
|
||||
@@ -203,7 +228,7 @@ describe("ResourceCommands", () => {
|
||||
data: [{ _id: "1", name: "Test" }],
|
||||
});
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -213,13 +238,14 @@ describe("ResourceCommands", () => {
|
||||
"json",
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle response that is already an array", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue([{ _id: "1" }]);
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -229,6 +255,7 @@ describe("ResourceCommands", () => {
|
||||
"json",
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -237,7 +264,7 @@ describe("ResourceCommands", () => {
|
||||
new Error("API error (500): Server Error"),
|
||||
);
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "list"]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
@@ -251,7 +278,7 @@ describe("ResourceCommands", () => {
|
||||
name: "Test",
|
||||
});
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -261,7 +288,8 @@ describe("ResourceCommands", () => {
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts = mockExecuteApiRequest.mock.calls[0][0];
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.operation).toBe("read");
|
||||
expect(opts.id).toBe("abc-123");
|
||||
});
|
||||
@@ -269,7 +297,7 @@ describe("ResourceCommands", () => {
|
||||
it("should support output format flag", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123" });
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -280,13 +308,14 @@ describe("ResourceCommands", () => {
|
||||
"json",
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle get errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(new Error("not found 404"));
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -303,7 +332,7 @@ describe("ResourceCommands", () => {
|
||||
it("should call API with create operation and data", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ _id: "new-123" });
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -314,7 +343,8 @@ describe("ResourceCommands", () => {
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts = mockExecuteApiRequest.mock.calls[0][0];
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.operation).toBe("create");
|
||||
expect(opts.data).toEqual({ name: "New Incident" });
|
||||
});
|
||||
@@ -322,14 +352,14 @@ describe("ResourceCommands", () => {
|
||||
it("should support reading data from a file", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ _id: "new-123" });
|
||||
|
||||
const tmpFile = path.join(
|
||||
const tmpFile: string = path.join(
|
||||
os.tmpdir(),
|
||||
"cli-test-" + Date.now() + ".json",
|
||||
);
|
||||
fs.writeFileSync(tmpFile, '{"name":"From File"}');
|
||||
|
||||
try {
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -349,14 +379,14 @@ describe("ResourceCommands", () => {
|
||||
});
|
||||
|
||||
it("should error when neither --data nor --file is provided", async () => {
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "create"]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error on invalid JSON in --data", async () => {
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -374,7 +404,7 @@ describe("ResourceCommands", () => {
|
||||
it("should call API with update operation, id, and data", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ _id: "abc-123" });
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -386,7 +416,8 @@ describe("ResourceCommands", () => {
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts = mockExecuteApiRequest.mock.calls[0][0];
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.operation).toBe("update");
|
||||
expect(opts.id).toBe("abc-123");
|
||||
expect(opts.data).toEqual({ name: "Updated" });
|
||||
@@ -395,7 +426,7 @@ describe("ResourceCommands", () => {
|
||||
it("should handle update errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(new Error("API error"));
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -414,7 +445,7 @@ describe("ResourceCommands", () => {
|
||||
it("should call API with delete operation and id", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({});
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -424,7 +455,8 @@ describe("ResourceCommands", () => {
|
||||
]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
const opts = mockExecuteApiRequest.mock.calls[0][0];
|
||||
const opts: Record<string, unknown> =
|
||||
mockExecuteApiRequest.mock.calls[0][0];
|
||||
expect(opts.operation).toBe("delete");
|
||||
expect(opts.id).toBe("abc-123");
|
||||
});
|
||||
@@ -432,7 +464,7 @@ describe("ResourceCommands", () => {
|
||||
it("should handle API errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(new Error("not found 404"));
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -449,18 +481,19 @@ describe("ResourceCommands", () => {
|
||||
it("should call API with count operation", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ count: 42 });
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "count"]);
|
||||
|
||||
expect(mockExecuteApiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteApiRequest.mock.calls[0][0].operation).toBe("count");
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalledWith(42);
|
||||
});
|
||||
|
||||
it("should pass query filter", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue({ count: 5 });
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -478,25 +511,27 @@ describe("ResourceCommands", () => {
|
||||
it("should handle response without count field", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue(99);
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "count"]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalledWith(99);
|
||||
});
|
||||
|
||||
it("should handle non-object response in count", async () => {
|
||||
mockExecuteApiRequest.mockResolvedValue("some-string");
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "count"]);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
expect(console.log).toHaveBeenCalledWith("some-string");
|
||||
});
|
||||
|
||||
it("should handle count errors", async () => {
|
||||
mockExecuteApiRequest.mockRejectedValue(new Error("API error"));
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync(["node", "test", "incident", "count"]);
|
||||
|
||||
expect(process.exit).toHaveBeenCalled();
|
||||
@@ -508,7 +543,7 @@ describe("ResourceCommands", () => {
|
||||
ConfigManager.removeContext("test");
|
||||
mockExecuteApiRequest.mockResolvedValue({ data: [] });
|
||||
|
||||
const program = createProgramWithResources();
|
||||
const program: Command = createProgramWithResources();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import { generateAllFieldsSelect } from "../Utils/SelectFieldGenerator";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
describe("SelectFieldGenerator", () => {
|
||||
describe("generateAllFieldsSelect", () => {
|
||||
describe("database models", () => {
|
||||
it("should return fields for a known database model (Incident)", () => {
|
||||
const select = generateAllFieldsSelect("Incident", "database");
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"database",
|
||||
);
|
||||
expect(Object.keys(select).length).toBeGreaterThan(0);
|
||||
// Should have some common fields
|
||||
expect(select).toHaveProperty("_id");
|
||||
});
|
||||
|
||||
it("should return fields for Monitor model", () => {
|
||||
const select = generateAllFieldsSelect("Monitor", "database");
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Monitor",
|
||||
"database",
|
||||
);
|
||||
expect(Object.keys(select).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should return default select for unknown database model", () => {
|
||||
const select = generateAllFieldsSelect(
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"NonExistentModel12345",
|
||||
"database",
|
||||
);
|
||||
@@ -29,7 +36,10 @@ describe("SelectFieldGenerator", () => {
|
||||
|
||||
it("should filter fields based on access control", () => {
|
||||
// Testing with a real model that has access control
|
||||
const select = generateAllFieldsSelect("Incident", "database");
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"database",
|
||||
);
|
||||
// We just verify it returns something reasonable
|
||||
expect(typeof select).toBe("object");
|
||||
expect(Object.keys(select).length).toBeGreaterThan(0);
|
||||
@@ -39,7 +49,10 @@ describe("SelectFieldGenerator", () => {
|
||||
describe("analytics models", () => {
|
||||
it("should return default select for known analytics model (LogItem)", () => {
|
||||
// The Log analytics model has tableName "LogItem"
|
||||
const select = generateAllFieldsSelect("LogItem", "analytics");
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"LogItem",
|
||||
"analytics",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
@@ -48,7 +61,7 @@ describe("SelectFieldGenerator", () => {
|
||||
});
|
||||
|
||||
it("should return default select for unknown analytics model", () => {
|
||||
const select = generateAllFieldsSelect(
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"NonExistentAnalytics",
|
||||
"analytics",
|
||||
);
|
||||
@@ -62,7 +75,7 @@ describe("SelectFieldGenerator", () => {
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should return default select for unknown model type", () => {
|
||||
const select = generateAllFieldsSelect(
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"unknown" as any,
|
||||
);
|
||||
@@ -74,7 +87,7 @@ describe("SelectFieldGenerator", () => {
|
||||
});
|
||||
|
||||
it("should return default select for empty tableName", () => {
|
||||
const select = generateAllFieldsSelect("", "database");
|
||||
const select: JSONObject = generateAllFieldsSelect("", "database");
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
@@ -83,14 +96,20 @@ describe("SelectFieldGenerator", () => {
|
||||
});
|
||||
|
||||
it("should handle outer exception and return default select", () => {
|
||||
const DatabaseModels = require("Common/Models/DatabaseModels/Index").default;
|
||||
const origFind = DatabaseModels.find;
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const DatabaseModels: Record<string, unknown> =
|
||||
require("Common/Models/DatabaseModels/Index").default;
|
||||
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const origFind: unknown = DatabaseModels.find;
|
||||
try {
|
||||
DatabaseModels.find = () => {
|
||||
DatabaseModels.find = (): never => {
|
||||
throw new Error("Simulated error");
|
||||
};
|
||||
|
||||
const select = generateAllFieldsSelect("Incident", "database");
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"database",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
@@ -102,12 +121,22 @@ describe("SelectFieldGenerator", () => {
|
||||
});
|
||||
|
||||
it("should return default when getTableColumns returns empty", () => {
|
||||
const tableColumnModule = require("Common/Types/Database/TableColumn");
|
||||
const origGetTableColumns = tableColumnModule.getTableColumns;
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const tableColumnModule: Record<
|
||||
string,
|
||||
unknown
|
||||
> = require("Common/Types/Database/TableColumn");
|
||||
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const origGetTableColumns: unknown = tableColumnModule.getTableColumns;
|
||||
try {
|
||||
tableColumnModule.getTableColumns = () => ({});
|
||||
tableColumnModule.getTableColumns = (): Record<string, unknown> => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const select = generateAllFieldsSelect("Incident", "database");
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"Incident",
|
||||
"database",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
@@ -119,29 +148,51 @@ describe("SelectFieldGenerator", () => {
|
||||
});
|
||||
|
||||
it("should return default when all columns are filtered out", () => {
|
||||
const tableColumnModule = require("Common/Types/Database/TableColumn");
|
||||
const origGetTableColumns = tableColumnModule.getTableColumns;
|
||||
const DatabaseModels = require("Common/Models/DatabaseModels/Index").default;
|
||||
const origFind = DatabaseModels.find;
|
||||
const Permission = require("Common/Types/Permission").default;
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
const tableColumnModule: Record<
|
||||
string,
|
||||
unknown
|
||||
> = require("Common/Types/Database/TableColumn");
|
||||
const origGetTableColumns: unknown = tableColumnModule.getTableColumns;
|
||||
const DatabaseModels: Record<string, unknown> =
|
||||
require("Common/Models/DatabaseModels/Index").default;
|
||||
const origFind: unknown = DatabaseModels.find;
|
||||
const Permission: Record<string, unknown> =
|
||||
require("Common/Types/Permission").default;
|
||||
/* eslint-enable @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires */
|
||||
|
||||
try {
|
||||
tableColumnModule.getTableColumns = () => ({ field1: {}, field2: {} });
|
||||
tableColumnModule.getTableColumns = (): Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
> => {
|
||||
return { field1: {}, field2: {} };
|
||||
};
|
||||
|
||||
DatabaseModels.find = (fn) => {
|
||||
function MockModel() {
|
||||
DatabaseModels.find = (fn: (model: unknown) => boolean): unknown => {
|
||||
function MockModel(this: Record<string, unknown>): void {
|
||||
this.tableName = "MockTable";
|
||||
this.getColumnAccessControlForAllColumns = () => ({
|
||||
field1: { read: [Permission.CurrentUser] },
|
||||
field2: { read: [Permission.CurrentUser] },
|
||||
});
|
||||
this.getColumnAccessControlForAllColumns = (): Record<
|
||||
string,
|
||||
unknown
|
||||
> => {
|
||||
return {
|
||||
field1: { read: [Permission.CurrentUser] },
|
||||
field2: { read: [Permission.CurrentUser] },
|
||||
};
|
||||
};
|
||||
}
|
||||
const matches: boolean = fn(MockModel);
|
||||
if (matches) {
|
||||
return MockModel;
|
||||
}
|
||||
const matches = fn(MockModel);
|
||||
if (matches) return MockModel;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const select = generateAllFieldsSelect("MockTable", "database");
|
||||
const select: JSONObject = generateAllFieldsSelect(
|
||||
"MockTable",
|
||||
"database",
|
||||
);
|
||||
expect(select).toEqual({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Command } from "commander";
|
||||
import { registerUtilityCommands } from "../Commands/UtilityCommands";
|
||||
import { registerResourceCommands } from "../Commands/ResourceCommands";
|
||||
import * as ConfigManager from "../Core/ConfigManager";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
const CONFIG_DIR = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
||||
const CONFIG_DIR: string = path.join(os.homedir(), ".oneuptime");
|
||||
const CONFIG_FILE: string = path.join(CONFIG_DIR, "config.json");
|
||||
|
||||
describe("UtilityCommands", () => {
|
||||
let originalConfigContent: string | null = null;
|
||||
@@ -46,7 +45,7 @@ describe("UtilityCommands", () => {
|
||||
});
|
||||
|
||||
function createProgram(): Command {
|
||||
const program = new Command();
|
||||
const program: Command = new Command();
|
||||
program.exitOverride();
|
||||
program.configureOutput({
|
||||
writeOut: () => {},
|
||||
@@ -62,18 +61,18 @@ describe("UtilityCommands", () => {
|
||||
|
||||
describe("version command", () => {
|
||||
it("should print version", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "version"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
// Should print a version string (either from package.json or fallback)
|
||||
const versionArg = consoleLogSpy.mock.calls[0][0];
|
||||
const versionArg: string = consoleLogSpy.mock.calls[0][0];
|
||||
expect(typeof versionArg).toBe("string");
|
||||
});
|
||||
});
|
||||
|
||||
describe("whoami command", () => {
|
||||
it("should show not authenticated when no credentials", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
@@ -85,7 +84,7 @@ describe("UtilityCommands", () => {
|
||||
apiKey: "abcdefghijklm",
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("URL: https://test.com");
|
||||
@@ -102,7 +101,7 @@ describe("UtilityCommands", () => {
|
||||
apiKey: "abc",
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("API Key: ****");
|
||||
@@ -112,7 +111,7 @@ describe("UtilityCommands", () => {
|
||||
process.env["ONEUPTIME_API_KEY"] = "env-key-long-enough";
|
||||
process.env["ONEUPTIME_URL"] = "https://env.com";
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("URL: https://env.com");
|
||||
@@ -120,13 +119,13 @@ describe("UtilityCommands", () => {
|
||||
|
||||
it("should handle whoami outer catch block", async () => {
|
||||
// Mock getCurrentContext to throw an unexpected error
|
||||
const spy = jest
|
||||
const spy: jest.SpyInstance = jest
|
||||
.spyOn(ConfigManager, "getCurrentContext")
|
||||
.mockImplementation(() => {
|
||||
throw new Error("Unexpected crash");
|
||||
});
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
@@ -137,12 +136,14 @@ describe("UtilityCommands", () => {
|
||||
process.env["ONEUPTIME_API_KEY"] = "env-key-long-enough";
|
||||
process.env["ONEUPTIME_URL"] = "https://env.com";
|
||||
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "whoami"]);
|
||||
|
||||
// Should NOT have a "Context:" call since no context is set
|
||||
const contextCalls = consoleLogSpy.mock.calls.filter(
|
||||
(call: any[]) => typeof call[0] === "string" && call[0].startsWith("Context:"),
|
||||
const contextCalls: any[][] = consoleLogSpy.mock.calls.filter(
|
||||
(call: any[]) => {
|
||||
return typeof call[0] === "string" && call[0].startsWith("Context:");
|
||||
},
|
||||
);
|
||||
expect(contextCalls).toHaveLength(0);
|
||||
});
|
||||
@@ -150,20 +151,22 @@ describe("UtilityCommands", () => {
|
||||
|
||||
describe("resources command", () => {
|
||||
it("should list all resources", async () => {
|
||||
// We need registerResourceCommands for discoverResources to work
|
||||
// but discoverResources is imported directly, so it should work
|
||||
const program = createProgram();
|
||||
/*
|
||||
* We need registerResourceCommands for discoverResources to work
|
||||
* but discoverResources is imported directly, so it should work
|
||||
*/
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync(["node", "test", "resources"]);
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
// Should show total count
|
||||
const lastCall =
|
||||
const lastCall: string =
|
||||
consoleLogSpy.mock.calls[consoleLogSpy.mock.calls.length - 1][0];
|
||||
expect(lastCall).toContain("Total:");
|
||||
});
|
||||
|
||||
it("should filter by type", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
@@ -176,7 +179,7 @@ describe("UtilityCommands", () => {
|
||||
});
|
||||
|
||||
it("should show message when filter returns no results", async () => {
|
||||
const program = createProgram();
|
||||
const program: Command = createProgram();
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"test",
|
||||
|
||||
@@ -34,9 +34,7 @@ export function generateAllFieldsSelect(
|
||||
): JSONObject {
|
||||
try {
|
||||
if (modelType === "database") {
|
||||
const ModelClass:
|
||||
| (new () => BaseModel)
|
||||
| undefined = DatabaseModels.find(
|
||||
const ModelClass: (new () => BaseModel) | undefined = DatabaseModels.find(
|
||||
(Model: new () => BaseModel): boolean => {
|
||||
try {
|
||||
const instance: BaseModel = new Model();
|
||||
@@ -85,18 +83,15 @@ export function generateAllFieldsSelect(
|
||||
}
|
||||
|
||||
if (modelType === "analytics") {
|
||||
const ModelClass:
|
||||
| (new () => AnalyticsBaseModel)
|
||||
| undefined = AnalyticsModels.find(
|
||||
(Model: new () => AnalyticsBaseModel): boolean => {
|
||||
const ModelClass: (new () => AnalyticsBaseModel) | undefined =
|
||||
AnalyticsModels.find((Model: new () => AnalyticsBaseModel): boolean => {
|
||||
try {
|
||||
const instance: AnalyticsBaseModel = new Model();
|
||||
return instance.tableName === tableName;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!ModelClass) {
|
||||
return getDefaultSelect();
|
||||
|
||||
@@ -3,7 +3,9 @@ import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const DisabledMonitors: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const DisabledMonitors: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<MonitorTable
|
||||
query={{
|
||||
|
||||
@@ -3,7 +3,9 @@ import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const NotOperationalMonitors: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const NotOperationalMonitors: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<MonitorTable
|
||||
query={{
|
||||
|
||||
@@ -3,7 +3,9 @@ import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const DisabledMonitors: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const DisabledMonitors: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<MonitorTable
|
||||
query={{
|
||||
|
||||
@@ -3,7 +3,9 @@ import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const DisabledMonitors: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const DisabledMonitors: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<MonitorTable
|
||||
query={{
|
||||
|
||||
@@ -2,7 +2,9 @@ import IncomingCallNumber from "../../Components/NotificationMethods/IncomingCal
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const IncomingCallPhoneNumbers: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const IncomingCallPhoneNumbers: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return <IncomingCallNumber />;
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ type OnCallDutyPolicy = InstanceType<typeof OnCallDutyPolicyModel>;
|
||||
type OnCallDutyPolicyEscalationRule = InstanceType<
|
||||
typeof OnCallDutyPolicyEscalationRuleModel
|
||||
>;
|
||||
type OnCallDutyPolicySchedule = InstanceType<typeof OnCallDutyPolicyScheduleModel>;
|
||||
type OnCallDutyPolicySchedule = InstanceType<
|
||||
typeof OnCallDutyPolicyScheduleModel
|
||||
>;
|
||||
type Project = InstanceType<typeof ProjectModel>;
|
||||
type Team = InstanceType<typeof TeamModel>;
|
||||
type User = InstanceType<typeof UserModel>;
|
||||
@@ -90,7 +92,12 @@ type IncidentItemFromCommon = RequiredModelFields<
|
||||
export interface IncidentItem
|
||||
extends Omit<
|
||||
IncidentItemFromCommon,
|
||||
"declaredAt" | "createdAt" | "currentIncidentState" | "incidentSeverity" | "monitors" | "projectId"
|
||||
| "declaredAt"
|
||||
| "createdAt"
|
||||
| "currentIncidentState"
|
||||
| "incidentSeverity"
|
||||
| "monitors"
|
||||
| "projectId"
|
||||
> {
|
||||
rootCause?: string;
|
||||
declaredAt: string;
|
||||
@@ -115,7 +122,11 @@ type AlertItemFromCommon = RequiredModelFields<
|
||||
export interface AlertItem
|
||||
extends Omit<
|
||||
AlertItemFromCommon,
|
||||
"createdAt" | "currentAlertState" | "alertSeverity" | "monitor" | "projectId"
|
||||
| "createdAt"
|
||||
| "currentAlertState"
|
||||
| "alertSeverity"
|
||||
| "monitor"
|
||||
| "projectId"
|
||||
> {
|
||||
rootCause?: string;
|
||||
createdAt: string;
|
||||
@@ -129,7 +140,12 @@ export interface AlertItem
|
||||
export interface IncidentState
|
||||
extends RequiredModelFields<
|
||||
IncidentStateModel,
|
||||
"_id" | "name" | "isResolvedState" | "isAcknowledgedState" | "isCreatedState" | "order"
|
||||
| "_id"
|
||||
| "name"
|
||||
| "isResolvedState"
|
||||
| "isAcknowledgedState"
|
||||
| "isCreatedState"
|
||||
| "order"
|
||||
> {
|
||||
color: ColorField;
|
||||
}
|
||||
@@ -137,7 +153,12 @@ export interface IncidentState
|
||||
export interface AlertState
|
||||
extends RequiredModelFields<
|
||||
AlertStateModel,
|
||||
"_id" | "name" | "isResolvedState" | "isAcknowledgedState" | "isCreatedState" | "order"
|
||||
| "_id"
|
||||
| "name"
|
||||
| "isResolvedState"
|
||||
| "isAcknowledgedState"
|
||||
| "isCreatedState"
|
||||
| "order"
|
||||
> {
|
||||
color: ColorField;
|
||||
}
|
||||
@@ -170,7 +191,11 @@ type IncidentEpisodeItemFromCommon = RequiredModelFields<
|
||||
export interface IncidentEpisodeItem
|
||||
extends Omit<
|
||||
IncidentEpisodeItemFromCommon,
|
||||
"createdAt" | "declaredAt" | "currentIncidentState" | "incidentSeverity" | "projectId"
|
||||
| "createdAt"
|
||||
| "declaredAt"
|
||||
| "currentIncidentState"
|
||||
| "incidentSeverity"
|
||||
| "projectId"
|
||||
> {
|
||||
rootCause?: string;
|
||||
createdAt: string;
|
||||
@@ -212,7 +237,8 @@ type NoteItemFromCommon = RequiredModelFields<
|
||||
"_id" | "note" | "createdAt"
|
||||
>;
|
||||
|
||||
export interface NoteItem extends Omit<NoteItemFromCommon, "createdAt" | "createdByUser"> {
|
||||
export interface NoteItem
|
||||
extends Omit<NoteItemFromCommon, "createdAt" | "createdByUser"> {
|
||||
createdAt: string;
|
||||
createdByUser:
|
||||
| (RequiredModelFields<User, "_id" | "name"> & {
|
||||
@@ -222,9 +248,13 @@ export interface NoteItem extends Omit<NoteItemFromCommon, "createdAt" | "create
|
||||
| null;
|
||||
}
|
||||
|
||||
type FeedItemFromCommon = RequiredModelFields<AlertFeed | IncidentFeed, "_id" | "feedInfoInMarkdown" | "createdAt">;
|
||||
type FeedItemFromCommon = RequiredModelFields<
|
||||
AlertFeed | IncidentFeed,
|
||||
"_id" | "feedInfoInMarkdown" | "createdAt"
|
||||
>;
|
||||
|
||||
export interface FeedItem extends Omit<FeedItemFromCommon, "createdAt" | "displayColor" | "postedAt"> {
|
||||
export interface FeedItem
|
||||
extends Omit<FeedItemFromCommon, "createdAt" | "displayColor" | "postedAt"> {
|
||||
feedInfoInMarkdown: string;
|
||||
moreInformationInMarkdown?: string;
|
||||
displayColor: ColorField;
|
||||
|
||||
@@ -79,7 +79,10 @@ export default function AlertCard({
|
||||
/>
|
||||
<Text
|
||||
className="text-[10px] font-semibold"
|
||||
style={{ color: theme.colors.textSecondary, letterSpacing: 0.3 }}
|
||||
style={{
|
||||
color: theme.colors.textSecondary,
|
||||
letterSpacing: 0.3,
|
||||
}}
|
||||
>
|
||||
ALERT
|
||||
</Text>
|
||||
|
||||
@@ -111,7 +111,10 @@ export default function EpisodeCard(
|
||||
/>
|
||||
<Text
|
||||
className="text-[10px] font-semibold"
|
||||
style={{ color: theme.colors.textSecondary, letterSpacing: 0.3 }}
|
||||
style={{
|
||||
color: theme.colors.textSecondary,
|
||||
letterSpacing: 0.3,
|
||||
}}
|
||||
>
|
||||
{type === "incident" ? "INCIDENT EPISODE" : "ALERT EPISODE"}
|
||||
</Text>
|
||||
@@ -131,7 +134,8 @@ export default function EpisodeCard(
|
||||
letterSpacing: 0.2,
|
||||
}}
|
||||
>
|
||||
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
|
||||
{episode.episodeNumberWithPrefix ||
|
||||
`#${episode.episodeNumber}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -87,7 +87,10 @@ export default function IncidentCard({
|
||||
/>
|
||||
<Text
|
||||
className="text-[10px] font-semibold"
|
||||
style={{ color: theme.colors.textSecondary, letterSpacing: 0.3 }}
|
||||
style={{
|
||||
color: theme.colors.textSecondary,
|
||||
letterSpacing: 0.3,
|
||||
}}
|
||||
>
|
||||
INCIDENT
|
||||
</Text>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -96,4 +96,4 @@ export default function MarkdownContent({
|
||||
{markdownText}
|
||||
</Markdown>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ export default function NotesSection({
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.borderGlass,
|
||||
shadowColor: theme.isDark ? "#000" : theme.colors.accentGradientMid,
|
||||
shadowColor: theme.isDark
|
||||
? "#000"
|
||||
: theme.colors.accentGradientMid,
|
||||
shadowOpacity: theme.isDark ? 0.16 : 0.06,
|
||||
shadowOffset: { width: 0, height: 5 },
|
||||
shadowRadius: 10,
|
||||
|
||||
@@ -35,4 +35,4 @@ export default function RootCauseCard({
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,11 @@ export default function SectionHeader({
|
||||
borderColor: theme.colors.borderGlass,
|
||||
}}
|
||||
>
|
||||
<Ionicons name={iconName} size={13} color={theme.colors.actionPrimary} />
|
||||
<Ionicons
|
||||
name={iconName}
|
||||
size={13}
|
||||
color={theme.colors.actionPrimary}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
className="text-[12px] font-semibold uppercase"
|
||||
|
||||
@@ -58,7 +58,9 @@ export default function SegmentedControl<T extends string>({
|
||||
<Text
|
||||
className="text-body-sm font-semibold"
|
||||
style={{
|
||||
color: isActive ? activeContentColor : theme.colors.textSecondary,
|
||||
color: isActive
|
||||
? activeContentColor
|
||||
: theme.colors.textSecondary,
|
||||
letterSpacing: 0.2,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -17,7 +17,10 @@ interface UseAllProjectOnCallPoliciesResult {
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
function getEntityId(entity?: { _id?: string; id?: string }): string | undefined {
|
||||
function getEntityId(entity?: {
|
||||
_id?: string;
|
||||
id?: string;
|
||||
}): string | undefined {
|
||||
return entity?._id ?? entity?.id;
|
||||
}
|
||||
|
||||
@@ -33,7 +36,8 @@ function toAssignments(
|
||||
projectName: project.name,
|
||||
policyId: getEntityId(rule.onCallDutyPolicy),
|
||||
policyName: rule.onCallDutyPolicy?.name ?? "Unknown policy",
|
||||
escalationRuleName: rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
|
||||
escalationRuleName:
|
||||
rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
|
||||
assignmentType: "user",
|
||||
assignmentDetail: "You are directly assigned",
|
||||
});
|
||||
@@ -45,7 +49,8 @@ function toAssignments(
|
||||
projectName: project.name,
|
||||
policyId: getEntityId(rule.onCallDutyPolicy),
|
||||
policyName: rule.onCallDutyPolicy?.name ?? "Unknown policy",
|
||||
escalationRuleName: rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
|
||||
escalationRuleName:
|
||||
rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
|
||||
assignmentType: "team",
|
||||
assignmentDetail: `Via team: ${rule.team?.name ?? "Unknown"}`,
|
||||
});
|
||||
@@ -57,7 +62,8 @@ function toAssignments(
|
||||
projectName: project.name,
|
||||
policyId: getEntityId(rule.onCallDutyPolicy),
|
||||
policyName: rule.onCallDutyPolicy?.name ?? "Unknown policy",
|
||||
escalationRuleName: rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
|
||||
escalationRuleName:
|
||||
rule.onCallDutyPolicyEscalationRule?.name ?? "Unknown rule",
|
||||
assignmentType: "schedule",
|
||||
assignmentDetail: `Via schedule: ${rule.onCallDutyPolicySchedule?.name ?? "Unknown"}`,
|
||||
});
|
||||
@@ -107,15 +113,19 @@ export function useAllProjectOnCallPolicies(): UseAllProjectOnCallPoliciesResult
|
||||
|
||||
const projects: ProjectOnCallAssignments[] = [];
|
||||
|
||||
results.forEach((result: PromiseSettledResult<ProjectOnCallAssignments | null>) => {
|
||||
if (result.status === "fulfilled" && result.value) {
|
||||
projects.push(result.value);
|
||||
}
|
||||
});
|
||||
results.forEach(
|
||||
(result: PromiseSettledResult<ProjectOnCallAssignments | null>) => {
|
||||
if (result.status === "fulfilled" && result.value) {
|
||||
projects.push(result.value);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return projects.sort((a: ProjectOnCallAssignments, b: ProjectOnCallAssignments) => {
|
||||
return a.projectName.localeCompare(b.projectName);
|
||||
});
|
||||
return projects.sort(
|
||||
(a: ProjectOnCallAssignments, b: ProjectOnCallAssignments) => {
|
||||
return a.projectName.localeCompare(b.projectName);
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -74,7 +74,9 @@ export default function MainTabNavigator(): React.JSX.Element {
|
||||
height: Platform.OS === "ios" ? 78 : 68,
|
||||
paddingBottom: Platform.OS === "ios" ? 18 : 10,
|
||||
paddingTop: 10,
|
||||
shadowColor: theme.isDark ? "#000000" : theme.colors.accentGradientMid,
|
||||
shadowColor: theme.isDark
|
||||
? "#000000"
|
||||
: theme.colors.accentGradientMid,
|
||||
shadowOpacity: theme.isDark ? 0.35 : 0.12,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowRadius: 18,
|
||||
|
||||
@@ -5,8 +5,9 @@ import { useTheme } from "../theme";
|
||||
import MyOnCallPoliciesScreen from "../screens/MyOnCallPoliciesScreen";
|
||||
import type { OnCallStackParamList } from "./types";
|
||||
|
||||
const Stack: ReturnType<typeof createNativeStackNavigator<OnCallStackParamList>> =
|
||||
createNativeStackNavigator<OnCallStackParamList>();
|
||||
const Stack: ReturnType<
|
||||
typeof createNativeStackNavigator<OnCallStackParamList>
|
||||
> = createNativeStackNavigator<OnCallStackParamList>();
|
||||
|
||||
export default function OnCallStackNavigator(): React.JSX.Element {
|
||||
const { theme } = useTheme();
|
||||
|
||||
@@ -185,7 +185,8 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
const isResolved: boolean = resolveState?._id === currentStateId;
|
||||
const isAcknowledged: boolean = acknowledgeState?._id === currentStateId;
|
||||
const rootCauseTextRaw: string = toPlainText(alert.rootCause);
|
||||
const rootCauseText: string | undefined = rootCauseTextRaw.trim() || undefined;
|
||||
const rootCauseText: string | undefined =
|
||||
rootCauseTextRaw.trim() || undefined;
|
||||
const descriptionText: string = toPlainText(alert.description);
|
||||
|
||||
return (
|
||||
@@ -373,78 +374,81 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
|
||||
}}
|
||||
>
|
||||
<View className="flex-row gap-3">
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateAcknowledged,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
acknowledgeState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Acknowledge alert"
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Acknowledge
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateAcknowledged,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
acknowledgeState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Acknowledge alert"
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Acknowledge
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateResolved,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(resolveState._id, resolveState.name);
|
||||
}}
|
||||
disabled={changingState}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Resolve alert"
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-done-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Resolve
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateResolved,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
resolveState._id,
|
||||
resolveState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Resolve alert"
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-done-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Resolve
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -193,7 +193,8 @@ export default function AlertEpisodeDetailScreen({
|
||||
const isResolved: boolean = resolveState?._id === currentStateId;
|
||||
const isAcknowledged: boolean = acknowledgeState?._id === currentStateId;
|
||||
const rootCauseTextRaw: string = toPlainText(episode.rootCause);
|
||||
const rootCauseText: string | undefined = rootCauseTextRaw.trim() || undefined;
|
||||
const rootCauseText: string | undefined =
|
||||
rootCauseTextRaw.trim() || undefined;
|
||||
const descriptionText: string = toPlainText(episode.description);
|
||||
|
||||
return (
|
||||
@@ -381,71 +382,74 @@ export default function AlertEpisodeDetailScreen({
|
||||
}}
|
||||
>
|
||||
<View className="flex-row gap-3">
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateAcknowledged,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
acknowledgeState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Acknowledge
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateResolved,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(resolveState._id, resolveState.name);
|
||||
}}
|
||||
disabled={changingState}
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-done-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Resolve
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateAcknowledged,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
acknowledgeState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Acknowledge
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateResolved,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
resolveState._id,
|
||||
resolveState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-done-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Resolve
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -56,7 +56,10 @@ function StatCard({
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[theme.colors.accentGradientStart + "2B", theme.colors.accentGradientEnd + "1A"]}
|
||||
colors={[
|
||||
theme.colors.accentGradientStart + "2B",
|
||||
theme.colors.accentGradientEnd + "1A",
|
||||
]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={{
|
||||
@@ -353,7 +356,9 @@ export default function HomeScreen(): React.JSX.Element {
|
||||
backgroundColor: theme.colors.backgroundElevated,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.borderGlass,
|
||||
shadowColor: theme.isDark ? "#000" : theme.colors.accentGradientMid,
|
||||
shadowColor: theme.isDark
|
||||
? "#000"
|
||||
: theme.colors.accentGradientMid,
|
||||
shadowOpacity: theme.isDark ? 0.24 : 0.09,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowRadius: 14,
|
||||
|
||||
@@ -199,7 +199,8 @@ export default function IncidentDetailScreen({
|
||||
const isResolved: boolean = resolveState?._id === currentStateId;
|
||||
const isAcknowledged: boolean = acknowledgeState?._id === currentStateId;
|
||||
const rootCauseTextRaw: string = toPlainText(incident.rootCause);
|
||||
const rootCauseText: string | undefined = rootCauseTextRaw.trim() || undefined;
|
||||
const rootCauseText: string | undefined =
|
||||
rootCauseTextRaw.trim() || undefined;
|
||||
const descriptionText: string = toPlainText(incident.description);
|
||||
|
||||
return (
|
||||
@@ -408,78 +409,81 @@ export default function IncidentDetailScreen({
|
||||
}}
|
||||
>
|
||||
<View className="flex-row gap-3">
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateAcknowledged,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
acknowledgeState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Acknowledge incident"
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Acknowledge
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateAcknowledged,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
acknowledgeState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Acknowledge incident"
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Acknowledge
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateResolved,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(resolveState._id, resolveState.name);
|
||||
}}
|
||||
disabled={changingState}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Resolve incident"
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-done-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Resolve
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateResolved,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
resolveState._id,
|
||||
resolveState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
activeOpacity={0.85}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Resolve incident"
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-done-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Resolve
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -202,7 +202,8 @@ export default function IncidentEpisodeDetailScreen({
|
||||
const isResolved: boolean = resolveState?._id === currentStateId;
|
||||
const isAcknowledged: boolean = acknowledgeState?._id === currentStateId;
|
||||
const rootCauseTextRaw: string = toPlainText(episode.rootCause);
|
||||
const rootCauseText: string | undefined = rootCauseTextRaw.trim() || undefined;
|
||||
const rootCauseText: string | undefined =
|
||||
rootCauseTextRaw.trim() || undefined;
|
||||
const descriptionText: string = toPlainText(episode.description);
|
||||
|
||||
return (
|
||||
@@ -388,71 +389,74 @@ export default function IncidentEpisodeDetailScreen({
|
||||
}}
|
||||
>
|
||||
<View className="flex-row gap-3">
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateAcknowledged,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
acknowledgeState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Acknowledge
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateResolved,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(resolveState._id, resolveState.name);
|
||||
}}
|
||||
disabled={changingState}
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-done-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Resolve
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{!isAcknowledged && !isResolved && acknowledgeState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateAcknowledged,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
acknowledgeState._id,
|
||||
acknowledgeState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-circle-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Acknowledge
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
{resolveState ? (
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px] overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: theme.colors.stateResolved,
|
||||
}}
|
||||
onPress={() => {
|
||||
return handleStateChange(
|
||||
resolveState._id,
|
||||
resolveState.name,
|
||||
);
|
||||
}}
|
||||
disabled={changingState}
|
||||
>
|
||||
{changingState ? (
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
) : (
|
||||
<>
|
||||
<Ionicons
|
||||
name="checkmark-done-outline"
|
||||
size={17}
|
||||
color="#FFFFFF"
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
<Text
|
||||
className="text-[14px] font-bold"
|
||||
style={{ color: "#FFFFFF" }}
|
||||
>
|
||||
Resolve
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user