Compare commits
1 Commits
feat/cli-a
...
feat/pat-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79a6e8e1c6 |
@@ -3,7 +3,7 @@ import { ExecException } from "child_process";
|
||||
import { HoppErrorCode } from "../../types/errors";
|
||||
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
|
||||
|
||||
describe("Test `hopp test <file_path_or_id>` command:", () => {
|
||||
describe("Test `hopp test <file>` command:", () => {
|
||||
describe("Argument parsing", () => {
|
||||
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
|
||||
const args = "test";
|
||||
@@ -131,7 +131,7 @@ describe("Test `hopp test <file_path_or_id>` command:", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:", () => {
|
||||
describe("Test `hopp test <file> --env <file>` command:", () => {
|
||||
describe("Supplied environment export file validations", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
|
||||
@@ -310,7 +310,7 @@ describe("Test `hopp test <file_path_or_id> --env <file_path_or_id>` command:",
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test `hopp test <file_path_or_id> --delay <delay_in_ms>` command:", () => {
|
||||
describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
|
||||
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
|
||||
|
||||
test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
|
||||
@@ -343,116 +343,3 @@ describe("Test `hopp test <file_path_or_id> --delay <delay_in_ms>` command:", ()
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip("Test `hopp test <file_path_or_id> --env <file_path_or_id> --token <access_token> --server <server_url>` command:", () => {
|
||||
const {
|
||||
REQ_BODY_ENV_VARS_COLL_ID,
|
||||
COLLECTION_LEVEL_HEADERS_AUTH_COLL_ID,
|
||||
REQ_BODY_ENV_VARS_ENVS_ID,
|
||||
PERSONAL_ACCESS_TOKEN,
|
||||
} = process.env;
|
||||
|
||||
if (
|
||||
!REQ_BODY_ENV_VARS_COLL_ID ||
|
||||
!COLLECTION_LEVEL_HEADERS_AUTH_COLL_ID ||
|
||||
!REQ_BODY_ENV_VARS_ENVS_ID ||
|
||||
!PERSONAL_ACCESS_TOKEN
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SERVER_URL = "https://stage-shc.hoppscotch.io/backend";
|
||||
|
||||
describe("Validations", () => {
|
||||
test("Errors with the code `INVALID_ARGUMENT` on not supplying a value for the `--token` flag", async () => {
|
||||
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --token`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Errors with the code `INVALID_ARGUMENT` on not supplying a value for the `--server` flag", async () => {
|
||||
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --server`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||
});
|
||||
|
||||
test("Errors with the code `TOKEN_INVALID` if the supplied access token is invalid", async () => {
|
||||
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --token invalid-token --server ${SERVER_URL}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("TOKEN_INVALID");
|
||||
});
|
||||
|
||||
test("Errors with the code `TOKEN_INVALID` if the supplied collection ID is invalid", async () => {
|
||||
const args = `test invalid-id --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ID");
|
||||
});
|
||||
|
||||
test("Errors with the code `TOKEN_INVALID` if the supplied environment ID is invalid", async () => {
|
||||
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --env invalid-id --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
|
||||
const { stderr } = await runCLI(args);
|
||||
|
||||
const out = getErrorCode(stderr);
|
||||
expect(out).toBe<HoppErrorCode>("INVALID_ID");
|
||||
});
|
||||
});
|
||||
|
||||
test("Successfully retrieves a collection with the ID", async () => {
|
||||
const args = `test ${COLLECTION_LEVEL_HEADERS_AUTH_COLL_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Successfully retrieves collections and environments from a workspace using their respective IDs", async () => {
|
||||
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --env ${REQ_BODY_ENV_VARS_ENVS_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Supports specifying collection file path along with environment ID", async () => {
|
||||
const TESTS_PATH = getTestJsonFilePath(
|
||||
"req-body-env-vars-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const args = `test ${TESTS_PATH} --env ${REQ_BODY_ENV_VARS_ENVS_ID} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Supports specifying environment file path along with collection ID", async () => {
|
||||
const ENV_PATH = getTestJsonFilePath(
|
||||
"req-body-env-vars-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `test ${REQ_BODY_ENV_VARS_COLL_ID} --env ${ENV_PATH} --token ${PERSONAL_ACCESS_TOKEN} --server ${SERVER_URL}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
|
||||
test("Supports specifying both collection and environment file paths", async () => {
|
||||
const TESTS_PATH = getTestJsonFilePath(
|
||||
"req-body-env-vars-coll.json",
|
||||
"collection"
|
||||
);
|
||||
const ENV_PATH = getTestJsonFilePath(
|
||||
"req-body-env-vars-envs.json",
|
||||
"environment"
|
||||
);
|
||||
const args = `test ${TESTS_PATH} --env ${ENV_PATH} --token ${PERSONAL_ACCESS_TOKEN}`;
|
||||
|
||||
const { error } = await runCLI(args);
|
||||
expect(error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
import { handleError } from "../handlers/error";
|
||||
import { parseDelayOption } from "../options/test/delay";
|
||||
import { parseEnvsData } from "../options/test/env";
|
||||
import { TestCmdOptions } from "../types/commands";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { isHoppCLIError } from "../utils/checks";
|
||||
import {
|
||||
collectionsRunner,
|
||||
collectionsRunnerExit,
|
||||
collectionsRunnerResult,
|
||||
} from "../utils/collections";
|
||||
import { handleError } from "../handlers/error";
|
||||
import { parseCollectionData } from "../utils/mutators";
|
||||
import { parseEnvsData } from "../options/test/env";
|
||||
import { TestCmdOptions } from "../types/commands";
|
||||
import { parseDelayOption } from "../options/test/delay";
|
||||
import { HoppEnvs } from "../types/request";
|
||||
import { isHoppCLIError } from "../utils/checks";
|
||||
|
||||
export const test = (pathOrId: string, options: TestCmdOptions) => async () => {
|
||||
export const test = (path: string, options: TestCmdOptions) => async () => {
|
||||
try {
|
||||
const delay = options.delay ? parseDelayOption(options.delay) : 0;
|
||||
const delay = options.delay ? parseDelayOption(options.delay) : 0
|
||||
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] }
|
||||
const collections = await parseCollectionData(path)
|
||||
|
||||
const envs = options.env
|
||||
? await parseEnvsData(options.env, options.token, options.server)
|
||||
: <HoppEnvs>{ global: [], selected: [] };
|
||||
|
||||
const collections = await parseCollectionData(
|
||||
pathOrId,
|
||||
options.token,
|
||||
options.server
|
||||
);
|
||||
|
||||
const report = await collectionsRunner({ collections, envs, delay });
|
||||
const hasSucceeded = collectionsRunnerResult(report);
|
||||
|
||||
collectionsRunnerExit(hasSucceeded);
|
||||
} catch (e) {
|
||||
if (isHoppCLIError(e)) {
|
||||
handleError(e);
|
||||
const report = await collectionsRunner({collections, envs, delay})
|
||||
const hasSucceeded = collectionsRunnerResult(report)
|
||||
collectionsRunnerExit(hasSucceeded)
|
||||
} catch(e) {
|
||||
if(isHoppCLIError(e)) {
|
||||
handleError(e)
|
||||
process.exit(1);
|
||||
} else throw e;
|
||||
}
|
||||
else throw e
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,15 +82,6 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
case "TESTS_FAILING":
|
||||
ERROR_MSG = error.data;
|
||||
break;
|
||||
case "TOKEN_EXPIRED":
|
||||
ERROR_MSG = `Token is expired: ${error.data}`;
|
||||
break;
|
||||
case "TOKEN_INVALID":
|
||||
ERROR_MSG = `Token is invalid/removed: ${error.data}`;
|
||||
break;
|
||||
case "INVALID_ID":
|
||||
ERROR_MSG = `The collection/environment is not valid/not accessible to the user: ${error.data}`;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!S.isEmpty(ERROR_MSG)) {
|
||||
|
||||
@@ -49,22 +49,14 @@ program.exitOverride().configureOutput({
|
||||
program
|
||||
.command("test")
|
||||
.argument(
|
||||
"<file_path_or_id>",
|
||||
"path to a hoppscotch collection.json file or collection ID from a workspace for CI testing"
|
||||
)
|
||||
.option(
|
||||
"-e, --env <file_path_or_id>",
|
||||
"path to an environment variables json file or environment ID from a workspace"
|
||||
"<file_path>",
|
||||
"path to a hoppscotch collection.json file for CI testing"
|
||||
)
|
||||
.option("-e, --env <file_path>", "path to an environment variables json file")
|
||||
.option(
|
||||
"-d, --delay <delay_in_ms>",
|
||||
"delay in milliseconds(ms) between consecutive requests within a collection"
|
||||
)
|
||||
.option(
|
||||
"--token <access_token>",
|
||||
"personal access token to access collections/environments from a workspace"
|
||||
)
|
||||
.option("--server <server_url>", "server URL for SH instance")
|
||||
.allowExcessArguments(false)
|
||||
.allowUnknownOption(false)
|
||||
.description("running hoppscotch collection.json file")
|
||||
@@ -74,7 +66,7 @@ program
|
||||
"https://docs.hoppscotch.io/documentation/clients/cli#commands"
|
||||
)}`
|
||||
)
|
||||
.action(async (pathOrId, options) => await test(pathOrId, options)());
|
||||
.action(async (path, options) => await test(path, options)());
|
||||
|
||||
export const cli = async (args: string[]) => {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Environment, EnvironmentSchemaVersion } from "@hoppscotch/data";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import fs from "fs/promises";
|
||||
import { Environment } from "@hoppscotch/data";
|
||||
import { entityReference } from "verzod";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -12,94 +10,13 @@ import {
|
||||
} from "../../types/request";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
interface WorkspaceEnvironment {
|
||||
id: string;
|
||||
teamID: string;
|
||||
name: string;
|
||||
variables: HoppEnvPair[];
|
||||
}
|
||||
|
||||
// Helper functions to transform workspace environment data to the `HoppEnvironment` format
|
||||
const transformWorkspaceEnvironment = (
|
||||
workspaceEnvironment: WorkspaceEnvironment
|
||||
): Environment => {
|
||||
const { teamID, variables, ...rest } = workspaceEnvironment;
|
||||
|
||||
// Add `secret` field if the data conforms to an older schema
|
||||
const transformedEnvVars = variables.map((variable) => {
|
||||
if (!("secret" in variable)) {
|
||||
return {
|
||||
...(variable as HoppEnvPair),
|
||||
secret: false,
|
||||
} as HoppEnvPair;
|
||||
}
|
||||
|
||||
return variable;
|
||||
});
|
||||
|
||||
return {
|
||||
v: EnvironmentSchemaVersion,
|
||||
variables: transformedEnvVars,
|
||||
...rest,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object
|
||||
* @param pathOrId Path of env.json file to be parsed
|
||||
* @param [accessToken] Personal access token to fetch workspace environments
|
||||
* @param [serverUrl] server URL for SH instance
|
||||
* @param path Path of env.json file to be parsed
|
||||
* @returns For successful parsing we get HoppEnvs object
|
||||
*/
|
||||
export async function parseEnvsData(
|
||||
pathOrId: string,
|
||||
accessToken?: string,
|
||||
serverUrl?: string
|
||||
) {
|
||||
let contents = null;
|
||||
let fileExistsInPath = null;
|
||||
|
||||
try {
|
||||
await fs.access(pathOrId);
|
||||
fileExistsInPath = true;
|
||||
} catch (e) {
|
||||
fileExistsInPath = false;
|
||||
}
|
||||
|
||||
if (accessToken && !fileExistsInPath) {
|
||||
try {
|
||||
const hostname = serverUrl || "https://api.hoppscotch.io";
|
||||
const url = `${hostname.endsWith("/") ? hostname.slice(0, -1) : hostname}/v1/access-tokens/environment/${pathOrId}`;
|
||||
|
||||
const { data }: { data: WorkspaceEnvironment } = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
contents = transformWorkspaceEnvironment(data);
|
||||
} catch (err) {
|
||||
const errReason = (
|
||||
err as AxiosError<{
|
||||
reason?: any;
|
||||
message: string;
|
||||
error: string;
|
||||
statusCode: number;
|
||||
}>
|
||||
).response?.data?.reason;
|
||||
|
||||
if (errReason) {
|
||||
throw error({
|
||||
code: errReason,
|
||||
data: pathOrId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contents === null) {
|
||||
contents = await readJsonFile(pathOrId);
|
||||
}
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path);
|
||||
const envPairs: Array<HoppEnvPair | Record<string, string>> = [];
|
||||
|
||||
// The legacy key-value pair format that is still supported
|
||||
@@ -116,7 +33,7 @@ export async function parseEnvsData(
|
||||
// CLI doesnt support bulk environments export
|
||||
// Hence we check for this case and throw an error if it matches the format
|
||||
if (HoppBulkEnvExportObjectResult.success) {
|
||||
throw error({ code: "BULK_ENV_FILE", path: pathOrId, data: error });
|
||||
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
// Checks if the environment file is of the correct format
|
||||
@@ -125,7 +42,7 @@ export async function parseEnvsData(
|
||||
!HoppEnvKeyPairResult.success &&
|
||||
HoppEnvExportObjectResult.type === "err"
|
||||
) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path: pathOrId, data: error });
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
if (HoppEnvKeyPairResult.success) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
export type TestCmdOptions = {
|
||||
env: string | undefined;
|
||||
delay: string | undefined;
|
||||
token: string | undefined;
|
||||
server: string | undefined;
|
||||
};
|
||||
|
||||
export type HOPP_ENV_FILE_EXT = "json";
|
||||
|
||||
@@ -26,9 +26,6 @@ type HoppErrors = {
|
||||
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
INVALID_FILE_TYPE: HoppErrorData;
|
||||
TOKEN_EXPIRED: HoppErrorData;
|
||||
TOKEN_INVALID: HoppErrorData;
|
||||
INVALID_ID: HoppErrorData;
|
||||
};
|
||||
|
||||
export type HoppErrorCode = keyof HoppErrors;
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
import {
|
||||
CollectionSchemaVersion,
|
||||
HoppCollection,
|
||||
HoppRESTRequest,
|
||||
} from "@hoppscotch/data";
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import fs from "fs/promises";
|
||||
import { entityReference } from "verzod";
|
||||
import { z } from "zod";
|
||||
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { error } from "../types/errors";
|
||||
import { FormDataEntry } from "../types/request";
|
||||
import { isHoppErrnoException } from "./checks";
|
||||
|
||||
interface WorkspaceCollection {
|
||||
id: string;
|
||||
data: string | null;
|
||||
title: string;
|
||||
parentID: string | null;
|
||||
folders: WorkspaceCollection[];
|
||||
requests: WorkspaceRequest[];
|
||||
}
|
||||
|
||||
interface WorkspaceRequest {
|
||||
id: string;
|
||||
collectionID: string;
|
||||
teamID: string;
|
||||
title: string;
|
||||
request: string;
|
||||
}
|
||||
|
||||
const getValidRequests = (
|
||||
collections: HoppCollection[],
|
||||
collectionFilePath: string
|
||||
@@ -64,50 +42,6 @@ const getValidRequests = (
|
||||
});
|
||||
};
|
||||
|
||||
// Helper functions to transform workspace collection data to the `HoppCollection` format
|
||||
const transformWorkspaceRequests = (requests: WorkspaceRequest[]) =>
|
||||
requests.map(({ request }) => JSON.parse(request));
|
||||
|
||||
const transformChildCollections = (
|
||||
childCollections: WorkspaceCollection[]
|
||||
): HoppCollection[] => {
|
||||
return childCollections.map(({ id, title, data, folders, requests }) => {
|
||||
const parsedData = data ? JSON.parse(data) : {};
|
||||
const { auth = { authType: "inherit", authActive: false }, headers = [] } =
|
||||
parsedData;
|
||||
|
||||
return {
|
||||
v: CollectionSchemaVersion,
|
||||
id,
|
||||
name: title,
|
||||
folders: transformChildCollections(folders),
|
||||
requests: transformWorkspaceRequests(requests),
|
||||
auth,
|
||||
headers,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const transformWorkspaceCollection = (
|
||||
collection: WorkspaceCollection
|
||||
): HoppCollection => {
|
||||
const { id, title, data, requests, folders } = collection;
|
||||
|
||||
const parsedData = data ? JSON.parse(data) : {};
|
||||
const { auth = { authType: "inherit", authActive: false }, headers = [] } =
|
||||
parsedData;
|
||||
|
||||
return {
|
||||
v: CollectionSchemaVersion,
|
||||
id,
|
||||
name: title,
|
||||
folders: transformChildCollections(folders),
|
||||
requests: transformWorkspaceRequests(requests),
|
||||
auth,
|
||||
headers,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses array of FormDataEntry to FormData.
|
||||
* @param values Array of FormDataEntry.
|
||||
@@ -158,64 +92,16 @@ export async function readJsonFile(path: string): Promise<unknown> {
|
||||
|
||||
/**
|
||||
* Parses collection json file for given path:context.path, and validates
|
||||
* the parsed collectiona array
|
||||
* @param pathOrId Collection json file path
|
||||
* @param [accessToken] Personal access token to fetch workspace environments
|
||||
* @param [serverUrl] server URL for SH instance
|
||||
* @returns For successful parsing we get array of HoppCollection
|
||||
* the parsed collectiona array.
|
||||
* @param path Collection json file path.
|
||||
* @returns For successful parsing we get array of HoppCollection,
|
||||
*/
|
||||
export async function parseCollectionData(
|
||||
pathOrId: string,
|
||||
accessToken?: string,
|
||||
serverUrl?: string
|
||||
path: string
|
||||
): Promise<HoppCollection[]> {
|
||||
let contents = null;
|
||||
let fileExistsInPath = null;
|
||||
let contents = await readJsonFile(path);
|
||||
|
||||
try {
|
||||
await fs.access(pathOrId);
|
||||
fileExistsInPath = true;
|
||||
} catch (e) {
|
||||
fileExistsInPath = false;
|
||||
}
|
||||
|
||||
if (accessToken && !fileExistsInPath) {
|
||||
try {
|
||||
const hostname = serverUrl || "https://api.hoppscotch.io";
|
||||
const url = `${hostname.endsWith("/") ? hostname.slice(0, -1) : hostname}/v1/access-tokens/collection/${pathOrId}`;
|
||||
|
||||
const { data }: { data: WorkspaceCollection } = await axios.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
contents = transformWorkspaceCollection(data);
|
||||
} catch (err) {
|
||||
const errReason = (
|
||||
err as AxiosError<{
|
||||
reason?: any;
|
||||
message: string;
|
||||
error: string;
|
||||
statusCode: number;
|
||||
}>
|
||||
).response?.data?.reason;
|
||||
|
||||
if (errReason) {
|
||||
throw error({
|
||||
code: errReason,
|
||||
data: pathOrId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to reading from file if contents are not available
|
||||
if (contents === null) {
|
||||
contents = await readJsonFile(pathOrId);
|
||||
}
|
||||
|
||||
const maybeArrayOfCollections: HoppCollection[] = Array.isArray(contents)
|
||||
const maybeArrayOfCollections: unknown[] = Array.isArray(contents)
|
||||
? contents
|
||||
: [contents];
|
||||
|
||||
@@ -224,14 +110,12 @@ export async function parseCollectionData(
|
||||
.safeParse(maybeArrayOfCollections);
|
||||
|
||||
if (!collectionSchemaParsedResult.success) {
|
||||
console.error(`Error is `, collectionSchemaParsedResult.error);
|
||||
|
||||
throw error({
|
||||
code: "MALFORMED_COLLECTION",
|
||||
path: pathOrId,
|
||||
path,
|
||||
data: "Please check the collection data.",
|
||||
});
|
||||
}
|
||||
|
||||
return getValidRequests(collectionSchemaParsedResult.data, pathOrId);
|
||||
return getValidRequests(collectionSchemaParsedResult.data, path);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AccessTokensGenerate: typeof import('./components/accessTokens/Generate.vue')['default']
|
||||
AccessTokensGenerateModal: typeof import('./components/accessTokens/GenerateModal.vue')['default']
|
||||
AccessTokensList: typeof import('./components/accessTokens/List.vue')['default']
|
||||
AccessTokensOverview: typeof import('./components/accessTokens/Overview.vue')['default']
|
||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||
AppBanner: typeof import('./components/app/Banner.vue')['default']
|
||||
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
|
||||
@@ -148,7 +152,7 @@ declare module 'vue' {
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||
IconLucideBrush: (typeof import("~icons/lucide/brush"))["default"]
|
||||
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
|
||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||
@@ -158,9 +162,10 @@ declare module 'vue' {
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideRss: (typeof import("~icons/lucide/rss"))["default"]
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||
IconLucideX: typeof import('~icons/lucide/x')['default']
|
||||
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
|
||||
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<HoppSmartModal dialog title="New Personal Access Token" @close="hideModal">
|
||||
<template #body>
|
||||
<template v-if="accessToken">
|
||||
<p class="text-amber-500 mb-4 border rounded-md border-amber-600 p-4">
|
||||
Make sure to copy your personal access token now. You won’t be able to
|
||||
see it again!
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="rounded-md bg-primaryLight p-4 mt-4 flex items-center justify-between"
|
||||
>
|
||||
<div class="text-secondaryDark">{{ accessToken }}</div>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
filled
|
||||
:icon="copyIcon"
|
||||
@click="copyAccessToken"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<div class="text-secondaryDark font-semibold">Label</div>
|
||||
<HoppSmartInput
|
||||
v-model="accessTokenLabel"
|
||||
placeholder=" "
|
||||
class="floating-input"
|
||||
/>
|
||||
<div class="text-secondaryLight">What's this token for?</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="expiration" class="text-secondaryDark font-semibold"
|
||||
>Expiration</label
|
||||
>
|
||||
|
||||
<div class="grid grid-cols-2 items-center gap-x-2">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="tippyActions?.focus()"
|
||||
>
|
||||
<HoppSmartSelectWrapper>
|
||||
<input
|
||||
id="expiration"
|
||||
:value="expiration"
|
||||
readonly
|
||||
class="flex flex-1 cursor-pointer bg-transparent px-4 py-2 rounded border border-divider"
|
||||
/>
|
||||
</HoppSmartSelectWrapper>
|
||||
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
tabindex="0"
|
||||
role="menu"
|
||||
class="flex flex-col focus:outline-none"
|
||||
@keyup.escape="hide"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="expirationOption in Object.keys(expirationOptions)"
|
||||
:key="expirationOption"
|
||||
:label="expirationOption"
|
||||
:icon="
|
||||
expirationOption === expiration
|
||||
? IconCircleDot
|
||||
: IconCircle
|
||||
"
|
||||
:active="expirationOption === expiration"
|
||||
:aria-selected="expirationOption === expiration"
|
||||
@click="
|
||||
() => {
|
||||
expiration = expirationOption
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
|
||||
<span class="text-secondaryLight">{{ expirationDateText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-secondaryDark font-semibold">Scope</div>
|
||||
|
||||
<p class="text-secondaryLight">
|
||||
Read-only access to workspace data.<br />
|
||||
Personal Access Tokens can't access your personal workspace.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<HoppButtonSecondary
|
||||
v-if="accessToken"
|
||||
:label="t('action.close')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
|
||||
<div v-else class="flex items-center gap-x-2">
|
||||
<HoppButtonPrimary
|
||||
:loading="tokenGenerateActionLoading"
|
||||
filled
|
||||
outline
|
||||
label="Generate Token"
|
||||
@click="generateAccessToken"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { shortDateTime } from "~/helpers/utils/date"
|
||||
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCircle from "~icons/lucide/circle"
|
||||
import IconCircleDot from "~icons/lucide/circle-dot"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
tokenGenerateActionLoading: boolean
|
||||
accessToken: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
(
|
||||
e: "generate-access-token",
|
||||
{ label, expiryInDays }: { label: string; expiryInDays: number | null }
|
||||
): void
|
||||
}>()
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<TippyComponent[] | null>(null)
|
||||
|
||||
const copyIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
|
||||
const accessTokenLabel = ref<string>("")
|
||||
const expiration = ref<string>("30 days")
|
||||
|
||||
const expirationOptions: Record<string, number | null> = {
|
||||
"7 days": 7,
|
||||
"30 days": 30,
|
||||
"60 days": 60,
|
||||
"90 days": 90,
|
||||
"No expiration": null,
|
||||
}
|
||||
|
||||
const expirationDateText = computed(() => {
|
||||
const chosenExpiryInDays = expirationOptions[expiration.value]
|
||||
|
||||
if (chosenExpiryInDays === null) {
|
||||
return "This token will never expire!"
|
||||
}
|
||||
|
||||
const currentDate = new Date()
|
||||
currentDate.setDate(currentDate.getDate() + chosenExpiryInDays)
|
||||
|
||||
const expirationDate = shortDateTime(currentDate, false)
|
||||
return `This token will expire on ${expirationDate}`
|
||||
})
|
||||
|
||||
const copyAccessToken = () => {
|
||||
if (!props.accessToken) {
|
||||
toast.error("error.something_went_wrong")
|
||||
return
|
||||
}
|
||||
|
||||
copyToClipboard(props.accessToken)
|
||||
copyIcon.value = IconCheck
|
||||
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
|
||||
const generateAccessToken = async () => {
|
||||
if (!accessTokenLabel.value) {
|
||||
toast.error("Please provide a label for the token")
|
||||
return
|
||||
}
|
||||
|
||||
emit("generate-access-token", {
|
||||
label: accessTokenLabel.value,
|
||||
expiryInDays: expirationOptions[expiration.value],
|
||||
})
|
||||
}
|
||||
|
||||
const hideModal = () => emit("hide-modal")
|
||||
</script>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="max-w-2xl space-y-4">
|
||||
<div
|
||||
v-for="{ id, label, lastUsedOn, expiresOn } in accessTokens"
|
||||
:key="id"
|
||||
class="flex items-center justify-between rounded border border-divider p-4"
|
||||
>
|
||||
<span class="font-semibold text-secondaryDark text-sm">
|
||||
{{ label }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="text-secondaryLight space-y-1">
|
||||
<div class="space-x-1">
|
||||
<span class="font-semibold">Last used on:</span>
|
||||
<span>
|
||||
{{ shortDateTime(lastUsedOn, false) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-x-1">
|
||||
<span class="font-semibold">Expires on:</span>
|
||||
<span>
|
||||
{{
|
||||
expiresOn ? shortDateTime(expiresOn, false) : "No expiration"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HoppButtonSecondary
|
||||
label="Delete"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
emit('delete-access-token', {
|
||||
tokenId: id,
|
||||
tokenLabel: label,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HoppSmartIntersection
|
||||
v-if="hasMoreTokens"
|
||||
@intersecting="emit('fetch-more-tokens')"
|
||||
>
|
||||
<div v-if="tokensListLoading" class="flex flex-col items-center py-3">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
</HoppSmartIntersection>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { shortDateTime } from "~/helpers/utils/date"
|
||||
import { AccessToken } from "~/pages/profile.vue"
|
||||
|
||||
defineProps<{
|
||||
accessTokens: AccessToken[]
|
||||
hasMoreTokens: boolean
|
||||
tokensListLoading: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "fetch-more-tokens"): void
|
||||
(
|
||||
e: "delete-access-token",
|
||||
{ tokenId, tokenLabel }: { tokenId: string; tokenLabel: string }
|
||||
): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="space-y-1">
|
||||
<h4 class="font-semibold text-secondaryDark">Personal Access Tokens</h4>
|
||||
|
||||
<p class="text-secondaryLight">
|
||||
Personal access tokens currently helps you connect the CLI to your
|
||||
Hoppscotch account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<HoppButtonSecondary
|
||||
filled
|
||||
outline
|
||||
label="Generate new token"
|
||||
@click="emit('show-access-tokens-generate-modal')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
(e: "show-access-tokens-generate-modal"): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -61,7 +61,7 @@
|
||||
<div
|
||||
v-for="(member, index) in membersList"
|
||||
:key="`member-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
class="flex"
|
||||
>
|
||||
<input
|
||||
class="flex flex-1 bg-transparent px-4 py-2"
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
export function shortDateTime(date: string | number | Date) {
|
||||
export function shortDateTime(
|
||||
date: string | number | Date,
|
||||
includeTime: boolean = true
|
||||
) {
|
||||
return new Date(date).toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
...(includeTime
|
||||
? {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,38 +182,79 @@
|
||||
<ProfileUserDelete />
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab :id="'teams'" :label="t('team.title')">
|
||||
<Teams :modal="false" class="p-4" />
|
||||
</HoppSmartTab>
|
||||
|
||||
<HoppSmartTab id="tokens" label="Tokens" class="space-y-4">
|
||||
<AccessTokensOverview
|
||||
@show-access-tokens-generate-modal="
|
||||
showAccessTokensGenerateModal = true
|
||||
"
|
||||
/>
|
||||
|
||||
<AccessTokensList
|
||||
:access-tokens="accessTokens"
|
||||
:has-more-tokens="hasMoreTokens"
|
||||
:tokens-list-loading="tokensListLoading"
|
||||
@delete-access-token="showDeleteAccessTokenConfirmation"
|
||||
@fetch-more-tokens="fetchAccessTokens"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AccessTokensGenerateModal
|
||||
v-if="showAccessTokensGenerateModal"
|
||||
:access-token="accessToken"
|
||||
:token-generate-action-loading="tokenGenerateActionLoading"
|
||||
@generate-access-token="generateAccessToken"
|
||||
@hide-modal="hideAccessTokenGenerateModal"
|
||||
/>
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmDeleteAccessToken"
|
||||
:loading-state="tokenDeleteActionLoading"
|
||||
:title="`Are you sure you want to delete the access token ${tokenToDelete?.label}?`"
|
||||
@hide-modal="confirmDeleteAccessToken = false"
|
||||
@resolve="() => deleteAccessToken()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect, computed } from "vue"
|
||||
import axios from "axios"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { Ref, computed, onMounted, ref, watchEffect } from "vue"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { usePageHead } from "@composables/head"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
|
||||
import { toggleSetting } from "~/newstore/settings"
|
||||
|
||||
import IconVerified from "~icons/lucide/verified"
|
||||
import IconSettings from "~icons/lucide/settings"
|
||||
|
||||
import * as E from "fp-ts/Either"
|
||||
import IconVerified from "~icons/lucide/verified"
|
||||
|
||||
type ProfileTabs = "sync" | "teams"
|
||||
|
||||
export type AccessToken = {
|
||||
id: string
|
||||
label: string
|
||||
createdOn: Date
|
||||
lastUsedOn: Date
|
||||
expiresOn: Date | null
|
||||
}
|
||||
|
||||
const selectedProfileTab = ref<ProfileTabs>("sync")
|
||||
|
||||
const t = useI18n()
|
||||
@@ -224,6 +265,12 @@ usePageHead({
|
||||
title: computed(() => t("navigation.profile")),
|
||||
})
|
||||
|
||||
const accessTokens: Ref<AccessToken[]> = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchAccessTokens()
|
||||
})
|
||||
|
||||
const SYNC_COLLECTIONS = useSetting("syncCollections")
|
||||
const SYNC_ENVIRONMENTS = useSetting("syncEnvironments")
|
||||
const SYNC_HISTORY = useSetting("syncHistory")
|
||||
@@ -236,6 +283,24 @@ const probableUser = useReadonlyStream(
|
||||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
const confirmDeleteAccessToken = ref(false)
|
||||
const hasMoreTokens = ref(true)
|
||||
const showAccessTokensGenerateModal = ref(false)
|
||||
const tokenDeleteActionLoading = ref(false)
|
||||
const tokenGenerateActionLoading = ref(false)
|
||||
const tokensListLoading = ref(false)
|
||||
|
||||
const accessToken: Ref<string | null> = ref(null)
|
||||
const tokenToDelete = ref<{ id: string; label: string } | null>(null)
|
||||
|
||||
const limit = 10
|
||||
let offset = 0
|
||||
|
||||
const accessTokenEndpointMetadata = {
|
||||
axiosPlatformConfig: platform.auth.axiosPlatformConfig?.() ?? {},
|
||||
endpointPrefix: `${import.meta.env.VITE_BACKEND_API_URL}/access-tokens`,
|
||||
}
|
||||
|
||||
const loadingCurrentUser = computed(() => {
|
||||
if (!probableUser.value) return false
|
||||
else if (!currentUser.value) return true
|
||||
@@ -305,4 +370,116 @@ const sendEmailVerification = () => {
|
||||
verifyingEmailAddress.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const fetchAccessTokens = async () => {
|
||||
tokensListLoading.value = true
|
||||
|
||||
const { axiosPlatformConfig, endpointPrefix } = accessTokenEndpointMetadata
|
||||
|
||||
const endpoint = `${endpointPrefix}/list?offset=${offset}&limit=${limit}`
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(endpoint, axiosPlatformConfig)
|
||||
|
||||
accessTokens.value.push(...data)
|
||||
|
||||
if (data.length > 0) {
|
||||
offset += data.length
|
||||
}
|
||||
|
||||
hasMoreTokens.value = data.length === limit
|
||||
} catch (err) {
|
||||
toast.error("Something went wrong while fetching the list of tokens")
|
||||
} finally {
|
||||
tokensListLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const generateAccessToken = async ({
|
||||
label,
|
||||
expiryInDays,
|
||||
}: {
|
||||
label: string
|
||||
expiryInDays: number | null
|
||||
}) => {
|
||||
tokenGenerateActionLoading.value = true
|
||||
|
||||
const { axiosPlatformConfig, endpointPrefix } = accessTokenEndpointMetadata
|
||||
|
||||
const endpoint = `${endpointPrefix}/create`
|
||||
|
||||
const body = {
|
||||
label,
|
||||
expiryInDays,
|
||||
}
|
||||
|
||||
try {
|
||||
const { data }: { data: { token: string; info: AccessToken } } =
|
||||
await axios.post(endpoint, body, axiosPlatformConfig)
|
||||
|
||||
accessTokens.value.unshift(data.info)
|
||||
accessToken.value = data.token
|
||||
|
||||
// Incrementing the offset value by 1 to account for the newly generated token
|
||||
offset += 1
|
||||
} catch (err) {
|
||||
toast.error(`Something went wrong while generating the access token`)
|
||||
} finally {
|
||||
tokenGenerateActionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAccessToken = async () => {
|
||||
if (tokenToDelete.value === null) {
|
||||
toast.error("error.something_went_wrong")
|
||||
return
|
||||
}
|
||||
|
||||
const { id: tokenIdToDelete } = tokenToDelete.value
|
||||
|
||||
tokenDeleteActionLoading.value = true
|
||||
|
||||
const { axiosPlatformConfig, endpointPrefix } = accessTokenEndpointMetadata
|
||||
|
||||
const endpoint = `${endpointPrefix}/revoke?id=${tokenIdToDelete}`
|
||||
|
||||
try {
|
||||
await axios.delete(endpoint, axiosPlatformConfig)
|
||||
|
||||
accessTokens.value = accessTokens.value.filter(
|
||||
(token) => token.id !== tokenIdToDelete
|
||||
)
|
||||
|
||||
// Decreasing the offset value by 1 to account for the deleted token
|
||||
offset = offset > 0 ? offset - 1 : offset
|
||||
} catch (err) {
|
||||
toast.error("Something went wrong while deleting the access token")
|
||||
} finally {
|
||||
tokenDeleteActionLoading.value = false
|
||||
confirmDeleteAccessToken.value = false
|
||||
|
||||
tokenToDelete.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const hideAccessTokenGenerateModal = () => {
|
||||
// Reset the reactive state variable holding access token value and hide the modal
|
||||
accessToken.value = null
|
||||
showAccessTokensGenerateModal.value = false
|
||||
}
|
||||
|
||||
const showDeleteAccessTokenConfirmation = ({
|
||||
tokenId,
|
||||
tokenLabel,
|
||||
}: {
|
||||
tokenId: string
|
||||
tokenLabel: string
|
||||
}) => {
|
||||
confirmDeleteAccessToken.value = true
|
||||
|
||||
tokenToDelete.value = {
|
||||
id: tokenId,
|
||||
label: tokenLabel,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user