mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-01 02:21:45 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dd7e728ff | |||
| 3a349bccfe | |||
| 13a667a9b1 | |||
| 420c6e2c4a | |||
| bbdfbcb9ca | |||
| d1e6f8fb33 | |||
| 930a816f42 | |||
| ec0143aa93 | |||
| 3cc54dea22 | |||
| a8fb144c09 | |||
| 6813fa8bf2 | |||
| cf7de26a2e | |||
| 8676272657 | |||
| c3aecfdc0c |
@@ -0,0 +1,566 @@
|
|||||||
|
const COMMENT_MARKER = "<!-- yaak-contribution-policy -->";
|
||||||
|
|
||||||
|
const MAINTAINER_LOGINS = new Set(["gschier"]);
|
||||||
|
const MAINTAINER_ASSOCIATIONS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
||||||
|
const MAINTAINER_PERMISSIONS = new Set(["admin", "maintain", "write"]);
|
||||||
|
|
||||||
|
const LARGE_DIFF_CHANGED_FILES = 20;
|
||||||
|
const LARGE_DIFF_CHANGED_LINES = 800;
|
||||||
|
|
||||||
|
const LABELS = {
|
||||||
|
accepted: {
|
||||||
|
name: "contribution: accepted",
|
||||||
|
color: "0E8A16",
|
||||||
|
description: "Community PR appears to match Yaak's contribution policy.",
|
||||||
|
},
|
||||||
|
approvedFeedback: {
|
||||||
|
name: "contribution: approved feedback",
|
||||||
|
color: "5319E7",
|
||||||
|
description: "Community PR links an approved feedback item.",
|
||||||
|
},
|
||||||
|
needsTemplate: {
|
||||||
|
name: "contribution: needs template",
|
||||||
|
color: "D93F0B",
|
||||||
|
description: "Community PR needs a completed pull request template.",
|
||||||
|
},
|
||||||
|
needsApproval: {
|
||||||
|
name: "contribution: needs approval",
|
||||||
|
color: "B60205",
|
||||||
|
description: "Community PR needs an approved feedback item before review.",
|
||||||
|
},
|
||||||
|
largeDiff: {
|
||||||
|
name: "contribution: large diff",
|
||||||
|
color: "FBCA04",
|
||||||
|
description:
|
||||||
|
"Community PR has a larger-than-usual diff for a small-scope contribution.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MANAGED_LABEL_NAMES = Object.values(LABELS).map((label) => label.name);
|
||||||
|
|
||||||
|
const CHECKBOXES = {
|
||||||
|
smallScope: "This PR is a bug fix or small-scope improvement.",
|
||||||
|
approvedFeedback:
|
||||||
|
"If this PR is not a bug fix or small-scope improvement, I linked an approved feedback item below.",
|
||||||
|
readContributing:
|
||||||
|
"I have read and followed [`CONTRIBUTING.md`](CONTRIBUTING.md).",
|
||||||
|
testedLocally: "I tested this change locally.",
|
||||||
|
testsUpdated: "I added or updated tests when reasonable.",
|
||||||
|
};
|
||||||
|
|
||||||
|
function escapeRegExp(value) {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBody(body) {
|
||||||
|
return (body || "").replace(/\r\n/g, "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripComments(value) {
|
||||||
|
return value.replace(/<!--[\s\S]*?-->/g, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSection(body, heading) {
|
||||||
|
const pattern = new RegExp(`^##\\s+${escapeRegExp(heading)}\\s*$`, "gim");
|
||||||
|
const match = pattern.exec(body);
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rest = body.slice(match.index + match[0].length);
|
||||||
|
const nextHeadingIndex = rest.search(/^##\s+/m);
|
||||||
|
return nextHeadingIndex === -1 ? rest : rest.slice(0, nextHeadingIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMeaningfulText(value) {
|
||||||
|
return stripComments(value || "").length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkboxState(body, label) {
|
||||||
|
const flexibleLabel = escapeRegExp(label).replace(/\\ /g, "\\s+");
|
||||||
|
const pattern = new RegExp(
|
||||||
|
`^\\s*[-*]\\s*\\[([ xX])\\]\\s*${flexibleLabel}\\s*$`,
|
||||||
|
"im",
|
||||||
|
);
|
||||||
|
const match = body.match(pattern);
|
||||||
|
|
||||||
|
if (match == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1].toLowerCase() === "x";
|
||||||
|
}
|
||||||
|
|
||||||
|
function findFeedbackUrl(body) {
|
||||||
|
return (
|
||||||
|
body.match(
|
||||||
|
/https?:\/\/(?:www\.)?(?:yaak\.app\/feedback|feedback\.yaak\.app)\/[^\s)>\]]+/i,
|
||||||
|
)?.[0] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function analyzePullRequest(pr) {
|
||||||
|
const body = normalizeBody(pr.body);
|
||||||
|
const states = Object.fromEntries(
|
||||||
|
Object.entries(CHECKBOXES).map(([key, label]) => [
|
||||||
|
key,
|
||||||
|
checkboxState(body, label),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const sectionCount = ["Summary", "Submission", "Related"].filter(
|
||||||
|
(heading) => getSection(body, heading) != null,
|
||||||
|
).length;
|
||||||
|
const checkboxCount = Object.values(states).filter(
|
||||||
|
(state) => state != null,
|
||||||
|
).length;
|
||||||
|
const templateUsed = sectionCount >= 2 && checkboxCount >= 3;
|
||||||
|
const blockers = [];
|
||||||
|
const totalChangedLines =
|
||||||
|
Number(pr.additions || 0) + Number(pr.deletions || 0);
|
||||||
|
const changedFiles = Number(pr.changed_files || 0);
|
||||||
|
const largeDiff =
|
||||||
|
changedFiles > LARGE_DIFF_CHANGED_FILES ||
|
||||||
|
totalChangedLines > LARGE_DIFF_CHANGED_LINES;
|
||||||
|
|
||||||
|
if (!templateUsed) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.needsTemplate.name,
|
||||||
|
message:
|
||||||
|
"Update the PR description with the repository pull request template.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const summary = getSection(body, "Summary");
|
||||||
|
const hasSummary = hasMeaningfulText(summary);
|
||||||
|
const feedbackUrl = findFeedbackUrl(body);
|
||||||
|
const smallScope = states.smallScope === true;
|
||||||
|
const approvedFeedback = states.approvedFeedback === true;
|
||||||
|
|
||||||
|
if (!hasSummary) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.needsTemplate.name,
|
||||||
|
message: "Add a short summary describing the bug fix or improvement.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (smallScope && approvedFeedback) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.needsTemplate.name,
|
||||||
|
message:
|
||||||
|
"Choose either the small-scope checkbox or the approved-feedback checkbox, not both.",
|
||||||
|
});
|
||||||
|
} else if (!smallScope && !approvedFeedback) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.needsTemplate.name,
|
||||||
|
message:
|
||||||
|
"Check whether this is a bug fix or small-scope improvement, or confirm that an approved feedback item is linked.",
|
||||||
|
});
|
||||||
|
} else if (approvedFeedback && feedbackUrl == null) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.needsApproval.name,
|
||||||
|
message:
|
||||||
|
"Link the approved feedback item where contribution approval was explicitly stated.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (states.readContributing !== true) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.needsTemplate.name,
|
||||||
|
message: "Confirm that `CONTRIBUTING.md` was read and followed.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (states.testedLocally !== true) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.needsTemplate.name,
|
||||||
|
message: "Confirm that the change was tested locally.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (states.testsUpdated !== true) {
|
||||||
|
blockers.push({
|
||||||
|
label: LABELS.needsTemplate.name,
|
||||||
|
message: "Confirm that tests were added or updated when reasonable.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredLabels = new Set(blockers.map((blocker) => blocker.label));
|
||||||
|
|
||||||
|
if (blockers.length === 0) {
|
||||||
|
desiredLabels.add(
|
||||||
|
states.approvedFeedback
|
||||||
|
? LABELS.approvedFeedback.name
|
||||||
|
: LABELS.accepted.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (largeDiff) {
|
||||||
|
desiredLabels.add(LABELS.largeDiff.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockers,
|
||||||
|
changedFiles,
|
||||||
|
desiredLabels: [...desiredLabels],
|
||||||
|
largeDiff,
|
||||||
|
templateUsed,
|
||||||
|
totalChangedLines,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBlockingComment(analysis) {
|
||||||
|
const lines = [
|
||||||
|
COMMENT_MARKER,
|
||||||
|
"Thanks for the PR. Yaak currently accepts community PRs for bug fixes and small-scope improvements, plus larger changes that link an approved feedback item from https://yaak.app/feedback.",
|
||||||
|
"",
|
||||||
|
"This PR cannot be accepted yet. Please update the PR description to address:",
|
||||||
|
"",
|
||||||
|
...analysis.blockers.map((blocker) => `- ${blocker.message}`),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (analysis.largeDiff) {
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
`This PR also changes ${analysis.changedFiles} files and ${analysis.totalChangedLines} lines, so it has been labeled as a large diff. That label is advisory, but maintainers may ask for the scope to be reduced.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(
|
||||||
|
"",
|
||||||
|
"I did not overwrite the PR body, since that can remove useful context. Editing the description directly is the safest way to keep your notes while completing the template.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeResult({ pr, analysis, skipped, skipReason }) {
|
||||||
|
if (skipped) {
|
||||||
|
return `#${pr.number} ${pr.title} - skipped (${skipReason})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status =
|
||||||
|
analysis.blockers.length > 0
|
||||||
|
? `blocked: ${analysis.blockers.map((blocker) => blocker.message).join("; ")}`
|
||||||
|
: "accepted";
|
||||||
|
const labels =
|
||||||
|
analysis.desiredLabels.length > 0
|
||||||
|
? analysis.desiredLabels.join(", ")
|
||||||
|
: "none";
|
||||||
|
|
||||||
|
return `#${pr.number} ${pr.title} - ${status}; labels: ${labels}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isOfficialMaintainer({ github, owner, repo, pr }) {
|
||||||
|
if (MAINTAINER_LOGINS.has(pr.user.login)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MAINTAINER_ASSOCIATIONS.has(pr.author_association)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
username: pr.user.login,
|
||||||
|
});
|
||||||
|
|
||||||
|
return MAINTAINER_PERMISSIONS.has(response.data.permission);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status === 404) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureManagedLabels({ github, owner, repo }) {
|
||||||
|
for (const label of Object.values(LABELS)) {
|
||||||
|
try {
|
||||||
|
await github.rest.issues.getLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
name: label.name,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status !== 404) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.createLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
name: label.name,
|
||||||
|
color: label.color,
|
||||||
|
description: label.description,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncLabels({ github, owner, repo, issueNumber, desiredLabels }) {
|
||||||
|
const desired = new Set(desiredLabels);
|
||||||
|
|
||||||
|
await ensureManagedLabels({ github, owner, repo });
|
||||||
|
|
||||||
|
for (const labelName of MANAGED_LABEL_NAMES) {
|
||||||
|
if (desired.has(labelName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await github.rest.issues.removeLabel({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
name: labelName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.status !== 404) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desired.size > 0) {
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
labels: [...desired],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findPolicyComment({ github, owner, repo, issueNumber }) {
|
||||||
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return comments.find(
|
||||||
|
(comment) =>
|
||||||
|
comment.user.type === "Bot" && comment.body?.includes(COMMENT_MARKER),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertPolicyComment({ github, owner, repo, issueNumber, body }) {
|
||||||
|
const existingComment = await findPolicyComment({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingComment == null) {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: issueNumber,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: existingComment.id,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePolicyComment({ github, owner, repo, issueNumber }) {
|
||||||
|
const existingComment = await findPolicyComment({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingComment == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await github.rest.issues.deleteComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: existingComment.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkPullRequest({
|
||||||
|
github,
|
||||||
|
core,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pullNumber,
|
||||||
|
dryRun,
|
||||||
|
}) {
|
||||||
|
const response = await github.rest.pulls.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pullNumber,
|
||||||
|
});
|
||||||
|
const pr = response.data;
|
||||||
|
const issueNumber = pr.number;
|
||||||
|
|
||||||
|
if (pr.draft) {
|
||||||
|
core.notice(`Skipping contribution policy for draft PR #${pr.number}.`);
|
||||||
|
return {
|
||||||
|
blocked: false,
|
||||||
|
number: pr.number,
|
||||||
|
summary: summarizeResult({
|
||||||
|
pr,
|
||||||
|
skipped: true,
|
||||||
|
skipReason: "draft",
|
||||||
|
}),
|
||||||
|
skipped: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await isOfficialMaintainer({ github, owner, repo, pr })) {
|
||||||
|
core.notice(
|
||||||
|
`Skipping contribution policy for maintainer PR #${pr.number} from @${pr.user.login}.`,
|
||||||
|
);
|
||||||
|
if (!dryRun) {
|
||||||
|
await syncLabels({ github, owner, repo, issueNumber, desiredLabels: [] });
|
||||||
|
await deletePolicyComment({ github, owner, repo, issueNumber });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
blocked: false,
|
||||||
|
number: pr.number,
|
||||||
|
summary: summarizeResult({
|
||||||
|
pr,
|
||||||
|
skipped: true,
|
||||||
|
skipReason: `maintainer @${pr.user.login}`,
|
||||||
|
}),
|
||||||
|
skipped: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysis = analyzePullRequest(pr);
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
const summary = summarizeResult({ pr, analysis });
|
||||||
|
core.notice(`[dry-run] ${summary}`);
|
||||||
|
return {
|
||||||
|
blocked: analysis.blockers.length > 0,
|
||||||
|
number: pr.number,
|
||||||
|
summary,
|
||||||
|
skipped: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await syncLabels({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
desiredLabels: analysis.desiredLabels,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (analysis.blockers.length > 0) {
|
||||||
|
await upsertPolicyComment({
|
||||||
|
github,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issueNumber,
|
||||||
|
body: buildBlockingComment(analysis),
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
blocked: true,
|
||||||
|
number: pr.number,
|
||||||
|
summary: summarizeResult({ pr, analysis }),
|
||||||
|
skipped: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await deletePolicyComment({ github, owner, repo, issueNumber });
|
||||||
|
core.notice(`Contribution policy check passed for PR #${pr.number}.`);
|
||||||
|
return {
|
||||||
|
blocked: false,
|
||||||
|
number: pr.number,
|
||||||
|
summary: summarizeResult({ pr, analysis }),
|
||||||
|
skipped: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listOpenPullRequests({ github, owner, repo }) {
|
||||||
|
return github.paginate(github.rest.pulls.list, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
state: "open",
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run({ github, context, core }) {
|
||||||
|
const { owner, repo } = context.repo;
|
||||||
|
const payloadPr = context.payload.pull_request;
|
||||||
|
const dryRun =
|
||||||
|
context.eventName === "workflow_dispatch" &&
|
||||||
|
context.payload.inputs?.dry_run !== "false";
|
||||||
|
const pullRequests =
|
||||||
|
payloadPr == null
|
||||||
|
? await listOpenPullRequests({ github, owner, repo })
|
||||||
|
: [payloadPr];
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
core.notice("Running contribution policy in dry-run mode.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pr of pullRequests) {
|
||||||
|
results.push(
|
||||||
|
await checkPullRequest({
|
||||||
|
github,
|
||||||
|
core,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pullNumber: pr.number,
|
||||||
|
dryRun,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await core.summary
|
||||||
|
.addHeading(`Contribution Policy ${dryRun ? "Dry Run" : "Results"}`)
|
||||||
|
.addTable([
|
||||||
|
[
|
||||||
|
{ data: "PR", header: true },
|
||||||
|
{ data: "Result", header: true },
|
||||||
|
],
|
||||||
|
...results.map((result) => [`#${result.number}`, result.summary]),
|
||||||
|
])
|
||||||
|
.write();
|
||||||
|
|
||||||
|
const blockedPullRequests = results.filter((result) => result.blocked);
|
||||||
|
|
||||||
|
if (blockedPullRequests.length > 0) {
|
||||||
|
if (dryRun) {
|
||||||
|
core.warning(
|
||||||
|
`Dry run found contribution policy failures for PR(s): ${blockedPullRequests
|
||||||
|
.map((result) => `#${result.number}`)
|
||||||
|
.join(", ")}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setFailed(
|
||||||
|
`Contribution policy failed for PR(s): ${blockedPullRequests
|
||||||
|
.map((result) => `#${result.number}`)
|
||||||
|
.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
analyzePullRequest,
|
||||||
|
run,
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: Contribution Policy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
description: Preview labels and comments without changing PRs
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check contribution policy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout policy script
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Check contribution policy
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const { run } = require("./.github/scripts/check-contribution-policy.js");
|
||||||
|
await run({ github, context, core });
|
||||||
Generated
+9
-9
@@ -215,7 +215,7 @@ dependencies = [
|
|||||||
"objc2-foundation 0.3.1",
|
"objc2-foundation 0.3.1",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
"wl-clipboard-rs",
|
"wl-clipboard-rs",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
@@ -1151,7 +1151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static 1.5.0",
|
"lazy_static 1.5.0",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1970,7 +1970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6534,7 +6534,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6547,7 +6547,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.9.4",
|
"linux-raw-sys 0.9.4",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7508,9 +7508,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tar"
|
name = "tar"
|
||||||
version = "0.4.45"
|
version = "0.4.46"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
|
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"filetime",
|
"filetime",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -7988,7 +7988,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.3",
|
"getrandom 0.3.3",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.0.7",
|
"rustix 1.0.7",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9317,7 +9317,7 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Banner, VStack } from "@yaakapp-internal/ui";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
||||||
import { appInfo } from "../lib/appInfo";
|
import { appInfo } from "../lib/appInfo";
|
||||||
|
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||||
import { showErrorToast } from "../lib/toast";
|
import { showErrorToast } from "../lib/toast";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
@@ -89,6 +90,8 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
|
|||||||
</Banner>
|
</Banner>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<CommercialUseBanner source="git-clone" title="Using Git for work?" />
|
||||||
|
|
||||||
<PlainInput
|
<PlainInput
|
||||||
required
|
required
|
||||||
label="Repository URL"
|
label="Repository URL"
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
|
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useKeyValue } from "../hooks/useKeyValue";
|
||||||
|
import { appInfo } from "../lib/appInfo";
|
||||||
|
import { pricingUrl } from "../lib/pricingUrl";
|
||||||
|
import { DismissibleBanner } from "./core/DismissibleBanner";
|
||||||
|
|
||||||
|
const COMMERCIAL_USE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
const COMMERCIAL_USE_BANNER_MESSAGE =
|
||||||
|
"Personal use of Yaak is free. If you’re using Yaak at work, please purchase a license.";
|
||||||
|
|
||||||
|
export function CommercialUseBanner({
|
||||||
|
source,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
source: string;
|
||||||
|
title: string;
|
||||||
|
}) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const snoozeStartedRef = useRef(false);
|
||||||
|
const {
|
||||||
|
isLoading: isSnoozeLoading,
|
||||||
|
set: setSnoozedAt,
|
||||||
|
value: snoozedAt,
|
||||||
|
} = useKeyValue<string | null>({
|
||||||
|
namespace: "global",
|
||||||
|
key: "commercial-use-banner-snoozed-at",
|
||||||
|
fallback: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let canceled = false;
|
||||||
|
|
||||||
|
shouldShowCommercialUsePrompt()
|
||||||
|
.then((shouldShow) => {
|
||||||
|
if (!canceled) setVisible(shouldShow);
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
};
|
||||||
|
}, [source]);
|
||||||
|
|
||||||
|
const snoozed = isSnoozed(snoozedAt, COMMERCIAL_USE_SNOOZE_MS);
|
||||||
|
const handleShow = useCallback(() => {
|
||||||
|
if (snoozeStartedRef.current || snoozed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
snoozeStartedRef.current = true;
|
||||||
|
setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() })).catch(console.error);
|
||||||
|
}, [setSnoozedAt, snoozed, source]);
|
||||||
|
|
||||||
|
if (!visible || isSnoozeLoading || snoozed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<DismissibleBanner
|
||||||
|
id={`commercial-use:${source}`}
|
||||||
|
color="info"
|
||||||
|
className="w-full"
|
||||||
|
onDismiss={() =>
|
||||||
|
setSnoozedAt(JSON.stringify({ source, at: new Date().toISOString() }))
|
||||||
|
}
|
||||||
|
onShow={handleShow}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: "Purchase License",
|
||||||
|
color: "info",
|
||||||
|
variant: "solid",
|
||||||
|
onClick: () => {
|
||||||
|
openCommercialUsePricing(source).catch(console.error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-semibold text-text">{title}</p>
|
||||||
|
<p className="mt-0.5 text-text-subtle">{COMMERCIAL_USE_BANNER_MESSAGE}</p>
|
||||||
|
</div>
|
||||||
|
</DismissibleBanner>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shouldShowCommercialUsePrompt(): Promise<boolean> {
|
||||||
|
// Open-source builds omit the Rust license plugin, so never show commercial-use prompts there.
|
||||||
|
if (appInfo.featureLicense !== true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const license = await invoke<LicenseCheckStatus>("plugin:yaak-license|check");
|
||||||
|
return license.status !== "active" && license.status !== "trialing";
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Failed to check license before commercial-use prompt", err);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openCommercialUsePricing(source: string): Promise<void> {
|
||||||
|
await openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSnoozed(value: string | null, ms: number): boolean {
|
||||||
|
if (value == null) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const snooze = JSON.parse(value) as { at?: unknown };
|
||||||
|
const at = typeof snooze.at === "string" ? snooze.at : null;
|
||||||
|
return isWithinMs(at, ms);
|
||||||
|
} catch {
|
||||||
|
// Older builds stored only the timestamp, so keep respecting that as a global snooze.
|
||||||
|
return isWithinMs(value, ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWithinMs(date: string | null, ms: number): boolean {
|
||||||
|
if (date == null) return false;
|
||||||
|
|
||||||
|
const time = new Date(date).getTime();
|
||||||
|
if (Number.isNaN(time)) return false;
|
||||||
|
|
||||||
|
return Date.now() - time < ms;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import slugify from "slugify";
|
|||||||
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
||||||
import { pluralizeCount } from "../lib/pluralize";
|
import { pluralizeCount } from "../lib/pluralize";
|
||||||
import { invokeCmd } from "../lib/tauri";
|
import { invokeCmd } from "../lib/tauri";
|
||||||
|
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { Checkbox } from "./core/Checkbox";
|
import { Checkbox } from "./core/Checkbox";
|
||||||
import { DetailsBanner } from "./core/DetailsBanner";
|
import { DetailsBanner } from "./core/DetailsBanner";
|
||||||
@@ -85,8 +86,10 @@ function ExportDataDialogContent({
|
|||||||
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
||||||
const noneSelected = numSelected === 0;
|
const noneSelected = numSelected === 0;
|
||||||
return (
|
return (
|
||||||
<div className="w-full grid grid-rows-[minmax(0,1fr)_auto]">
|
<div className="h-full w-full grid grid-rows-[minmax(0,1fr)_auto] overflow-hidden rounded-b-lg">
|
||||||
<VStack space={3} className="overflow-auto px-5 pb-6">
|
<VStack space={3} className="overflow-auto px-5 pb-6">
|
||||||
|
<CommercialUseBanner source="data-export" title="Exporting work data?" />
|
||||||
|
|
||||||
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -137,9 +140,9 @@ function ExportDataDialogContent({
|
|||||||
/>
|
/>
|
||||||
</DetailsBanner>
|
</DetailsBanner>
|
||||||
</VStack>
|
</VStack>
|
||||||
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface-highlight py-2 border-t border-border-subtle">
|
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface py-3 border-t border-border-subtle">
|
||||||
<div>
|
<div>
|
||||||
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtle">
|
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtlest">
|
||||||
Create Run Button
|
Create Run Button
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ import { HStack, Icon, InlineCode } from "@yaakapp-internal/ui";
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { openFolderSettings } from "../commands/openFolderSettings";
|
import { openFolderSettings } from "../commands/openFolderSettings";
|
||||||
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
||||||
|
import { useAuthDropdownOptions } from "../hooks/useAuthTab";
|
||||||
import { useHttpAuthenticationConfig } from "../hooks/useHttpAuthenticationConfig";
|
import { useHttpAuthenticationConfig } from "../hooks/useHttpAuthenticationConfig";
|
||||||
import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication";
|
import { useInheritedAuthentication } from "../hooks/useInheritedAuthentication";
|
||||||
import { useRenderTemplate } from "../hooks/useRenderTemplate";
|
import { useRenderTemplate } from "../hooks/useRenderTemplate";
|
||||||
import { resolvedModelName } from "../lib/resolvedModelName";
|
import { resolvedModelName } from "../lib/resolvedModelName";
|
||||||
|
import { Button } from "./core/Button";
|
||||||
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
import { Dropdown, type DropdownItem } from "./core/Dropdown";
|
||||||
import { IconButton } from "./core/IconButton";
|
import { IconButton } from "./core/IconButton";
|
||||||
import { Input, type InputProps } from "./core/Input";
|
import { Input, type InputProps } from "./core/Input";
|
||||||
import { Link } from "./core/Link";
|
import { Link } from "./core/Link";
|
||||||
|
import { RadioDropdown } from "./core/RadioDropdown";
|
||||||
import { SegmentedControl } from "./core/SegmentedControl";
|
import { SegmentedControl } from "./core/SegmentedControl";
|
||||||
import { DynamicForm } from "./DynamicForm";
|
import { DynamicForm } from "./DynamicForm";
|
||||||
import { EmptyStateText } from "./EmptyStateText";
|
import { EmptyStateText } from "./EmptyStateText";
|
||||||
@@ -35,7 +38,8 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
async (authentication: Record<string, unknown>) => await patchModel(model, { authentication }),
|
async (authentication: Record<string, unknown>) =>
|
||||||
|
await patchModel(model, { authentication }),
|
||||||
[model],
|
[model],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -47,7 +51,8 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
return (
|
return (
|
||||||
<EmptyStateText>
|
<EmptyStateText>
|
||||||
<p>
|
<p>
|
||||||
Auth plugin not found for <InlineCode>{model.authenticationType}</InlineCode>
|
Auth plugin not found for{" "}
|
||||||
|
<InlineCode>{model.authenticationType}</InlineCode>
|
||||||
</p>
|
</p>
|
||||||
</EmptyStateText>
|
</EmptyStateText>
|
||||||
);
|
);
|
||||||
@@ -56,11 +61,20 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
if (inheritedAuth == null) {
|
if (inheritedAuth == null) {
|
||||||
if (model.model === "workspace" || model.model === "folder") {
|
if (model.model === "workspace" || model.model === "folder") {
|
||||||
return (
|
return (
|
||||||
<EmptyStateText className="flex-col gap-1">
|
<EmptyStateText className="flex-col gap-3">
|
||||||
<p>
|
<div className="not-italic flex flex-col items-center gap-3 text-center">
|
||||||
Apply auth to all requests in <strong>{resolvedModelName(model)}</strong>
|
<p className="max-w-md text-sm text-text-subtle">
|
||||||
</p>
|
Choose an auth method to apply it to all requests in{" "}
|
||||||
<Link href="https://yaak.app/docs/using-yaak/request-inheritance">Documentation</Link>
|
<strong className="font-semibold text-text-subtle">
|
||||||
|
{resolvedModelName(model)}
|
||||||
|
</strong>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<AuthenticationTypeDropdown model={model} />
|
||||||
|
<Link href="https://yaak.app/docs/using-yaak/request-inheritance">
|
||||||
|
Documentation
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</EmptyStateText>
|
</EmptyStateText>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -83,7 +97,8 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="underline hover:text-text"
|
className="underline hover:text-text"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (inheritedAuth.model === "folder") openFolderSettings(inheritedAuth.id, "auth");
|
if (inheritedAuth.model === "folder")
|
||||||
|
openFolderSettings(inheritedAuth.id, "auth");
|
||||||
else openWorkspaceSettings("auth");
|
else openWorkspaceSettings("auth");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -103,7 +118,8 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
hideLabel
|
hideLabel
|
||||||
name="enabled"
|
name="enabled"
|
||||||
value={
|
value={
|
||||||
model.authentication.disabled === false || model.authentication.disabled == null
|
model.authentication.disabled === false ||
|
||||||
|
model.authentication.disabled == null
|
||||||
? "__TRUE__"
|
? "__TRUE__"
|
||||||
: model.authentication.disabled === true
|
: model.authentication.disabled === true
|
||||||
? "__FALSE__"
|
? "__FALSE__"
|
||||||
@@ -151,7 +167,9 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
stateKey={`auth.${model.id}.dynamic`}
|
stateKey={`auth.${model.id}.dynamic`}
|
||||||
value={model.authentication.disabled}
|
value={model.authentication.disabled}
|
||||||
onChange={(v) => handleChange({ ...model.authentication, disabled: v })}
|
onChange={(v) =>
|
||||||
|
handleChange({ ...model.authentication, disabled: v })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -169,6 +187,33 @@ export function HttpAuthenticationEditor({ model }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AuthenticationTypeDropdown({ model }: Props) {
|
||||||
|
const options = useAuthDropdownOptions(model);
|
||||||
|
|
||||||
|
if (options == null) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioDropdown
|
||||||
|
items={options.items}
|
||||||
|
itemsAfter={options.itemsAfter}
|
||||||
|
itemsBefore={options.itemsBefore}
|
||||||
|
value={options.value}
|
||||||
|
onChange={options.onChange}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
variant="border"
|
||||||
|
size="sm"
|
||||||
|
rightSlot={
|
||||||
|
<Icon icon="chevron_down" size="sm" className="text-text-subtle" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Select Auth
|
||||||
|
</Button>
|
||||||
|
</RadioDropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AuthenticationDisabledInput({
|
function AuthenticationDisabledInput({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -198,7 +243,11 @@ function AuthenticationDisabledInput({
|
|||||||
rightSlot={
|
rightSlot={
|
||||||
<div className="px-1 flex items-center">
|
<div className="px-1 flex items-center">
|
||||||
<div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap">
|
<div className="rounded-full bg-surface-highlight text-xs px-1.5 py-0.5 text-text-subtle whitespace-nowrap">
|
||||||
{rendered.isPending ? "loading" : rendered.data ? "enabled" : "disabled"}
|
{rendered.isPending
|
||||||
|
? "loading"
|
||||||
|
: rendered.data
|
||||||
|
? "enabled"
|
||||||
|
: "disabled"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { VStack } from "@yaakapp-internal/ui";
|
import { VStack } from "@yaakapp-internal/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useLocalStorage } from "react-use";
|
import { useLocalStorage } from "react-use";
|
||||||
|
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||||
import { Button } from "./core/Button";
|
import { Button } from "./core/Button";
|
||||||
import { SelectFile } from "./SelectFile";
|
import { SelectFile } from "./SelectFile";
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ export function ImportDataDialog({ importData }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack space={5} className="pb-4">
|
<VStack space={5} className="pb-4">
|
||||||
|
<CommercialUseBanner source="data-import" title="Importing work data?" />
|
||||||
|
|
||||||
<VStack space={1}>
|
<VStack space={1}>
|
||||||
<ul className="list-disc pl-5">
|
<ul className="list-disc pl-5">
|
||||||
<li>OpenAPI 3.0, 3.1</li>
|
<li>OpenAPI 3.0, 3.1</li>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
modelSupportsSetting,
|
modelSupportsSetting,
|
||||||
type RequestSettingDefinition,
|
type RequestSettingDefinition,
|
||||||
SETTING_FOLLOW_REDIRECTS,
|
SETTING_FOLLOW_REDIRECTS,
|
||||||
|
SETTING_REQUEST_MESSAGE_SIZE,
|
||||||
SETTING_REQUEST_TIMEOUT,
|
SETTING_REQUEST_TIMEOUT,
|
||||||
SETTING_SEND_COOKIES,
|
SETTING_SEND_COOKIES,
|
||||||
SETTING_STORE_COOKIES,
|
SETTING_STORE_COOKIES,
|
||||||
@@ -22,21 +23,44 @@ import { Checkbox } from "./core/Checkbox";
|
|||||||
import { PlainInput } from "./core/PlainInput";
|
import { PlainInput } from "./core/PlainInput";
|
||||||
import {
|
import {
|
||||||
SettingOverrideRow,
|
SettingOverrideRow,
|
||||||
|
SettingRow,
|
||||||
SettingRowBoolean,
|
SettingRowBoolean,
|
||||||
SettingRowNumber,
|
|
||||||
SettingsList,
|
SettingsList,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from "./core/SettingRow";
|
} from "./core/SettingRow";
|
||||||
|
|
||||||
|
const BYTES_PER_MB = 1024 * 1024;
|
||||||
|
const MAX_REQUEST_MESSAGE_SIZE_BYTES = 2_147_483_647;
|
||||||
|
const MAX_MESSAGE_SIZE_MB = MAX_REQUEST_MESSAGE_SIZE_BYTES / BYTES_PER_MB;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showSectionTitles?: boolean;
|
showSectionTitles?: boolean;
|
||||||
model: ModelWithSettings;
|
model: ModelWithSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModelWithSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
|
type ModelWithSettings =
|
||||||
|
| Workspace
|
||||||
|
| Folder
|
||||||
|
| HttpRequest
|
||||||
|
| WebsocketRequest
|
||||||
|
| GrpcRequest;
|
||||||
type ModelWithHttpSettings = Workspace | Folder | HttpRequest;
|
type ModelWithHttpSettings = Workspace | Folder | HttpRequest;
|
||||||
type ModelWithTlsSettings = Workspace | Folder | HttpRequest | WebsocketRequest | GrpcRequest;
|
type ModelWithTlsSettings =
|
||||||
type ModelWithCookieSettings = Workspace | Folder | HttpRequest | WebsocketRequest;
|
| Workspace
|
||||||
|
| Folder
|
||||||
|
| HttpRequest
|
||||||
|
| WebsocketRequest
|
||||||
|
| GrpcRequest;
|
||||||
|
type ModelWithCookieSettings =
|
||||||
|
| Workspace
|
||||||
|
| Folder
|
||||||
|
| HttpRequest
|
||||||
|
| WebsocketRequest;
|
||||||
|
type ModelWithMessageSizeSettings =
|
||||||
|
| Workspace
|
||||||
|
| Folder
|
||||||
|
| WebsocketRequest
|
||||||
|
| GrpcRequest;
|
||||||
type BooleanSetting = boolean | InheritedBoolSetting;
|
type BooleanSetting = boolean | InheritedBoolSetting;
|
||||||
type IntegerSetting = number | InheritedIntSetting;
|
type IntegerSetting = number | InheritedIntSetting;
|
||||||
type CookieSettingsPatch = {
|
type CookieSettingsPatch = {
|
||||||
@@ -50,12 +74,19 @@ type HttpSettingsPatch = {
|
|||||||
type TlsSettingsPatch = {
|
type TlsSettingsPatch = {
|
||||||
settingValidateCertificates?: ModelWithTlsSettings["settingValidateCertificates"];
|
settingValidateCertificates?: ModelWithTlsSettings["settingValidateCertificates"];
|
||||||
};
|
};
|
||||||
|
type MessageSizeSettingsPatch = {
|
||||||
|
settingRequestMessageSize?: ModelWithMessageSizeSettings["settingRequestMessageSize"];
|
||||||
|
};
|
||||||
|
|
||||||
export function ModelSettingsEditor({ model, showSectionTitles = false }: Props) {
|
export function ModelSettingsEditor({
|
||||||
|
model,
|
||||||
|
showSectionTitles = false,
|
||||||
|
}: Props) {
|
||||||
const ancestors = useModelAncestors(model);
|
const ancestors = useModelAncestors(model);
|
||||||
const supportsHttpSettings = modelSupportsHttpSettings(model);
|
const supportsHttpSettings = modelSupportsHttpSettings(model);
|
||||||
const supportsCookieSettings = modelSupportsCookieSettings(model);
|
const supportsCookieSettings = modelSupportsCookieSettings(model);
|
||||||
const supportsTlsSettings = modelSupportsTlsSettings(model);
|
const supportsTlsSettings = modelSupportsTlsSettings(model);
|
||||||
|
const supportsMessageSizeSettings = modelSupportsMessageSizeSettings(model);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsList className="space-y-8">
|
<SettingsList className="space-y-8">
|
||||||
@@ -77,6 +108,22 @@ export function ModelSettingsEditor({ model, showSectionTitles = false }: Props)
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{supportsMessageSizeSettings && (
|
||||||
|
<MessageSizeSettingRow
|
||||||
|
settingDefinition={SETTING_REQUEST_MESSAGE_SIZE}
|
||||||
|
setting={model.settingRequestMessageSize}
|
||||||
|
inheritedValue={resolveInheritedValue(
|
||||||
|
ancestors,
|
||||||
|
SETTING_REQUEST_MESSAGE_SIZE.modelKey,
|
||||||
|
model.settingRequestMessageSize,
|
||||||
|
)}
|
||||||
|
onChange={(settingRequestMessageSize) =>
|
||||||
|
patchMessageSizeSettings(model, {
|
||||||
|
settingRequestMessageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<BooleanSettingRow
|
<BooleanSettingRow
|
||||||
settingDefinition={SETTING_VALIDATE_CERTIFICATES}
|
settingDefinition={SETTING_VALIDATE_CERTIFICATES}
|
||||||
setting={model.settingValidateCertificates}
|
setting={model.settingValidateCertificates}
|
||||||
@@ -110,7 +157,9 @@ export function ModelSettingsEditor({ model, showSectionTitles = false }: Props)
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
{supportsCookieSettings && (
|
{supportsCookieSettings && (
|
||||||
<SettingsSection title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}>
|
<SettingsSection
|
||||||
|
title={supportsTlsSettings || showSectionTitles ? "Cookies" : null}
|
||||||
|
>
|
||||||
<BooleanSettingRow
|
<BooleanSettingRow
|
||||||
settingDefinition={SETTING_SEND_COOKIES}
|
settingDefinition={SETTING_SEND_COOKIES}
|
||||||
setting={model.settingSendCookies}
|
setting={model.settingSendCookies}
|
||||||
@@ -158,46 +207,103 @@ export function countOverriddenSettings(model: ModelWithSettings) {
|
|||||||
settings.push(model.settingFollowRedirects, model.settingRequestTimeout);
|
settings.push(model.settingFollowRedirects, model.settingRequestTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings.filter((setting) => isInheritedSetting(setting) && setting.enabled === true)
|
if (modelSupportsMessageSizeSettings(model)) {
|
||||||
.length;
|
settings.push(model.settingRequestMessageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.filter(
|
||||||
|
(setting) => isInheritedSetting(setting) && setting.enabled === true,
|
||||||
|
).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchCookieSettings(model: ModelWithCookieSettings, patch: Partial<CookieSettingsPatch>) {
|
function patchCookieSettings(
|
||||||
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
model: ModelWithCookieSettings,
|
||||||
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
patch: Partial<CookieSettingsPatch>,
|
||||||
if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>);
|
) {
|
||||||
if (model.model === "websocket_request")
|
switch (model.model) {
|
||||||
return patchModel(model, patch as Partial<WebsocketRequest>);
|
case "workspace":
|
||||||
throw new Error("Unsupported cookie settings model");
|
return patchModel(model, patch as Partial<Workspace>);
|
||||||
|
case "folder":
|
||||||
|
return patchModel(model, patch as Partial<Folder>);
|
||||||
|
case "http_request":
|
||||||
|
return patchModel(model, patch as Partial<HttpRequest>);
|
||||||
|
case "websocket_request":
|
||||||
|
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchHttpSettings(model: ModelWithHttpSettings, patch: Partial<HttpSettingsPatch>) {
|
function patchHttpSettings(
|
||||||
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
model: ModelWithHttpSettings,
|
||||||
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
patch: Partial<HttpSettingsPatch>,
|
||||||
return patchModel(model, patch as Partial<HttpRequest>);
|
) {
|
||||||
|
switch (model.model) {
|
||||||
|
case "workspace":
|
||||||
|
return patchModel(model, patch as Partial<Workspace>);
|
||||||
|
case "folder":
|
||||||
|
return patchModel(model, patch as Partial<Folder>);
|
||||||
|
case "http_request":
|
||||||
|
return patchModel(model, patch as Partial<HttpRequest>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchTlsSettings(model: ModelWithTlsSettings, patch: Partial<TlsSettingsPatch>) {
|
function patchTlsSettings(
|
||||||
if (model.model === "workspace") return patchModel(model, patch as Partial<Workspace>);
|
model: ModelWithTlsSettings,
|
||||||
if (model.model === "folder") return patchModel(model, patch as Partial<Folder>);
|
patch: Partial<TlsSettingsPatch>,
|
||||||
if (model.model === "http_request") return patchModel(model, patch as Partial<HttpRequest>);
|
) {
|
||||||
if (model.model === "websocket_request")
|
switch (model.model) {
|
||||||
return patchModel(model, patch as Partial<WebsocketRequest>);
|
case "workspace":
|
||||||
return patchModel(model, patch as Partial<GrpcRequest>);
|
return patchModel(model, patch as Partial<Workspace>);
|
||||||
|
case "folder":
|
||||||
|
return patchModel(model, patch as Partial<Folder>);
|
||||||
|
case "http_request":
|
||||||
|
return patchModel(model, patch as Partial<HttpRequest>);
|
||||||
|
case "websocket_request":
|
||||||
|
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||||
|
case "grpc_request":
|
||||||
|
return patchModel(model, patch as Partial<GrpcRequest>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelSupportsHttpSettings(model: ModelWithSettings): model is ModelWithHttpSettings {
|
function patchMessageSizeSettings(
|
||||||
|
model: ModelWithMessageSizeSettings,
|
||||||
|
patch: Partial<MessageSizeSettingsPatch>,
|
||||||
|
) {
|
||||||
|
switch (model.model) {
|
||||||
|
case "workspace":
|
||||||
|
return patchModel(model, patch as Partial<Workspace>);
|
||||||
|
case "folder":
|
||||||
|
return patchModel(model, patch as Partial<Folder>);
|
||||||
|
case "websocket_request":
|
||||||
|
return patchModel(model, patch as Partial<WebsocketRequest>);
|
||||||
|
case "grpc_request":
|
||||||
|
return patchModel(model, patch as Partial<GrpcRequest>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelSupportsHttpSettings(
|
||||||
|
model: ModelWithSettings,
|
||||||
|
): model is ModelWithHttpSettings {
|
||||||
return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT);
|
return modelSupportsSetting(model, SETTING_REQUEST_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelSupportsCookieSettings(model: ModelWithSettings): model is ModelWithCookieSettings {
|
function modelSupportsCookieSettings(
|
||||||
|
model: ModelWithSettings,
|
||||||
|
): model is ModelWithCookieSettings {
|
||||||
return modelSupportsSetting(model, SETTING_SEND_COOKIES);
|
return modelSupportsSetting(model, SETTING_SEND_COOKIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelSupportsTlsSettings(model: ModelWithSettings): model is ModelWithTlsSettings {
|
function modelSupportsTlsSettings(
|
||||||
|
model: ModelWithSettings,
|
||||||
|
): model is ModelWithTlsSettings {
|
||||||
return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES);
|
return modelSupportsSetting(model, SETTING_VALIDATE_CERTIFICATES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modelSupportsMessageSizeSettings(
|
||||||
|
model: ModelWithSettings,
|
||||||
|
): model is ModelWithMessageSizeSettings {
|
||||||
|
return modelSupportsSetting(model, SETTING_REQUEST_MESSAGE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
function BooleanSettingRow({
|
function BooleanSettingRow({
|
||||||
inheritedValue,
|
inheritedValue,
|
||||||
setting,
|
setting,
|
||||||
@@ -211,7 +317,11 @@ function BooleanSettingRow({
|
|||||||
}) {
|
}) {
|
||||||
const inherited = isInheritedSetting(setting);
|
const inherited = isInheritedSetting(setting);
|
||||||
const overridden = inherited ? setting.enabled === true : false;
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
const value = inherited
|
||||||
|
? overridden
|
||||||
|
? setting.value
|
||||||
|
: inheritedValue
|
||||||
|
: setting;
|
||||||
|
|
||||||
if (!inherited) {
|
if (!inherited) {
|
||||||
return (
|
return (
|
||||||
@@ -255,19 +365,28 @@ function IntegerSettingRow({
|
|||||||
}) {
|
}) {
|
||||||
const inherited = isInheritedSetting(setting);
|
const inherited = isInheritedSetting(setting);
|
||||||
const overridden = inherited ? setting.enabled === true : false;
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
const value = inherited ? (overridden ? setting.value : inheritedValue) : setting;
|
const value = inherited
|
||||||
|
? overridden
|
||||||
|
? setting.value
|
||||||
|
: inheritedValue
|
||||||
|
: setting;
|
||||||
|
|
||||||
if (!inherited) {
|
if (!inherited) {
|
||||||
return (
|
return (
|
||||||
<SettingRowNumber
|
<SettingRow
|
||||||
name={settingDefinition.modelKey}
|
|
||||||
title={settingDefinition.title}
|
title={settingDefinition.title}
|
||||||
description={settingDefinition.description}
|
description={settingDefinition.description}
|
||||||
value={value}
|
>
|
||||||
placeholder={`${settingDefinition.defaultValue}`}
|
<NumberUnitInput
|
||||||
validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
|
name={settingDefinition.modelKey}
|
||||||
onChange={(value) => onChange(value)}
|
label={settingDefinition.title}
|
||||||
/>
|
unit="ms"
|
||||||
|
value={`${value}`}
|
||||||
|
placeholder={`${settingDefinition.defaultValue}`}
|
||||||
|
validate={isValidInteger}
|
||||||
|
onChange={(value) => onChange(parseInteger(value))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,21 +397,18 @@ function IntegerSettingRow({
|
|||||||
overridden={overridden}
|
overridden={overridden}
|
||||||
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||||
>
|
>
|
||||||
<PlainInput
|
<NumberUnitInput
|
||||||
hideLabel
|
|
||||||
name={settingDefinition.modelKey}
|
name={settingDefinition.modelKey}
|
||||||
label={settingDefinition.title}
|
label={settingDefinition.title}
|
||||||
size="sm"
|
unit="ms"
|
||||||
type="number"
|
value={`${value}`}
|
||||||
placeholder={`${settingDefinition.defaultValue}`}
|
placeholder={`${settingDefinition.defaultValue}`}
|
||||||
defaultValue={`${value}`}
|
validate={isValidInteger}
|
||||||
containerClassName="!w-48"
|
|
||||||
validate={(value) => value === "" || Number.parseInt(value, 10) >= 0}
|
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
onChange({
|
onChange({
|
||||||
...setting,
|
...setting,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
value: Number.parseInt(value, 10) || 0,
|
value: parseInteger(value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -300,6 +416,141 @@ function IntegerSettingRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MessageSizeSettingRow({
|
||||||
|
inheritedValue,
|
||||||
|
setting,
|
||||||
|
settingDefinition,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
inheritedValue: number;
|
||||||
|
setting: IntegerSetting;
|
||||||
|
settingDefinition: RequestSettingDefinition<"settingRequestMessageSize">;
|
||||||
|
onChange: (setting: IntegerSetting) => void;
|
||||||
|
}) {
|
||||||
|
const inherited = isInheritedSetting(setting);
|
||||||
|
const overridden = inherited ? setting.enabled === true : false;
|
||||||
|
const value = inherited
|
||||||
|
? overridden
|
||||||
|
? setting.value
|
||||||
|
: inheritedValue
|
||||||
|
: setting;
|
||||||
|
const displayValue = formatMegabytes(value);
|
||||||
|
const placeholder = "0";
|
||||||
|
|
||||||
|
if (!inherited) {
|
||||||
|
return (
|
||||||
|
<SettingRow
|
||||||
|
title={settingDefinition.title}
|
||||||
|
description={settingDefinition.description}
|
||||||
|
>
|
||||||
|
<MessageSizeInput
|
||||||
|
name={settingDefinition.modelKey}
|
||||||
|
label={settingDefinition.title}
|
||||||
|
value={displayValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(value) => onChange(parseMegabytes(value))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingOverrideRow
|
||||||
|
title={settingDefinition.title}
|
||||||
|
description={settingDefinition.description}
|
||||||
|
overridden={overridden}
|
||||||
|
onResetOverride={() => onChange({ ...setting, enabled: false })}
|
||||||
|
>
|
||||||
|
<MessageSizeInput
|
||||||
|
name={settingDefinition.modelKey}
|
||||||
|
label={settingDefinition.title}
|
||||||
|
value={displayValue}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(value) =>
|
||||||
|
onChange({
|
||||||
|
...setting,
|
||||||
|
enabled: true,
|
||||||
|
value: parseMegabytes(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingOverrideRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MessageSizeInput({
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder: string;
|
||||||
|
value: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NumberUnitInput
|
||||||
|
name={name}
|
||||||
|
label={label}
|
||||||
|
unit="MB"
|
||||||
|
value={value}
|
||||||
|
inputMode="decimal"
|
||||||
|
step="any"
|
||||||
|
placeholder={placeholder}
|
||||||
|
validate={isValidMegabytes}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NumberUnitInput({
|
||||||
|
inputMode,
|
||||||
|
label,
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
step,
|
||||||
|
unit,
|
||||||
|
validate,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
inputMode?: "decimal" | "numeric";
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder: string;
|
||||||
|
step?: number | "any";
|
||||||
|
unit: string;
|
||||||
|
validate: (value: string) => boolean;
|
||||||
|
value: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<PlainInput
|
||||||
|
hideLabel
|
||||||
|
name={name}
|
||||||
|
label={label}
|
||||||
|
size="sm"
|
||||||
|
type="number"
|
||||||
|
inputMode={inputMode}
|
||||||
|
step={step}
|
||||||
|
placeholder={placeholder}
|
||||||
|
defaultValue={value}
|
||||||
|
className="[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||||
|
containerClassName="!w-48"
|
||||||
|
validate={validate}
|
||||||
|
rightSlot={
|
||||||
|
<span className="flex self-stretch items-center border-l border-border-subtle px-2 text-xs font-medium text-text-subtle">
|
||||||
|
{unit}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isInheritedSetting<T>(
|
function isInheritedSetting<T>(
|
||||||
setting: T | { enabled?: boolean; value: T },
|
setting: T | { enabled?: boolean; value: T },
|
||||||
): setting is { enabled?: boolean; value: T } {
|
): setting is { enabled?: boolean; value: T } {
|
||||||
@@ -308,7 +559,7 @@ function isInheritedSetting<T>(
|
|||||||
|
|
||||||
function resolveInheritedValue(
|
function resolveInheritedValue(
|
||||||
ancestors: (Folder | Workspace)[],
|
ancestors: (Folder | Workspace)[],
|
||||||
key: "settingRequestTimeout",
|
key: "settingRequestTimeout" | "settingRequestMessageSize",
|
||||||
fallback: IntegerSetting,
|
fallback: IntegerSetting,
|
||||||
): number;
|
): number;
|
||||||
function resolveInheritedValue(
|
function resolveInheritedValue(
|
||||||
@@ -338,10 +589,46 @@ function resolveInheritedValue(
|
|||||||
type WorkspaceSettings = Pick<
|
type WorkspaceSettings = Pick<
|
||||||
Workspace,
|
Workspace,
|
||||||
| "settingFollowRedirects"
|
| "settingFollowRedirects"
|
||||||
|
| "settingRequestMessageSize"
|
||||||
| "settingRequestTimeout"
|
| "settingRequestTimeout"
|
||||||
| "settingSendCookies"
|
| "settingSendCookies"
|
||||||
| "settingStoreCookies"
|
| "settingStoreCookies"
|
||||||
| "settingValidateCertificates"
|
| "settingValidateCertificates"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type BooleanWorkspaceSettingKey = Exclude<keyof WorkspaceSettings, "settingRequestTimeout">;
|
type BooleanWorkspaceSettingKey = Exclude<
|
||||||
|
keyof WorkspaceSettings,
|
||||||
|
"settingRequestTimeout" | "settingRequestMessageSize"
|
||||||
|
>;
|
||||||
|
|
||||||
|
function formatMegabytes(bytes: number) {
|
||||||
|
const megabytes = bytes / BYTES_PER_MB;
|
||||||
|
return Number.isInteger(megabytes)
|
||||||
|
? `${megabytes}`
|
||||||
|
: megabytes.toFixed(3).replace(/\.?0+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMegabytes(value: string) {
|
||||||
|
const megabytes = Number(value);
|
||||||
|
return Number.isFinite(megabytes) ? Math.round(megabytes * BYTES_PER_MB) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInteger(value: string) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? Math.trunc(parsed) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidInteger(value: string) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return value === "" || (Number.isInteger(parsed) && parsed >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidMegabytes(value: string) {
|
||||||
|
if (value === "") return true;
|
||||||
|
const megabytes = Number(value);
|
||||||
|
return (
|
||||||
|
Number.isFinite(megabytes) &&
|
||||||
|
megabytes >= 0 &&
|
||||||
|
megabytes <= MAX_MESSAGE_SIZE_MB
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
|
|||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { showConfirmDelete } from "../../lib/confirm";
|
import { showConfirmDelete } from "../../lib/confirm";
|
||||||
|
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import { Checkbox } from "../core/Checkbox";
|
import { Checkbox } from "../core/Checkbox";
|
||||||
import { DetailsBanner } from "../core/DetailsBanner";
|
import { DetailsBanner } from "../core/DetailsBanner";
|
||||||
@@ -232,6 +233,8 @@ export function SettingsCertificates() {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<CommercialUseBanner source="client-certificates" title="Using certificates for work?" />
|
||||||
|
|
||||||
{certificates.length > 0 && (
|
{certificates.length > 0 && (
|
||||||
<VStack space={3}>
|
<VStack space={3}>
|
||||||
{certificates.map((cert, index) => (
|
{certificates.map((cert, index) => (
|
||||||
|
|||||||
@@ -2,22 +2,15 @@ import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
|||||||
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||||
import { Heading, VStack } from "@yaakapp-internal/ui";
|
import { Heading, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
|
||||||
import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
|
import { useCheckForUpdates } from "../../hooks/useCheckForUpdates";
|
||||||
import { appInfo } from "../../lib/appInfo";
|
import { appInfo } from "../../lib/appInfo";
|
||||||
import {
|
|
||||||
SETTING_FOLLOW_REDIRECTS,
|
|
||||||
SETTING_REQUEST_TIMEOUT,
|
|
||||||
SETTING_SEND_COOKIES,
|
|
||||||
SETTING_STORE_COOKIES,
|
|
||||||
SETTING_VALIDATE_CERTIFICATES,
|
|
||||||
} from "../../lib/requestSettings";
|
|
||||||
import { revealInFinderText } from "../../lib/reveal";
|
import { revealInFinderText } from "../../lib/reveal";
|
||||||
import { CargoFeature } from "../CargoFeature";
|
import { CargoFeature } from "../CargoFeature";
|
||||||
|
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||||
|
import { DismissibleBanner } from "../core/DismissibleBanner";
|
||||||
import { IconButton } from "../core/IconButton";
|
import { IconButton } from "../core/IconButton";
|
||||||
import {
|
import {
|
||||||
ModelSettingRowBoolean,
|
ModelSettingRowBoolean,
|
||||||
ModelSettingRowNumber,
|
|
||||||
ModelSettingSelectControl,
|
ModelSettingSelectControl,
|
||||||
SettingValue,
|
SettingValue,
|
||||||
SettingRow,
|
SettingRow,
|
||||||
@@ -27,20 +20,29 @@ import {
|
|||||||
SettingsSection,
|
SettingsSection,
|
||||||
} from "../core/SettingRow";
|
} from "../core/SettingRow";
|
||||||
|
|
||||||
|
const WORKSPACE_SETTINGS_MOVED_AT = "2026-06-30";
|
||||||
|
|
||||||
export function SettingsGeneral() {
|
export function SettingsGeneral() {
|
||||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
const checkForUpdates = useCheckForUpdates();
|
const checkForUpdates = useCheckForUpdates();
|
||||||
|
|
||||||
if (settings == null || workspace == null) {
|
if (settings == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showWorkspaceSettingsMovedBanner =
|
||||||
|
settings.createdAt.slice(0, 10) < WORKSPACE_SETTINGS_MOVED_AT;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack space={1.5} className="mb-4">
|
<VStack space={1.5} className="mb-4">
|
||||||
<div className="mb-4">
|
<div>
|
||||||
<Heading>General</Heading>
|
<Heading>General</Heading>
|
||||||
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
<p className="text-text-subtle">
|
||||||
|
Configure general settings for update behavior and more.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 mb-5">
|
||||||
|
<CommercialUseBanner source="settings-general" title="Using Yaak for work?" />
|
||||||
</div>
|
</div>
|
||||||
<SettingsList className="space-y-8">
|
<SettingsList className="space-y-8">
|
||||||
<CargoFeature feature="updater">
|
<CargoFeature feature="updater">
|
||||||
@@ -76,7 +78,9 @@ export function SettingsGeneral() {
|
|||||||
description="Choose whether updates are installed automatically or manually."
|
description="Choose whether updates are installed automatically or manually."
|
||||||
name="autoupdate"
|
name="autoupdate"
|
||||||
value={settings.autoupdate ? "auto" : "manual"}
|
value={settings.autoupdate ? "auto" : "manual"}
|
||||||
onChange={(v) => patchModel(settings, { autoupdate: v === "auto" })}
|
onChange={(v) =>
|
||||||
|
patchModel(settings, { autoupdate: v === "auto" })
|
||||||
|
}
|
||||||
options={[
|
options={[
|
||||||
{ label: "Automatic", value: "auto" },
|
{ label: "Automatic", value: "auto" },
|
||||||
{ label: "Manual", value: "manual" },
|
{ label: "Manual", value: "manual" },
|
||||||
@@ -108,54 +112,19 @@ export function SettingsGeneral() {
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</CargoFeature>
|
</CargoFeature>
|
||||||
|
|
||||||
<SettingsSection
|
{showWorkspaceSettingsMovedBanner && (
|
||||||
title={
|
<DismissibleBanner
|
||||||
<>
|
id="workspace-settings-moved-2026-06-30"
|
||||||
Workspace{" "}
|
color="info"
|
||||||
<span className="inline-block bg-surface-highlight px-2 py-0.5 rounded text">
|
className="p-4 max-w-xl mx-auto"
|
||||||
{workspace.name}
|
>
|
||||||
</span>
|
<p>
|
||||||
</>
|
Workspace specific settings have moved to{" "}
|
||||||
}
|
<b>Workspace Settings</b>, accessible from the workspace switcher
|
||||||
>
|
menu.
|
||||||
<ModelSettingRowNumber
|
</p>
|
||||||
model={workspace}
|
</DismissibleBanner>
|
||||||
modelKey={SETTING_REQUEST_TIMEOUT.modelKey}
|
)}
|
||||||
title={SETTING_REQUEST_TIMEOUT.title}
|
|
||||||
description={SETTING_REQUEST_TIMEOUT.description}
|
|
||||||
placeholder={`${SETTING_REQUEST_TIMEOUT.defaultValue}`}
|
|
||||||
required
|
|
||||||
validate={(value) => Number.parseInt(value, 10) >= 0}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ModelSettingRowBoolean
|
|
||||||
model={workspace}
|
|
||||||
modelKey={SETTING_VALIDATE_CERTIFICATES.modelKey}
|
|
||||||
title={SETTING_VALIDATE_CERTIFICATES.title}
|
|
||||||
description={SETTING_VALIDATE_CERTIFICATES.description}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ModelSettingRowBoolean
|
|
||||||
model={workspace}
|
|
||||||
modelKey={SETTING_FOLLOW_REDIRECTS.modelKey}
|
|
||||||
title={SETTING_FOLLOW_REDIRECTS.title}
|
|
||||||
description={SETTING_FOLLOW_REDIRECTS.description}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ModelSettingRowBoolean
|
|
||||||
model={workspace}
|
|
||||||
modelKey={SETTING_SEND_COOKIES.modelKey}
|
|
||||||
title={SETTING_SEND_COOKIES.title}
|
|
||||||
description={SETTING_SEND_COOKIES.description}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ModelSettingRowBoolean
|
|
||||||
model={workspace}
|
|
||||||
modelKey={SETTING_STORE_COOKIES.modelKey}
|
|
||||||
title={SETTING_STORE_COOKIES.title}
|
|
||||||
description={SETTING_STORE_COOKIES.description}
|
|
||||||
/>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<SettingsSection title="App Info">
|
<SettingsSection title="App Info">
|
||||||
<SettingRow title="Version" description="Current Yaak version.">
|
<SettingRow title="Version" description="Current Yaak version.">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAtomValue } from "jotai";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||||
import { showConfirm } from "../../lib/confirm";
|
import { showConfirm } from "../../lib/confirm";
|
||||||
|
import { pricingUrl } from "../../lib/pricingUrl";
|
||||||
import { invokeCmd } from "../../lib/tauri";
|
import { invokeCmd } from "../../lib/tauri";
|
||||||
import { CargoFeature } from "../CargoFeature";
|
import { CargoFeature } from "../CargoFeature";
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
@@ -252,7 +253,9 @@ function LicenseSettings({ settings }: { settings: Settings }) {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Licenses help keep Yaak independent and sustainable.{" "}
|
Licenses help keep Yaak independent and sustainable.{" "}
|
||||||
<Link href="https://yaak.app/pricing?s=badge">Purchase a License →</Link>
|
<Link href={pricingUrl("app.license.badge-hide-confirm")}>
|
||||||
|
Purchase a License →
|
||||||
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</VStack>
|
</VStack>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { formatDate } from "date-fns/format";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useToggle } from "../../hooks/useToggle";
|
import { useToggle } from "../../hooks/useToggle";
|
||||||
import { pluralizeCount } from "../../lib/pluralize";
|
import { pluralizeCount } from "../../lib/pluralize";
|
||||||
|
import { pricingUrl } from "../../lib/pricingUrl";
|
||||||
import { CargoFeature } from "../CargoFeature";
|
import { CargoFeature } from "../CargoFeature";
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import { Link } from "../core/Link";
|
import { Link } from "../core/Link";
|
||||||
@@ -48,7 +49,7 @@ function SettingsLicenseCmp() {
|
|||||||
<span className="opacity-50">Personal use is always free, forever.</span>
|
<span className="opacity-50">Personal use is always free, forever.</span>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
||||||
<Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}>
|
<Link noUnderline href={pricingUrl(`app.license.learn.${check.data.status}`)}>
|
||||||
Learn More
|
Learn More
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +69,7 @@ function SettingsLicenseCmp() {
|
|||||||
</span>
|
</span>
|
||||||
<Separator className="my-2" />
|
<Separator className="my-2" />
|
||||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
||||||
<Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}>
|
<Link noUnderline href={pricingUrl(`app.license.learn.${check.data.status}`)}>
|
||||||
Learn More
|
Learn More
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +135,7 @@ function SettingsLicenseCmp() {
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => openUrl("https://yaak.app/dashboard?s=support&ref=app.yaak.desktop")}
|
onClick={() => openUrl("https://yaak.app/dashboard?intent=app.license.support")}
|
||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
>
|
>
|
||||||
Direct Support
|
Direct Support
|
||||||
@@ -150,9 +151,7 @@ function SettingsLicenseCmp() {
|
|||||||
color="primary"
|
color="primary"
|
||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openUrl(
|
openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`))
|
||||||
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.status ?? ""}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Purchase License
|
Purchase License
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
|||||||
import type { ProxySetting } from "@yaakapp-internal/models";
|
import type { ProxySetting } from "@yaakapp-internal/models";
|
||||||
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
|
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
|
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||||
import {
|
import {
|
||||||
SettingRowBoolean,
|
SettingRowBoolean,
|
||||||
SettingRowSelect,
|
SettingRowSelect,
|
||||||
@@ -33,6 +34,7 @@ export function SettingsProxy() {
|
|||||||
traffic, or routing through specific infrastructure.
|
traffic, or routing through specific infrastructure.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<CommercialUseBanner source="proxy-settings" title="Using a proxy for work?" />
|
||||||
<SettingsList className="space-y-8">
|
<SettingsList className="space-y-8">
|
||||||
<SettingsSection title="Proxy">
|
<SettingsSection title="Proxy">
|
||||||
<SettingRowSelect
|
<SettingRowSelect
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useExportData } from "../hooks/useExportData";
|
|||||||
import { appInfo } from "../lib/appInfo";
|
import { appInfo } from "../lib/appInfo";
|
||||||
import { showDialog } from "../lib/dialog";
|
import { showDialog } from "../lib/dialog";
|
||||||
import { importData } from "../lib/importData";
|
import { importData } from "../lib/importData";
|
||||||
|
import { pricingUrl } from "../lib/pricingUrl";
|
||||||
import type { DropdownRef } from "./core/Dropdown";
|
import type { DropdownRef } from "./core/Dropdown";
|
||||||
import { Dropdown } from "./core/Dropdown";
|
import { Dropdown } from "./core/Dropdown";
|
||||||
import { Icon } from "@yaakapp-internal/ui";
|
import { Icon } from "@yaakapp-internal/ui";
|
||||||
@@ -76,7 +77,8 @@ export function SettingsDropdown() {
|
|||||||
hidden: check.data == null || check.data.status === "active",
|
hidden: check.data == null || check.data.status === "active",
|
||||||
leftSlot: <Icon icon="circle_dollar_sign" />,
|
leftSlot: <Icon icon="circle_dollar_sign" />,
|
||||||
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
|
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
|
||||||
onSelect: () => openUrl("https://yaak.app/pricing"),
|
onSelect: () =>
|
||||||
|
openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Install CLI",
|
label: "Install CLI",
|
||||||
|
|||||||
@@ -105,10 +105,18 @@ function WebsocketEventRow({
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const iconColor =
|
const iconColor =
|
||||||
messageType === "close" || messageType === "open" ? "secondary" : isServer ? "info" : "primary";
|
messageType === "error"
|
||||||
|
? "warning"
|
||||||
|
: messageType === "close" || messageType === "open"
|
||||||
|
? "secondary"
|
||||||
|
: isServer
|
||||||
|
? "info"
|
||||||
|
: "primary";
|
||||||
|
|
||||||
const icon =
|
const icon =
|
||||||
messageType === "close" || messageType === "open"
|
messageType === "error"
|
||||||
|
? "alert_triangle"
|
||||||
|
: messageType === "close" || messageType === "open"
|
||||||
? "info"
|
? "info"
|
||||||
: isServer
|
: isServer
|
||||||
? "arrow_big_down_dash"
|
? "arrow_big_down_dash"
|
||||||
@@ -119,6 +127,8 @@ function WebsocketEventRow({
|
|||||||
"Disconnected from server"
|
"Disconnected from server"
|
||||||
) : messageType === "open" ? (
|
) : messageType === "open" ? (
|
||||||
"Connected to server"
|
"Connected to server"
|
||||||
|
) : messageType === "error" ? (
|
||||||
|
<span className="text-warning">{message}</span>
|
||||||
) : message === "" ? (
|
) : message === "" ? (
|
||||||
<em className="italic text-text-subtlest">No content</em>
|
<em className="italic text-text-subtlest">No content</em>
|
||||||
) : (
|
) : (
|
||||||
@@ -170,7 +180,9 @@ function WebsocketEventDetail({
|
|||||||
? "Connection Closed"
|
? "Connection Closed"
|
||||||
: event.messageType === "open"
|
: event.messageType === "open"
|
||||||
? "Connection Open"
|
? "Connection Open"
|
||||||
: `Message ${event.isServer ? "Received" : "Sent"}`;
|
: event.messageType === "error"
|
||||||
|
? "WebSocket Error"
|
||||||
|
: `Message ${event.isServer ? "Received" : "Sent"}`;
|
||||||
|
|
||||||
const actions: EventDetailAction[] =
|
const actions: EventDetailAction[] =
|
||||||
message !== ""
|
message !== ""
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
onCreateNewWorkspace={hide}
|
onCreateNewWorkspace={hide}
|
||||||
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
|
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
|
||||||
/>
|
/>
|
||||||
<WorkspaceEncryptionSetting layout="settings" size="xs" />
|
<div className="mt-4">
|
||||||
|
<WorkspaceEncryptionSetting layout="settings" size="xs" />
|
||||||
|
</div>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
<ModelSettingsEditor model={workspace} showSectionTitles />
|
<ModelSettingsEditor model={workspace} showSectionTitles />
|
||||||
</SettingsList>
|
</SettingsList>
|
||||||
|
|||||||
@@ -1,57 +1,84 @@
|
|||||||
import type { Color } from "@yaakapp-internal/plugins";
|
import type { Color } from "@yaakapp-internal/plugins";
|
||||||
import type { BannerProps } from "@yaakapp-internal/ui";
|
import type { BannerProps } from "@yaakapp-internal/ui";
|
||||||
import { Banner, HStack } from "@yaakapp-internal/ui";
|
import { Banner } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { useKeyValue } from "../../hooks/useKeyValue";
|
import { useKeyValue } from "../../hooks/useKeyValue";
|
||||||
|
import type { ButtonProps } from "./Button";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
|
|
||||||
export function DismissibleBanner({
|
export function DismissibleBanner({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
id,
|
id,
|
||||||
|
onDismiss,
|
||||||
|
onShow,
|
||||||
actions,
|
actions,
|
||||||
...props
|
...props
|
||||||
}: BannerProps & {
|
}: BannerProps & {
|
||||||
id: string;
|
id: string;
|
||||||
actions?: { label: string; onClick: () => void; color?: Color }[];
|
onDismiss?: () => void | Promise<void>;
|
||||||
|
onShow?: () => void | Promise<void>;
|
||||||
|
actions?: {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
color?: Color;
|
||||||
|
variant?: ButtonProps["variant"];
|
||||||
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
|
const {
|
||||||
|
isLoading,
|
||||||
|
set: setDismissed,
|
||||||
|
value: dismissed,
|
||||||
|
} = useKeyValue<boolean>({
|
||||||
namespace: "global",
|
namespace: "global",
|
||||||
key: ["dismiss-banner", id],
|
key: ["dismiss-banner", id],
|
||||||
fallback: false,
|
fallback: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dismissed) return null;
|
const shouldShow = !isLoading && !dismissed;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldShow) {
|
||||||
|
Promise.resolve(onShow?.()).catch(console.error);
|
||||||
|
}
|
||||||
|
}, [onShow, shouldShow]);
|
||||||
|
|
||||||
|
if (!shouldShow) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Banner
|
<Banner className={classNames(className, "relative")} {...props}>
|
||||||
className={classNames(className, "relative grid grid-cols-[1fr_auto] gap-3")}
|
<div className="@container">
|
||||||
{...props}
|
<div className="grid gap-2 @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center @[34rem]:gap-3">
|
||||||
>
|
{children}
|
||||||
{children}
|
<div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
|
||||||
<HStack space={1.5}>
|
<Button
|
||||||
{actions?.map((a) => (
|
variant="border"
|
||||||
<Button
|
color={props.color}
|
||||||
key={a.label}
|
size="xs"
|
||||||
variant="border"
|
onClick={() => {
|
||||||
color={a.color ?? props.color}
|
setDismissed(true).catch(console.error);
|
||||||
size="xs"
|
Promise.resolve(onDismiss?.()).catch(console.error);
|
||||||
onClick={a.onClick}
|
}}
|
||||||
title={a.label}
|
title="Dismiss message"
|
||||||
>
|
>
|
||||||
{a.label}
|
Dismiss
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
{actions?.map((a) => (
|
||||||
<Button
|
<Button
|
||||||
variant="border"
|
key={a.label}
|
||||||
color={props.color}
|
variant={a.variant ?? "border"}
|
||||||
size="xs"
|
color={a.color ?? props.color}
|
||||||
onClick={() => setDismissed((d) => !d)}
|
size="xs"
|
||||||
title="Dismiss message"
|
onClick={a.onClick}
|
||||||
>
|
title={a.label}
|
||||||
Dismiss
|
>
|
||||||
</Button>
|
{a.label}
|
||||||
</HStack>
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Banner>
|
</Banner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { HStack } from "@yaakapp-internal/ui";
|
import { HStack } from "@yaakapp-internal/ui";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { FocusEvent, HTMLAttributes, ReactNode } from "react";
|
import type { FocusEvent, InputHTMLAttributes, ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -28,10 +28,9 @@ export type PlainInputProps = Omit<
|
|||||||
| "extraExtensions"
|
| "extraExtensions"
|
||||||
| "forcedEnvironmentId"
|
| "forcedEnvironmentId"
|
||||||
> &
|
> &
|
||||||
Pick<HTMLAttributes<HTMLInputElement>, "onKeyDownCapture"> & {
|
Pick<InputHTMLAttributes<HTMLInputElement>, "inputMode" | "onKeyDownCapture" | "step"> & {
|
||||||
onFocusRaw?: HTMLAttributes<HTMLInputElement>["onFocus"];
|
onFocusRaw?: InputHTMLAttributes<HTMLInputElement>["onFocus"];
|
||||||
type?: "text" | "password" | "number";
|
type?: "text" | "password" | "number";
|
||||||
step?: number;
|
|
||||||
hideObscureToggle?: boolean;
|
hideObscureToggle?: boolean;
|
||||||
labelRightSlot?: ReactNode;
|
labelRightSlot?: ReactNode;
|
||||||
};
|
};
|
||||||
@@ -52,6 +51,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
|||||||
labelClassName,
|
labelClassName,
|
||||||
labelPosition = "top",
|
labelPosition = "top",
|
||||||
labelRightSlot,
|
labelRightSlot,
|
||||||
|
inputMode,
|
||||||
leftSlot,
|
leftSlot,
|
||||||
name,
|
name,
|
||||||
onBlur,
|
onBlur,
|
||||||
@@ -64,6 +64,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
|||||||
required,
|
required,
|
||||||
rightSlot,
|
rightSlot,
|
||||||
size = "md",
|
size = "md",
|
||||||
|
step,
|
||||||
tint,
|
tint,
|
||||||
type = "text",
|
type = "text",
|
||||||
validate,
|
validate,
|
||||||
@@ -204,12 +205,14 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
|
inputMode={inputMode}
|
||||||
onChange={(e) => handleChange(e.target.value)}
|
onChange={(e) => handleChange(e.target.value)}
|
||||||
onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))}
|
onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))}
|
||||||
className={classNames(commonClassName, "h-full disabled:opacity-disabled")}
|
className={classNames(commonClassName, "h-full disabled:opacity-disabled")}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
required={required}
|
required={required}
|
||||||
|
step={step}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onKeyDownCapture={onKeyDownCapture}
|
onKeyDownCapture={onKeyDownCapture}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { resolvedModelName } from "../../lib/resolvedModelName";
|
|||||||
import { showConfirm } from "../../lib/confirm";
|
import { showConfirm } from "../../lib/confirm";
|
||||||
import { showErrorToast } from "../../lib/toast";
|
import { showErrorToast } from "../../lib/toast";
|
||||||
import { sync } from "../../init/sync";
|
import { sync } from "../../init/sync";
|
||||||
|
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||||
import { Button } from "../core/Button";
|
import { Button } from "../core/Button";
|
||||||
import type { CheckboxProps } from "../core/Checkbox";
|
import type { CheckboxProps } from "../core/Checkbox";
|
||||||
import { Checkbox } from "../core/Checkbox";
|
import { Checkbox } from "../core/Checkbox";
|
||||||
@@ -205,7 +206,8 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
|||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
defaultRatio={0.6}
|
defaultRatio={0.6}
|
||||||
firstSlot={({ style }) => (
|
firstSlot={({ style }) => (
|
||||||
<div style={style} className="h-full px-4">
|
<div style={style} className="h-full px-4 grid grid-rows-[auto_minmax(0,1fr)] gap-3">
|
||||||
|
<CommercialUseBanner source="git-commit" title="Using Git for work?" />
|
||||||
<SplitLayout
|
<SplitLayout
|
||||||
storageKey="commit-vertical"
|
storageKey="commit-vertical"
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function addGitRemote(dir: string, defaultName?: string): Promise<G
|
|||||||
title: "Add Remote",
|
title: "Add Remote",
|
||||||
inputs: [
|
inputs: [
|
||||||
{ type: "text", label: "Name", name: "name", defaultValue: defaultName },
|
{ type: "text", label: "Name", name: "name", defaultValue: defaultName },
|
||||||
{ type: "text", label: "URL", name: "url" },
|
{ type: "text", label: "URL", name: "url", placeholder: "git@github.com:org/repo.git" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
if (r == null) throw new Error("Cancelled remote prompt");
|
if (r == null) throw new Error("Cancelled remote prompt");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useMemo } from "react";
|
|||||||
import { openFolderSettings } from "../commands/openFolderSettings";
|
import { openFolderSettings } from "../commands/openFolderSettings";
|
||||||
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
import { openWorkspaceSettings } from "../commands/openWorkspaceSettings";
|
||||||
import { IconTooltip } from "../components/core/IconTooltip";
|
import { IconTooltip } from "../components/core/IconTooltip";
|
||||||
|
import type { RadioDropdownProps } from "../components/core/RadioDropdown";
|
||||||
import type { TabItem } from "../components/core/Tabs/Tabs";
|
import type { TabItem } from "../components/core/Tabs/Tabs";
|
||||||
import { capitalize } from "../lib/capitalize";
|
import { capitalize } from "../lib/capitalize";
|
||||||
import { showConfirm } from "../lib/confirm";
|
import { showConfirm } from "../lib/confirm";
|
||||||
@@ -14,156 +15,192 @@ import type { AuthenticatedModel } from "./useInheritedAuthentication";
|
|||||||
import { useInheritedAuthentication } from "./useInheritedAuthentication";
|
import { useInheritedAuthentication } from "./useInheritedAuthentication";
|
||||||
import { useModelAncestors } from "./useModelAncestors";
|
import { useModelAncestors } from "./useModelAncestors";
|
||||||
|
|
||||||
export function useAuthTab<T extends string>(tabValue: T, model: AuthenticatedModel | null) {
|
export function useAuthTab<T extends string>(
|
||||||
|
tabValue: T,
|
||||||
|
model: AuthenticatedModel | null,
|
||||||
|
) {
|
||||||
|
const options = useAuthDropdownOptions(model);
|
||||||
|
|
||||||
|
return useMemo<TabItem[]>(() => {
|
||||||
|
if (model == null || options == null) return [];
|
||||||
|
|
||||||
|
const tab: TabItem = {
|
||||||
|
value: tabValue,
|
||||||
|
label: "Auth",
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [tab];
|
||||||
|
}, [model, options, tabValue]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuthDropdownOptions(
|
||||||
|
model: AuthenticatedModel | null,
|
||||||
|
): Omit<RadioDropdownProps, "children"> | null {
|
||||||
const authentication = useHttpAuthenticationSummaries();
|
const authentication = useHttpAuthenticationSummaries();
|
||||||
const inheritedAuth = useInheritedAuthentication(model);
|
const inheritedAuth = useInheritedAuthentication(model);
|
||||||
const ancestors = useModelAncestors(model);
|
const ancestors = useModelAncestors(model);
|
||||||
const parentModel = ancestors[0] ?? null;
|
const parentModel = ancestors[0] ?? null;
|
||||||
|
|
||||||
return useMemo<TabItem[]>(() => {
|
return useMemo(() => {
|
||||||
if (model == null) return [];
|
if (model == null) return null;
|
||||||
|
|
||||||
const tab: TabItem = {
|
return {
|
||||||
value: tabValue,
|
value: model.authenticationType,
|
||||||
label: "Auth",
|
items: [
|
||||||
options: {
|
...authentication.map((a) => ({
|
||||||
value: model.authenticationType,
|
label: a.label || "UNKNOWN",
|
||||||
items: [
|
shortLabel: a.shortLabel,
|
||||||
...authentication.map((a) => ({
|
value: a.name,
|
||||||
label: a.label || "UNKNOWN",
|
})),
|
||||||
shortLabel: a.shortLabel,
|
{ type: "separator" },
|
||||||
value: a.name,
|
{
|
||||||
})),
|
label: "Inherit from Parent",
|
||||||
{ type: "separator" },
|
shortLabel:
|
||||||
{
|
inheritedAuth != null &&
|
||||||
label: "Inherit from Parent",
|
inheritedAuth.authenticationType !== "none" ? (
|
||||||
shortLabel:
|
<HStack space={1.5}>
|
||||||
inheritedAuth != null && inheritedAuth.authenticationType !== "none" ? (
|
{authentication.find(
|
||||||
<HStack space={1.5}>
|
(a) => a.name === inheritedAuth.authenticationType,
|
||||||
{authentication.find((a) => a.name === inheritedAuth.authenticationType)
|
)?.shortLabel ?? "UNKNOWN"}
|
||||||
?.shortLabel ?? "UNKNOWN"}
|
<IconTooltip
|
||||||
<IconTooltip
|
icon="zap_off"
|
||||||
icon="zap_off"
|
iconSize="xs"
|
||||||
iconSize="xs"
|
content="Authentication was inherited from an ancestor"
|
||||||
content="Authentication was inherited from an ancestor"
|
/>
|
||||||
/>
|
</HStack>
|
||||||
</HStack>
|
) : (
|
||||||
) : (
|
"Auth"
|
||||||
"Auth"
|
),
|
||||||
),
|
value: null,
|
||||||
value: null,
|
},
|
||||||
},
|
{ label: "No Auth", shortLabel: "No Auth", value: "none" },
|
||||||
{ label: "No Auth", shortLabel: "No Auth", value: "none" },
|
],
|
||||||
],
|
itemsAfter: (() => {
|
||||||
itemsAfter: (() => {
|
const actions: (
|
||||||
const actions: (
|
| { type: "separator"; label: string }
|
||||||
| { type: "separator"; label: string }
|
| {
|
||||||
| { label: string; leftSlot: React.ReactNode; onSelect: () => Promise<void> }
|
label: string;
|
||||||
)[] = [];
|
leftSlot: React.ReactNode;
|
||||||
|
onSelect: () => Promise<void>;
|
||||||
// Promote: move auth from current model up to parent
|
|
||||||
if (
|
|
||||||
parentModel &&
|
|
||||||
model.authenticationType &&
|
|
||||||
model.authenticationType !== "none" &&
|
|
||||||
(parentModel.authenticationType == null || parentModel.authenticationType === "none")
|
|
||||||
) {
|
|
||||||
actions.push(
|
|
||||||
{ type: "separator", label: "Actions" },
|
|
||||||
{
|
|
||||||
label: `Promote to ${capitalize(parentModel.model)}`,
|
|
||||||
leftSlot: (
|
|
||||||
<Icon
|
|
||||||
icon={parentModel.model === "workspace" ? "corner_right_up" : "folder_up"}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
onSelect: async () => {
|
|
||||||
const confirmed = await showConfirm({
|
|
||||||
id: "promote-auth-confirm",
|
|
||||||
title: "Promote Authentication",
|
|
||||||
confirmText: "Promote",
|
|
||||||
description: (
|
|
||||||
<>
|
|
||||||
Move authentication config to{" "}
|
|
||||||
<InlineCode>{resolvedModelName(parentModel)}</InlineCode>?
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
if (confirmed) {
|
|
||||||
await patchModel(model, { authentication: {}, authenticationType: null });
|
|
||||||
await patchModel(parentModel, {
|
|
||||||
authentication: model.authentication,
|
|
||||||
authenticationType: model.authenticationType,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parentModel.model === "folder") {
|
|
||||||
openFolderSettings(parentModel.id, "auth");
|
|
||||||
} else {
|
|
||||||
openWorkspaceSettings("auth");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy from ancestor: copy auth config down to current model
|
|
||||||
const ancestorWithAuth = ancestors.find(
|
|
||||||
(a) => a.authenticationType != null && a.authenticationType !== "none",
|
|
||||||
);
|
|
||||||
if (ancestorWithAuth) {
|
|
||||||
if (actions.length === 0) {
|
|
||||||
actions.push({ type: "separator", label: "Actions" });
|
|
||||||
}
|
}
|
||||||
actions.push({
|
)[] = [];
|
||||||
label: `Copy from ${modelTypeLabel(ancestorWithAuth)}`,
|
|
||||||
|
// Promote: move auth from current model up to parent
|
||||||
|
if (
|
||||||
|
parentModel &&
|
||||||
|
model.authenticationType &&
|
||||||
|
model.authenticationType !== "none" &&
|
||||||
|
(parentModel.authenticationType == null ||
|
||||||
|
parentModel.authenticationType === "none")
|
||||||
|
) {
|
||||||
|
actions.push(
|
||||||
|
{ type: "separator", label: "Actions" },
|
||||||
|
{
|
||||||
|
label: `Promote to ${capitalize(parentModel.model)}`,
|
||||||
leftSlot: (
|
leftSlot: (
|
||||||
<Icon
|
<Icon
|
||||||
icon={
|
icon={
|
||||||
ancestorWithAuth.model === "workspace" ? "corner_right_down" : "folder_down"
|
parentModel.model === "workspace"
|
||||||
|
? "corner_right_up"
|
||||||
|
: "folder_up"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
onSelect: async () => {
|
onSelect: async () => {
|
||||||
const confirmed = await showConfirm({
|
const confirmed = await showConfirm({
|
||||||
id: "copy-auth-confirm",
|
id: "promote-auth-confirm",
|
||||||
title: "Copy Authentication",
|
title: "Promote Authentication",
|
||||||
confirmText: "Copy",
|
confirmText: "Promote",
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Copy{" "}
|
Move authentication config to{" "}
|
||||||
{authentication.find((a) => a.name === ancestorWithAuth.authenticationType)
|
<InlineCode>{resolvedModelName(parentModel)}</InlineCode>?
|
||||||
?.label ?? "authentication"}{" "}
|
|
||||||
config from <InlineCode>{resolvedModelName(ancestorWithAuth)}</InlineCode>?
|
|
||||||
This will override the current authentication but will not affect the{" "}
|
|
||||||
{modelTypeLabel(ancestorWithAuth).toLowerCase()}.
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
await patchModel(model, {
|
await patchModel(model, {
|
||||||
authentication: { ...ancestorWithAuth.authentication },
|
authentication: {},
|
||||||
authenticationType: ancestorWithAuth.authenticationType,
|
authenticationType: null,
|
||||||
});
|
});
|
||||||
|
await patchModel(parentModel, {
|
||||||
|
authentication: model.authentication,
|
||||||
|
authenticationType: model.authenticationType,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parentModel.model === "folder") {
|
||||||
|
openFolderSettings(parentModel.id, "auth");
|
||||||
|
} else {
|
||||||
|
openWorkspaceSettings("auth");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return actions.length > 0 ? actions : undefined;
|
// Copy from ancestor: copy auth config down to current model
|
||||||
})(),
|
const ancestorWithAuth = ancestors.find(
|
||||||
onChange: async (authenticationType) => {
|
(a) =>
|
||||||
let authentication: Folder["authentication"] = model.authentication;
|
a.authenticationType != null && a.authenticationType !== "none",
|
||||||
if (model.authenticationType !== authenticationType) {
|
);
|
||||||
authentication = {
|
if (ancestorWithAuth) {
|
||||||
// Reset auth if changing types
|
if (actions.length === 0) {
|
||||||
};
|
actions.push({ type: "separator", label: "Actions" });
|
||||||
}
|
}
|
||||||
await patchModel(model, { authentication, authenticationType });
|
actions.push({
|
||||||
},
|
label: `Copy from ${modelTypeLabel(ancestorWithAuth)}`,
|
||||||
|
leftSlot: (
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
ancestorWithAuth.model === "workspace"
|
||||||
|
? "corner_right_down"
|
||||||
|
: "folder_down"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onSelect: async () => {
|
||||||
|
const confirmed = await showConfirm({
|
||||||
|
id: "copy-auth-confirm",
|
||||||
|
title: "Copy Authentication",
|
||||||
|
confirmText: "Copy",
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Copy{" "}
|
||||||
|
{authentication.find(
|
||||||
|
(a) => a.name === ancestorWithAuth.authenticationType,
|
||||||
|
)?.label ?? "authentication"}{" "}
|
||||||
|
config from{" "}
|
||||||
|
<InlineCode>
|
||||||
|
{resolvedModelName(ancestorWithAuth)}
|
||||||
|
</InlineCode>
|
||||||
|
? This will override the current authentication but will not
|
||||||
|
affect the {modelTypeLabel(ancestorWithAuth).toLowerCase()}.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
await patchModel(model, {
|
||||||
|
authentication: { ...ancestorWithAuth.authentication },
|
||||||
|
authenticationType: ancestorWithAuth.authenticationType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions.length > 0 ? actions : undefined;
|
||||||
|
})(),
|
||||||
|
onChange: async (authenticationType) => {
|
||||||
|
let authentication: Folder["authentication"] = model.authentication;
|
||||||
|
if (model.authenticationType !== authenticationType) {
|
||||||
|
authentication = {
|
||||||
|
// Reset auth if changing types
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await patchModel(model, { authentication, authenticationType });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}, [authentication, inheritedAuth, model, parentModel, ancestors]);
|
||||||
return [tab];
|
|
||||||
}, [authentication, inheritedAuth, model, parentModel, tabValue, ancestors]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function pricingUrl(intent: string): string {
|
||||||
|
return `https://yaak.app/pricing?intent=${encodeURIComponent(intent)}`;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ type ModelType = AnyModel["model"];
|
|||||||
type WorkspaceRequestSettings = Pick<
|
type WorkspaceRequestSettings = Pick<
|
||||||
Workspace,
|
Workspace,
|
||||||
| "settingFollowRedirects"
|
| "settingFollowRedirects"
|
||||||
|
| "settingRequestMessageSize"
|
||||||
| "settingRequestTimeout"
|
| "settingRequestTimeout"
|
||||||
| "settingSendCookies"
|
| "settingSendCookies"
|
||||||
| "settingStoreCookies"
|
| "settingStoreCookies"
|
||||||
@@ -17,7 +18,9 @@ type ModelTypeWithSetting<K extends RequestSettingKey> = {
|
|||||||
[M in ModelType]: K extends keyof ModelForType<M> ? M : never;
|
[M in ModelType]: K extends keyof ModelForType<M> ? M : never;
|
||||||
}[ModelType];
|
}[ModelType];
|
||||||
|
|
||||||
export type RequestSettingDefinition<K extends RequestSettingKey = RequestSettingKey> = {
|
export type RequestSettingDefinition<
|
||||||
|
K extends RequestSettingKey = RequestSettingKey,
|
||||||
|
> = {
|
||||||
defaultValue: WorkspaceRequestSettings[K];
|
defaultValue: WorkspaceRequestSettings[K];
|
||||||
description: string;
|
description: string;
|
||||||
modelKey: K;
|
modelKey: K;
|
||||||
@@ -41,11 +44,26 @@ export const SETTING_REQUEST_TIMEOUT = defineRequestSetting({
|
|||||||
title: "Request Timeout",
|
title: "Request Timeout",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SETTING_REQUEST_MESSAGE_SIZE = defineRequestSetting({
|
||||||
|
defaultValue: 64 * 1024 * 1024,
|
||||||
|
description:
|
||||||
|
"Maximum gRPC or WebSocket message size in MB. Set to 0 to disable.",
|
||||||
|
modelKey: "settingRequestMessageSize",
|
||||||
|
models: ["workspace", "folder", "websocket_request", "grpc_request"],
|
||||||
|
title: "Message Size Limit",
|
||||||
|
});
|
||||||
|
|
||||||
export const SETTING_VALIDATE_CERTIFICATES = defineRequestSetting({
|
export const SETTING_VALIDATE_CERTIFICATES = defineRequestSetting({
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
description: "When disabled, skip validation of server certificates.",
|
description: "When disabled, skip validation of server certificates.",
|
||||||
modelKey: "settingValidateCertificates",
|
modelKey: "settingValidateCertificates",
|
||||||
models: ["workspace", "folder", "http_request", "websocket_request", "grpc_request"],
|
models: [
|
||||||
|
"workspace",
|
||||||
|
"folder",
|
||||||
|
"http_request",
|
||||||
|
"websocket_request",
|
||||||
|
"grpc_request",
|
||||||
|
],
|
||||||
title: "Validate TLS certificates",
|
title: "Validate TLS certificates",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,7 +77,8 @@ export const SETTING_FOLLOW_REDIRECTS = defineRequestSetting({
|
|||||||
|
|
||||||
export const SETTING_SEND_COOKIES = defineRequestSetting({
|
export const SETTING_SEND_COOKIES = defineRequestSetting({
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
description: "Attach matching cookies from the active cookie jar to outgoing requests.",
|
description:
|
||||||
|
"Attach matching cookies from the active cookie jar to outgoing requests.",
|
||||||
modelKey: "settingSendCookies",
|
modelKey: "settingSendCookies",
|
||||||
models: ["workspace", "folder", "http_request", "websocket_request"],
|
models: ["workspace", "folder", "http_request", "websocket_request"],
|
||||||
title: "Automatically send cookies",
|
title: "Automatically send cookies",
|
||||||
@@ -67,7 +86,8 @@ export const SETTING_SEND_COOKIES = defineRequestSetting({
|
|||||||
|
|
||||||
export const SETTING_STORE_COOKIES = defineRequestSetting({
|
export const SETTING_STORE_COOKIES = defineRequestSetting({
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
description: "Save cookies from Set-Cookie response headers to the active cookie jar.",
|
description:
|
||||||
|
"Save cookies from Set-Cookie response headers to the active cookie jar.",
|
||||||
modelKey: "settingStoreCookies",
|
modelKey: "settingStoreCookies",
|
||||||
models: ["workspace", "folder", "http_request", "websocket_request"],
|
models: ["workspace", "folder", "http_request", "websocket_request"],
|
||||||
title: "Automatically store cookies",
|
title: "Automatically store cookies",
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-pdf": "^10.0.1",
|
"react-pdf": "^10.0.1",
|
||||||
"react-syntax-highlighter": "^16.1.0",
|
"react-syntax-highlighter": "^16.1.0",
|
||||||
"react-use": "^17.6.0",
|
"react-use": "^17.6.1",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
"remark-frontmatter": "^5.0.0",
|
"remark-frontmatter": "^5.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
@@ -102,11 +102,11 @@
|
|||||||
"postcss-nesting": "^13.0.2",
|
"postcss-nesting": "^13.0.2",
|
||||||
"rollup": "^4.60.3",
|
"rollup": "^4.60.3",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.20",
|
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||||
"vite-plugin-static-copy": "^3.3.0",
|
"vite-plugin-static-copy": "^3.3.0",
|
||||||
"vite-plugin-svgr": "^4.5.0",
|
"vite-plugin-svgr": "^4.5.0",
|
||||||
"vite-plugin-top-level-await": "^1.5.0",
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-plugin-wasm": "^3.5.0",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
"vite-plus": "^0.1.20"
|
"vite-plus": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export default defineConfig(async () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
|
target: "esnext",
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
outDir: "../../dist/apps/yaak-client",
|
outDir: "../../dist/apps/yaak-client",
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.20",
|
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||||
"vite-plus": "^0.1.20"
|
"vite-plus": "^0.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::models_ext::QueryManagerExt;
|
|||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use tauri::{AppHandle, Runtime};
|
use tauri::{AppHandle, Runtime, is_dev};
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
const NAMESPACE: &str = "analytics";
|
const NAMESPACE: &str = "analytics";
|
||||||
@@ -36,6 +36,10 @@ pub fn get_or_upsert_launch_info<R: Runtime>(app_handle: &AppHandle<R>) -> &Laun
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if is_dev() {
|
||||||
|
info.current_version = "0.0.1".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
app_handle
|
app_handle
|
||||||
.with_tx(|tx| {
|
.with_tx(|tx| {
|
||||||
// Load the previously tracked version
|
// Load the previously tracked version
|
||||||
|
|||||||
@@ -295,7 +295,8 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
|||||||
unrendered_request.folder_id.as_deref(),
|
unrendered_request.folder_id.as_deref(),
|
||||||
environment_id,
|
environment_id,
|
||||||
)?;
|
)?;
|
||||||
let resolved_settings = app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?;
|
let resolved_settings =
|
||||||
|
app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?;
|
||||||
|
|
||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
@@ -332,6 +333,7 @@ async fn cmd_grpc_reflect<R: Runtime>(
|
|||||||
&metadata,
|
&metadata,
|
||||||
resolved_settings.validate_certificates.value,
|
resolved_settings.validate_certificates.value,
|
||||||
client_certificate,
|
client_certificate,
|
||||||
|
resolved_settings.request_message_size.value,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| GenericError(e.to_string()))?)
|
.map_err(|e| GenericError(e.to_string()))?)
|
||||||
@@ -353,7 +355,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
unrendered_request.folder_id.as_deref(),
|
unrendered_request.folder_id.as_deref(),
|
||||||
environment_id,
|
environment_id,
|
||||||
)?;
|
)?;
|
||||||
let resolved_settings = app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?;
|
let resolved_settings =
|
||||||
|
app_handle.db().resolve_settings_for_grpc_request(&unrendered_request)?;
|
||||||
|
|
||||||
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*app_handle.state::<PluginManager>()).clone());
|
||||||
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
let encryption_manager = Arc::new((*app_handle.state::<EncryptionManager>()).clone());
|
||||||
@@ -425,6 +428,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
&metadata,
|
&metadata,
|
||||||
resolved_settings.validate_certificates.value,
|
resolved_settings.validate_certificates.value,
|
||||||
client_cert.clone(),
|
client_cert.clone(),
|
||||||
|
resolved_settings.request_message_size.value,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -714,7 +718,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
Some(s) => GrpcEvent {
|
Some(s) => GrpcEvent {
|
||||||
error: Some(s.message().to_string()),
|
error: Some(s.message().to_string()),
|
||||||
status: Some(s.code() as i32),
|
status: Some(s.code() as i32),
|
||||||
content: "Failed to connect".to_string(),
|
content: "Request failed".to_string(),
|
||||||
metadata: metadata_to_map(s.metadata().clone()),
|
metadata: metadata_to_map(s.metadata().clone()),
|
||||||
event_type: GrpcEventType::ConnectionEnd,
|
event_type: GrpcEventType::ConnectionEnd,
|
||||||
..base_event.clone()
|
..base_event.clone()
|
||||||
@@ -722,7 +726,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
None => GrpcEvent {
|
None => GrpcEvent {
|
||||||
error: Some(e.message),
|
error: Some(e.message),
|
||||||
status: Some(Code::Unknown as i32),
|
status: Some(Code::Unknown as i32),
|
||||||
content: "Failed to connect".to_string(),
|
content: "Request failed".to_string(),
|
||||||
event_type: GrpcEventType::ConnectionEnd,
|
event_type: GrpcEventType::ConnectionEnd,
|
||||||
..base_event.clone()
|
..base_event.clone()
|
||||||
},
|
},
|
||||||
@@ -738,7 +742,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
&GrpcEvent {
|
&GrpcEvent {
|
||||||
error: Some(e.to_string()),
|
error: Some(e.to_string()),
|
||||||
status: Some(Code::Unknown as i32),
|
status: Some(Code::Unknown as i32),
|
||||||
content: "Failed to connect".to_string(),
|
content: "Request failed".to_string(),
|
||||||
event_type: GrpcEventType::ConnectionEnd,
|
event_type: GrpcEventType::ConnectionEnd,
|
||||||
..base_event.clone()
|
..base_event.clone()
|
||||||
},
|
},
|
||||||
@@ -781,7 +785,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
Some(s) => GrpcEvent {
|
Some(s) => GrpcEvent {
|
||||||
error: Some(s.message().to_string()),
|
error: Some(s.message().to_string()),
|
||||||
status: Some(s.code() as i32),
|
status: Some(s.code() as i32),
|
||||||
content: "Failed to connect".to_string(),
|
content: "Stream failed".to_string(),
|
||||||
metadata: metadata_to_map(s.metadata().clone()),
|
metadata: metadata_to_map(s.metadata().clone()),
|
||||||
event_type: GrpcEventType::ConnectionEnd,
|
event_type: GrpcEventType::ConnectionEnd,
|
||||||
..base_event.clone()
|
..base_event.clone()
|
||||||
@@ -789,7 +793,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
None => GrpcEvent {
|
None => GrpcEvent {
|
||||||
error: Some(e.message),
|
error: Some(e.message),
|
||||||
status: Some(Code::Unknown as i32),
|
status: Some(Code::Unknown as i32),
|
||||||
content: "Failed to connect".to_string(),
|
content: "Stream failed".to_string(),
|
||||||
event_type: GrpcEventType::ConnectionEnd,
|
event_type: GrpcEventType::ConnectionEnd,
|
||||||
..base_event.clone()
|
..base_event.clone()
|
||||||
},
|
},
|
||||||
@@ -806,7 +810,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
&GrpcEvent {
|
&GrpcEvent {
|
||||||
error: Some(e.to_string()),
|
error: Some(e.to_string()),
|
||||||
status: Some(Code::Unknown as i32),
|
status: Some(Code::Unknown as i32),
|
||||||
content: "Failed to connect".to_string(),
|
content: "Stream failed".to_string(),
|
||||||
event_type: GrpcEventType::ConnectionEnd,
|
event_type: GrpcEventType::ConnectionEnd,
|
||||||
..base_event.clone()
|
..base_event.clone()
|
||||||
},
|
},
|
||||||
@@ -878,7 +882,8 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
.db()
|
.db()
|
||||||
.upsert_grpc_event(
|
.upsert_grpc_event(
|
||||||
&GrpcEvent {
|
&GrpcEvent {
|
||||||
content: status.to_string(),
|
content: "Stream failed".to_string(),
|
||||||
|
error: Some(status.message().to_string()),
|
||||||
status: Some(status.code() as i32),
|
status: Some(status.code() as i32),
|
||||||
metadata: metadata_to_map(status.metadata().clone()),
|
metadata: metadata_to_map(status.metadata().clone()),
|
||||||
event_type: GrpcEventType::ConnectionEnd,
|
event_type: GrpcEventType::ConnectionEnd,
|
||||||
@@ -887,6 +892,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
&UpdateSource::from_window_label(window.label()),
|
&UpdateSource::from_window_label(window.label()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ impl YaakNotifier {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Checking for notifications");
|
info!("Checking for notifications");
|
||||||
|
|
||||||
#[cfg(feature = "license")]
|
#[cfg(feature = "license")]
|
||||||
let license_check = {
|
let license_check = {
|
||||||
@@ -115,17 +115,20 @@ impl YaakNotifier {
|
|||||||
]);
|
]);
|
||||||
let resp = req.send().await?;
|
let resp = req.send().await?;
|
||||||
if resp.status() != 200 {
|
if resp.status() != 200 {
|
||||||
debug!("Skipping notification status code {}", resp.status());
|
info!("Skipping notification status code {}", resp.status());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
for notification in resp.json::<Vec<YaakNotification>>().await? {
|
let notifications = resp.json::<Vec<YaakNotification>>().await?;
|
||||||
|
debug!("Received {} notifications", notifications.len());
|
||||||
|
|
||||||
|
for notification in notifications {
|
||||||
let seen = get_kv(app_handle).await?;
|
let seen = get_kv(app_handle).await?;
|
||||||
if seen.contains(¬ification.id) {
|
if seen.contains(¬ification.id) {
|
||||||
debug!("Already seen notification {}", notification.id);
|
debug!("Already seen notification {}", notification.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
debug!("Got notification {:?}", notification);
|
info!("Got notification {:?}", notification);
|
||||||
|
|
||||||
let _ = app_handle.emit_to(window.label(), "notification", notification.clone());
|
let _ = app_handle.emit_to(window.label(), "notification", notification.clone());
|
||||||
break; // Only show one notification
|
break; // Only show one notification
|
||||||
|
|||||||
@@ -50,6 +50,37 @@ pub async fn cmd_ws_send<R: Runtime>(
|
|||||||
ws_manager: State<'_, Mutex<WebsocketManager>>,
|
ws_manager: State<'_, Mutex<WebsocketManager>>,
|
||||||
) -> Result<WebsocketConnection> {
|
) -> Result<WebsocketConnection> {
|
||||||
let connection = app_handle.db().get_websocket_connection(connection_id)?;
|
let connection = app_handle.db().get_websocket_connection(connection_id)?;
|
||||||
|
|
||||||
|
match send_websocket_message(&connection, environment_id, &app_handle, &window, &ws_manager)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(connection) => Ok(connection),
|
||||||
|
Err(e) => {
|
||||||
|
app_handle.db().upsert_websocket_event(
|
||||||
|
&WebsocketEvent {
|
||||||
|
connection_id: connection.id.clone(),
|
||||||
|
request_id: connection.request_id.clone(),
|
||||||
|
workspace_id: connection.workspace_id.clone(),
|
||||||
|
is_server: false,
|
||||||
|
message_type: WebsocketEventType::Error,
|
||||||
|
message: e.to_string().into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&UpdateSource::from_window_label(window.label()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_websocket_message<R: Runtime>(
|
||||||
|
connection: &WebsocketConnection,
|
||||||
|
environment_id: Option<&str>,
|
||||||
|
app_handle: &AppHandle<R>,
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
ws_manager: &Mutex<WebsocketManager>,
|
||||||
|
) -> Result<WebsocketConnection> {
|
||||||
let unrendered_request = app_handle.db().get_websocket_request(&connection.request_id)?;
|
let unrendered_request = app_handle.db().get_websocket_request(&connection.request_id)?;
|
||||||
let environment_chain = app_handle.db().resolve_environments(
|
let environment_chain = app_handle.db().resolve_environments(
|
||||||
&unrendered_request.workspace_id,
|
&unrendered_request.workspace_id,
|
||||||
@@ -91,7 +122,7 @@ pub async fn cmd_ws_send<R: Runtime>(
|
|||||||
&UpdateSource::from_window_label(window.label()),
|
&UpdateSource::from_window_label(window.label()),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(connection)
|
Ok(connection.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -299,6 +330,7 @@ pub async fn cmd_ws_connect<R: Runtime>(
|
|||||||
receive_tx,
|
receive_tx,
|
||||||
resolved_settings.validate_certificates.value,
|
resolved_settings.validate_certificates.value,
|
||||||
client_cert,
|
client_cert,
|
||||||
|
resolved_settings.request_message_size.value,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+4
@@ -46,6 +46,7 @@ export type Folder = {
|
|||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
settingFollowRedirects: InheritedBoolSetting;
|
settingFollowRedirects: InheritedBoolSetting;
|
||||||
settingRequestTimeout: InheritedIntSetting;
|
settingRequestTimeout: InheritedIntSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GrpcRequest = {
|
export type GrpcRequest = {
|
||||||
@@ -69,6 +70,7 @@ export type GrpcRequest = {
|
|||||||
*/
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HttpRequest = {
|
export type HttpRequest = {
|
||||||
@@ -146,6 +148,7 @@ export type WebsocketRequest = {
|
|||||||
settingSendCookies: InheritedBoolSetting;
|
settingSendCookies: InheritedBoolSetting;
|
||||||
settingStoreCookies: InheritedBoolSetting;
|
settingStoreCookies: InheritedBoolSetting;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Workspace = {
|
export type Workspace = {
|
||||||
@@ -162,6 +165,7 @@ export type Workspace = {
|
|||||||
settingValidateCertificates: boolean;
|
settingValidateCertificates: boolean;
|
||||||
settingFollowRedirects: boolean;
|
settingFollowRedirects: boolean;
|
||||||
settingRequestTimeout: number;
|
settingRequestTimeout: number;
|
||||||
|
settingRequestMessageSize: number;
|
||||||
settingDnsOverrides: Array<DnsOverride>;
|
settingDnsOverrides: Array<DnsOverride>;
|
||||||
settingSendCookies: boolean;
|
settingSendCookies: boolean;
|
||||||
settingStoreCookies: boolean;
|
settingStoreCookies: boolean;
|
||||||
|
|||||||
@@ -33,15 +33,21 @@ impl AutoReflectionClient {
|
|||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
validate_certificates: bool,
|
validate_certificates: bool,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
max_message_size: usize,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let client_v1 = v1::server_reflection_client::ServerReflectionClient::with_origin(
|
let client_v1 = v1::server_reflection_client::ServerReflectionClient::with_origin(
|
||||||
get_transport(validate_certificates, client_cert.clone())?,
|
get_transport(validate_certificates, client_cert.clone())?,
|
||||||
uri.clone(),
|
uri.clone(),
|
||||||
);
|
)
|
||||||
let client_v1alpha = v1alpha::server_reflection_client::ServerReflectionClient::with_origin(
|
.max_decoding_message_size(max_message_size)
|
||||||
get_transport(validate_certificates, client_cert.clone())?,
|
.max_encoding_message_size(max_message_size);
|
||||||
uri.clone(),
|
let client_v1alpha =
|
||||||
);
|
v1alpha::server_reflection_client::ServerReflectionClient::with_origin(
|
||||||
|
get_transport(validate_certificates, client_cert.clone())?,
|
||||||
|
uri.clone(),
|
||||||
|
)
|
||||||
|
.max_decoding_message_size(max_message_size)
|
||||||
|
.max_encoding_message_size(max_message_size);
|
||||||
Ok(AutoReflectionClient { use_v1alpha: false, client_v1, client_v1alpha })
|
Ok(AutoReflectionClient { use_v1alpha: false, client_v1, client_v1alpha })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub struct GrpcConnection {
|
|||||||
conn: Client<HttpsConnector<HttpConnector>, BoxBody>,
|
conn: Client<HttpsConnector<HttpConnector>, BoxBody>,
|
||||||
pub uri: Uri,
|
pub uri: Uri,
|
||||||
use_reflection: bool,
|
use_reflection: bool,
|
||||||
|
max_message_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
@@ -97,8 +98,15 @@ impl GrpcConnection {
|
|||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
) -> Result<Response<DynamicMessage>> {
|
) -> Result<Response<DynamicMessage>> {
|
||||||
if self.use_reflection {
|
if self.use_reflection {
|
||||||
reflect_types_for_message(self.pool.clone(), &self.uri, message, metadata, client_cert)
|
reflect_types_for_message(
|
||||||
.await?;
|
self.pool.clone(),
|
||||||
|
&self.uri,
|
||||||
|
message,
|
||||||
|
metadata,
|
||||||
|
client_cert,
|
||||||
|
self.max_message_size,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
let method = &self.method(&service, &method).await?;
|
let method = &self.method(&service, &method).await?;
|
||||||
let input_message = method.input();
|
let input_message = method.input();
|
||||||
@@ -107,7 +115,7 @@ impl GrpcConnection {
|
|||||||
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?;
|
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?;
|
||||||
deserializer.end()?;
|
deserializer.end()?;
|
||||||
|
|
||||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size);
|
||||||
|
|
||||||
let mut req = req_message.into_request();
|
let mut req = req_message.into_request();
|
||||||
decorate_req(metadata, &mut req)?;
|
decorate_req(metadata, &mut req)?;
|
||||||
@@ -132,6 +140,7 @@ impl GrpcConnection {
|
|||||||
message,
|
message,
|
||||||
metadata,
|
metadata,
|
||||||
client_cert,
|
client_cert,
|
||||||
|
self.max_message_size,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -171,6 +180,7 @@ impl GrpcConnection {
|
|||||||
let md = metadata.clone();
|
let md = metadata.clone();
|
||||||
let use_reflection = self.use_reflection.clone();
|
let use_reflection = self.use_reflection.clone();
|
||||||
let client_cert = client_cert.clone();
|
let client_cert = client_cert.clone();
|
||||||
|
let max_message_size = self.max_message_size;
|
||||||
stream
|
stream
|
||||||
.then(move |json| {
|
.then(move |json| {
|
||||||
let pool = pool.clone();
|
let pool = pool.clone();
|
||||||
@@ -183,8 +193,15 @@ impl GrpcConnection {
|
|||||||
let json_clone = json.clone();
|
let json_clone = json.clone();
|
||||||
async move {
|
async move {
|
||||||
if use_reflection {
|
if use_reflection {
|
||||||
if let Err(e) =
|
if let Err(e) = reflect_types_for_message(
|
||||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
pool,
|
||||||
|
&uri,
|
||||||
|
&json,
|
||||||
|
&md,
|
||||||
|
client_cert,
|
||||||
|
max_message_size,
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
warn!("Failed to resolve Any types: {e}");
|
warn!("Failed to resolve Any types: {e}");
|
||||||
}
|
}
|
||||||
@@ -206,7 +223,7 @@ impl GrpcConnection {
|
|||||||
.filter_map(|x| x)
|
.filter_map(|x| x)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size);
|
||||||
let path = method_desc_to_path(method);
|
let path = method_desc_to_path(method);
|
||||||
let codec = DynamicCodec::new(method.clone());
|
let codec = DynamicCodec::new(method.clone());
|
||||||
|
|
||||||
@@ -237,6 +254,7 @@ impl GrpcConnection {
|
|||||||
let md = metadata.clone();
|
let md = metadata.clone();
|
||||||
let use_reflection = self.use_reflection.clone();
|
let use_reflection = self.use_reflection.clone();
|
||||||
let client_cert = client_cert.clone();
|
let client_cert = client_cert.clone();
|
||||||
|
let max_message_size = self.max_message_size;
|
||||||
stream
|
stream
|
||||||
.then(move |json| {
|
.then(move |json| {
|
||||||
let pool = pool.clone();
|
let pool = pool.clone();
|
||||||
@@ -249,8 +267,15 @@ impl GrpcConnection {
|
|||||||
let json_clone = json.clone();
|
let json_clone = json.clone();
|
||||||
async move {
|
async move {
|
||||||
if use_reflection {
|
if use_reflection {
|
||||||
if let Err(e) =
|
if let Err(e) = reflect_types_for_message(
|
||||||
reflect_types_for_message(pool, &uri, &json, &md, client_cert).await
|
pool,
|
||||||
|
&uri,
|
||||||
|
&json,
|
||||||
|
&md,
|
||||||
|
client_cert,
|
||||||
|
max_message_size,
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
warn!("Failed to resolve Any types: {e}");
|
warn!("Failed to resolve Any types: {e}");
|
||||||
}
|
}
|
||||||
@@ -272,7 +297,7 @@ impl GrpcConnection {
|
|||||||
.filter_map(|x| x)
|
.filter_map(|x| x)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size);
|
||||||
let path = method_desc_to_path(method);
|
let path = method_desc_to_path(method);
|
||||||
let codec = DynamicCodec::new(method.clone());
|
let codec = DynamicCodec::new(method.clone());
|
||||||
|
|
||||||
@@ -300,7 +325,7 @@ impl GrpcConnection {
|
|||||||
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?;
|
let req_message = DynamicMessage::deserialize(input_message, &mut deserializer)?;
|
||||||
deserializer.end()?;
|
deserializer.end()?;
|
||||||
|
|
||||||
let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone());
|
let mut client = grpc_client(self.conn.clone(), self.uri.clone(), self.max_message_size);
|
||||||
|
|
||||||
let mut req = req_message.into_request();
|
let mut req = req_message.into_request();
|
||||||
decorate_req(metadata, &mut req)?;
|
decorate_req(metadata, &mut req)?;
|
||||||
@@ -312,6 +337,23 @@ impl GrpcConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grpc_client(
|
||||||
|
conn: Client<HttpsConnector<HttpConnector>, BoxBody>,
|
||||||
|
uri: Uri,
|
||||||
|
max_message_size: usize,
|
||||||
|
) -> tonic::client::Grpc<Client<HttpsConnector<HttpConnector>, BoxBody>> {
|
||||||
|
tonic::client::Grpc::with_origin(conn, uri)
|
||||||
|
.max_decoding_message_size(max_message_size)
|
||||||
|
.max_encoding_message_size(max_message_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message_size_limit(setting: i32) -> usize {
|
||||||
|
match setting.try_into() {
|
||||||
|
Ok(0) | Err(_) => usize::MAX,
|
||||||
|
Ok(limit) => limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Configuration for GrpcHandle to compile proto files
|
/// Configuration for GrpcHandle to compile proto files
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GrpcConfig {
|
pub struct GrpcConfig {
|
||||||
@@ -348,6 +390,7 @@ impl GrpcHandle {
|
|||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
validate_certificates: bool,
|
validate_certificates: bool,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
request_message_size: i32,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let server_reflection = proto_files.is_empty();
|
let server_reflection = proto_files.is_empty();
|
||||||
let key = make_pool_key(id, uri, proto_files);
|
let key = make_pool_key(id, uri, proto_files);
|
||||||
@@ -359,7 +402,14 @@ impl GrpcHandle {
|
|||||||
|
|
||||||
let pool = if server_reflection {
|
let pool = if server_reflection {
|
||||||
let full_uri = uri_from_str(uri)?;
|
let full_uri = uri_from_str(uri)?;
|
||||||
fill_pool_from_reflection(&full_uri, metadata, validate_certificates, client_cert).await
|
fill_pool_from_reflection(
|
||||||
|
&full_uri,
|
||||||
|
metadata,
|
||||||
|
validate_certificates,
|
||||||
|
client_cert,
|
||||||
|
message_size_limit(request_message_size),
|
||||||
|
)
|
||||||
|
.await
|
||||||
} else {
|
} else {
|
||||||
fill_pool_from_files(&self.config, proto_files).await
|
fill_pool_from_files(&self.config, proto_files).await
|
||||||
}?;
|
}?;
|
||||||
@@ -376,12 +426,21 @@ impl GrpcHandle {
|
|||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
validate_certificates: bool,
|
validate_certificates: bool,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
request_message_size: i32,
|
||||||
) -> Result<Vec<ServiceDefinition>> {
|
) -> Result<Vec<ServiceDefinition>> {
|
||||||
// Ensure we have a pool; reflect only if missing
|
// Ensure we have a pool; reflect only if missing
|
||||||
if self.get_pool(id, uri, proto_files).is_none() {
|
if self.get_pool(id, uri, proto_files).is_none() {
|
||||||
info!("Reflecting gRPC services for {} at {}", id, uri);
|
info!("Reflecting gRPC services for {} at {}", id, uri);
|
||||||
self.reflect(id, uri, proto_files, metadata, validate_certificates, client_cert)
|
self.reflect(
|
||||||
.await?;
|
id,
|
||||||
|
uri,
|
||||||
|
proto_files,
|
||||||
|
metadata,
|
||||||
|
validate_certificates,
|
||||||
|
client_cert,
|
||||||
|
request_message_size,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pool = self
|
let pool = self
|
||||||
@@ -421,8 +480,10 @@ impl GrpcHandle {
|
|||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
validate_certificates: bool,
|
validate_certificates: bool,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
request_message_size: i32,
|
||||||
) -> Result<GrpcConnection> {
|
) -> Result<GrpcConnection> {
|
||||||
let use_reflection = proto_files.is_empty();
|
let use_reflection = proto_files.is_empty();
|
||||||
|
let max_message_size = message_size_limit(request_message_size);
|
||||||
if self.get_pool(id, uri, proto_files).is_none() {
|
if self.get_pool(id, uri, proto_files).is_none() {
|
||||||
self.reflect(
|
self.reflect(
|
||||||
id,
|
id,
|
||||||
@@ -431,6 +492,7 @@ impl GrpcHandle {
|
|||||||
metadata,
|
metadata,
|
||||||
validate_certificates,
|
validate_certificates,
|
||||||
client_cert.clone(),
|
client_cert.clone(),
|
||||||
|
request_message_size,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@@ -440,7 +502,13 @@ impl GrpcHandle {
|
|||||||
.clone();
|
.clone();
|
||||||
let uri = uri_from_str(uri)?;
|
let uri = uri_from_str(uri)?;
|
||||||
let conn = get_transport(validate_certificates, client_cert.clone())?;
|
let conn = get_transport(validate_certificates, client_cert.clone())?;
|
||||||
Ok(GrpcConnection { pool: Arc::new(RwLock::new(pool)), use_reflection, conn, uri })
|
Ok(GrpcConnection {
|
||||||
|
pool: Arc::new(RwLock::new(pool)),
|
||||||
|
use_reflection,
|
||||||
|
conn,
|
||||||
|
uri,
|
||||||
|
max_message_size,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_pool(&self, id: &str, uri: &str, proto_files: &Vec<PathBuf>) -> Option<&DescriptorPool> {
|
fn get_pool(&self, id: &str, uri: &str, proto_files: &Vec<PathBuf>) -> Option<&DescriptorPool> {
|
||||||
|
|||||||
@@ -119,9 +119,11 @@ pub async fn fill_pool_from_reflection(
|
|||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
validate_certificates: bool,
|
validate_certificates: bool,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
max_message_size: usize,
|
||||||
) -> Result<DescriptorPool> {
|
) -> Result<DescriptorPool> {
|
||||||
let mut pool = DescriptorPool::new();
|
let mut pool = DescriptorPool::new();
|
||||||
let mut client = AutoReflectionClient::new(uri, validate_certificates, client_cert)?;
|
let mut client =
|
||||||
|
AutoReflectionClient::new(uri, validate_certificates, client_cert, max_message_size)?;
|
||||||
|
|
||||||
for service in list_services(&mut client, metadata).await? {
|
for service in list_services(&mut client, metadata).await? {
|
||||||
if service == "grpc.reflection.v1alpha.ServerReflection" {
|
if service == "grpc.reflection.v1alpha.ServerReflection" {
|
||||||
@@ -192,6 +194,7 @@ pub(crate) async fn reflect_types_for_message(
|
|||||||
json: &str,
|
json: &str,
|
||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
max_message_size: usize,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// 1. Collect all Any types in the JSON
|
// 1. Collect all Any types in the JSON
|
||||||
let mut extra_types = Vec::new();
|
let mut extra_types = Vec::new();
|
||||||
@@ -201,7 +204,7 @@ pub(crate) async fn reflect_types_for_message(
|
|||||||
return Ok(()); // nothing to do
|
return Ok(()); // nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut client = AutoReflectionClient::new(uri, false, client_cert)?;
|
let mut client = AutoReflectionClient::new(uri, false, client_cert, max_message_size)?;
|
||||||
for extra_type in extra_types {
|
for extra_type in extra_types {
|
||||||
{
|
{
|
||||||
let guard = pool.read().await;
|
let guard = pool.read().await;
|
||||||
@@ -239,6 +242,7 @@ pub(crate) async fn reflect_types_for_dynamic_message(
|
|||||||
message: &DynamicMessage,
|
message: &DynamicMessage,
|
||||||
metadata: &BTreeMap<String, String>,
|
metadata: &BTreeMap<String, String>,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
max_message_size: usize,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut extra_types = HashSet::new();
|
let mut extra_types = HashSet::new();
|
||||||
collect_any_types_from_dynamic_message(message, &mut extra_types);
|
collect_any_types_from_dynamic_message(message, &mut extra_types);
|
||||||
@@ -247,7 +251,7 @@ pub(crate) async fn reflect_types_for_dynamic_message(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut client = AutoReflectionClient::new(uri, false, client_cert)?;
|
let mut client = AutoReflectionClient::new(uri, false, client_cert, max_message_size)?;
|
||||||
for extra_type in extra_types {
|
for extra_type in extra_types {
|
||||||
{
|
{
|
||||||
let guard = pool.read().await;
|
let guard = pool.read().await;
|
||||||
|
|||||||
+6
-1
@@ -109,6 +109,7 @@ export type Folder = {
|
|||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
settingFollowRedirects: InheritedBoolSetting;
|
settingFollowRedirects: InheritedBoolSetting;
|
||||||
settingRequestTimeout: InheritedIntSetting;
|
settingRequestTimeout: InheritedIntSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GraphQlIntrospection = {
|
export type GraphQlIntrospection = {
|
||||||
@@ -184,6 +185,7 @@ export type GrpcRequest = {
|
|||||||
*/
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HttpRequest = {
|
export type HttpRequest = {
|
||||||
@@ -456,7 +458,8 @@ export type WebsocketEvent = {
|
|||||||
messageType: WebsocketEventType;
|
messageType: WebsocketEventType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
export type WebsocketEventType =
|
||||||
|
"binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text";
|
||||||
|
|
||||||
export type WebsocketMessageType = "text" | "binary";
|
export type WebsocketMessageType = "text" | "binary";
|
||||||
|
|
||||||
@@ -482,6 +485,7 @@ export type WebsocketRequest = {
|
|||||||
settingSendCookies: InheritedBoolSetting;
|
settingSendCookies: InheritedBoolSetting;
|
||||||
settingStoreCookies: InheritedBoolSetting;
|
settingStoreCookies: InheritedBoolSetting;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Workspace = {
|
export type Workspace = {
|
||||||
@@ -498,6 +502,7 @@ export type Workspace = {
|
|||||||
settingValidateCertificates: boolean;
|
settingValidateCertificates: boolean;
|
||||||
settingFollowRedirects: boolean;
|
settingFollowRedirects: boolean;
|
||||||
settingRequestTimeout: number;
|
settingRequestTimeout: number;
|
||||||
|
settingRequestMessageSize: number;
|
||||||
settingDnsOverrides: Array<DnsOverride>;
|
settingDnsOverrides: Array<DnsOverride>;
|
||||||
settingSendCookies: boolean;
|
settingSendCookies: boolean;
|
||||||
settingStoreCookies: boolean;
|
settingStoreCookies: boolean;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function trackModelWrite<T>(write: Promise<T>): Promise<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function flushAllModelWrites(): Promise<void> {
|
export async function flushAllModelWrites(): Promise<void> {
|
||||||
const results = await Promise.allSettled([...pendingModelWrites]);
|
const results = await Promise.allSettled(pendingModelWrites);
|
||||||
const rejected = results.find((result) => result.status === "rejected");
|
const rejected = results.find((result) => result.status === "rejected");
|
||||||
if (rejected?.status === "rejected") {
|
if (rejected?.status === "rejected") {
|
||||||
throw rejected.reason;
|
throw rejected.reason;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE workspaces ADD COLUMN setting_request_message_size INTEGER DEFAULT 67108864 NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE folders ADD COLUMN setting_request_message_size TEXT DEFAULT '{"enabled":false,"value":67108864}' NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE websocket_requests ADD COLUMN setting_request_message_size TEXT DEFAULT '{"enabled":false,"value":67108864}' NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE grpc_requests ADD COLUMN setting_request_message_size TEXT DEFAULT '{"enabled":false,"value":67108864}' NOT NULL;
|
||||||
@@ -21,6 +21,8 @@ use ts_rs::TS;
|
|||||||
use yaak_database::{Result as DbResult, UpdateSource};
|
use yaak_database::{Result as DbResult, UpdateSource};
|
||||||
pub use yaak_database::{UpsertModelInfo, upsert_date};
|
pub use yaak_database::{UpsertModelInfo, upsert_date};
|
||||||
|
|
||||||
|
pub const DEFAULT_REQUEST_MESSAGE_SIZE: i32 = 64 * 1024 * 1024;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! impl_model {
|
macro_rules! impl_model {
|
||||||
($t:ty, $variant:ident) => {
|
($t:ty, $variant:ident) => {
|
||||||
@@ -120,6 +122,7 @@ pub struct ResolvedHttpRequestSettings {
|
|||||||
pub validate_certificates: ResolvedSetting<bool>,
|
pub validate_certificates: ResolvedSetting<bool>,
|
||||||
pub follow_redirects: ResolvedSetting<bool>,
|
pub follow_redirects: ResolvedSetting<bool>,
|
||||||
pub request_timeout: ResolvedSetting<i32>,
|
pub request_timeout: ResolvedSetting<i32>,
|
||||||
|
pub request_message_size: ResolvedSetting<i32>,
|
||||||
pub send_cookies: ResolvedSetting<bool>,
|
pub send_cookies: ResolvedSetting<bool>,
|
||||||
pub store_cookies: ResolvedSetting<bool>,
|
pub store_cookies: ResolvedSetting<bool>,
|
||||||
}
|
}
|
||||||
@@ -130,6 +133,7 @@ impl Default for ResolvedHttpRequestSettings {
|
|||||||
validate_certificates: ResolvedSetting::default_source(true),
|
validate_certificates: ResolvedSetting::default_source(true),
|
||||||
follow_redirects: ResolvedSetting::default_source(true),
|
follow_redirects: ResolvedSetting::default_source(true),
|
||||||
request_timeout: ResolvedSetting::default_source(0),
|
request_timeout: ResolvedSetting::default_source(0),
|
||||||
|
request_message_size: ResolvedSetting::default_source(DEFAULT_REQUEST_MESSAGE_SIZE),
|
||||||
send_cookies: ResolvedSetting::default_source(true),
|
send_cookies: ResolvedSetting::default_source(true),
|
||||||
store_cookies: ResolvedSetting::default_source(true),
|
store_cookies: ResolvedSetting::default_source(true),
|
||||||
}
|
}
|
||||||
@@ -400,6 +404,8 @@ pub struct Workspace {
|
|||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub setting_follow_redirects: bool,
|
pub setting_follow_redirects: bool,
|
||||||
pub setting_request_timeout: i32,
|
pub setting_request_timeout: i32,
|
||||||
|
#[serde(default = "default_request_message_size")]
|
||||||
|
pub setting_request_message_size: i32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub setting_dns_overrides: Vec<DnsOverride>,
|
pub setting_dns_overrides: Vec<DnsOverride>,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
@@ -445,6 +451,7 @@ impl UpsertModelInfo for Workspace {
|
|||||||
(EncryptionKeyChallenge, self.encryption_key_challenge.into()),
|
(EncryptionKeyChallenge, self.encryption_key_challenge.into()),
|
||||||
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
(SettingFollowRedirects, self.setting_follow_redirects.into()),
|
||||||
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
(SettingRequestTimeout, self.setting_request_timeout.into()),
|
||||||
|
(SettingRequestMessageSize, self.setting_request_message_size.into()),
|
||||||
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
(SettingValidateCertificates, self.setting_validate_certificates.into()),
|
||||||
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
|
(SettingDnsOverrides, serde_json::to_string(&self.setting_dns_overrides)?.into()),
|
||||||
(SettingSendCookies, self.setting_send_cookies.into()),
|
(SettingSendCookies, self.setting_send_cookies.into()),
|
||||||
@@ -463,7 +470,7 @@ impl UpsertModelInfo for Workspace {
|
|||||||
WorkspaceIden::EncryptionKeyChallenge,
|
WorkspaceIden::EncryptionKeyChallenge,
|
||||||
WorkspaceIden::SettingRequestTimeout,
|
WorkspaceIden::SettingRequestTimeout,
|
||||||
WorkspaceIden::SettingFollowRedirects,
|
WorkspaceIden::SettingFollowRedirects,
|
||||||
WorkspaceIden::SettingRequestTimeout,
|
WorkspaceIden::SettingRequestMessageSize,
|
||||||
WorkspaceIden::SettingValidateCertificates,
|
WorkspaceIden::SettingValidateCertificates,
|
||||||
WorkspaceIden::SettingDnsOverrides,
|
WorkspaceIden::SettingDnsOverrides,
|
||||||
WorkspaceIden::SettingSendCookies,
|
WorkspaceIden::SettingSendCookies,
|
||||||
@@ -491,6 +498,7 @@ impl UpsertModelInfo for Workspace {
|
|||||||
authentication_type: row.get("authentication_type")?,
|
authentication_type: row.get("authentication_type")?,
|
||||||
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
setting_follow_redirects: row.get("setting_follow_redirects")?,
|
||||||
setting_request_timeout: row.get("setting_request_timeout")?,
|
setting_request_timeout: row.get("setting_request_timeout")?,
|
||||||
|
setting_request_message_size: row.get("setting_request_message_size")?,
|
||||||
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
setting_validate_certificates: row.get("setting_validate_certificates")?,
|
||||||
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
|
setting_dns_overrides: serde_json::from_str(&setting_dns_overrides).unwrap_or_default(),
|
||||||
setting_send_cookies: row.get("setting_send_cookies")?,
|
setting_send_cookies: row.get("setting_send_cookies")?,
|
||||||
@@ -962,6 +970,8 @@ pub struct Folder {
|
|||||||
pub setting_validate_certificates: InheritedBoolSetting,
|
pub setting_validate_certificates: InheritedBoolSetting,
|
||||||
pub setting_follow_redirects: InheritedBoolSetting,
|
pub setting_follow_redirects: InheritedBoolSetting,
|
||||||
pub setting_request_timeout: InheritedIntSetting,
|
pub setting_request_timeout: InheritedIntSetting,
|
||||||
|
#[serde(default = "default_request_message_size_setting")]
|
||||||
|
pub setting_request_message_size: InheritedIntSetting,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Folder {
|
impl UpsertModelInfo for Folder {
|
||||||
@@ -1009,6 +1019,10 @@ impl UpsertModelInfo for Folder {
|
|||||||
),
|
),
|
||||||
(SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()),
|
(SettingFollowRedirects, serde_json::to_string(&self.setting_follow_redirects)?.into()),
|
||||||
(SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.into()),
|
(SettingRequestTimeout, serde_json::to_string(&self.setting_request_timeout)?.into()),
|
||||||
|
(
|
||||||
|
SettingRequestMessageSize,
|
||||||
|
serde_json::to_string(&self.setting_request_message_size)?.into(),
|
||||||
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1027,6 +1041,7 @@ impl UpsertModelInfo for Folder {
|
|||||||
FolderIden::SettingValidateCertificates,
|
FolderIden::SettingValidateCertificates,
|
||||||
FolderIden::SettingFollowRedirects,
|
FolderIden::SettingFollowRedirects,
|
||||||
FolderIden::SettingRequestTimeout,
|
FolderIden::SettingRequestTimeout,
|
||||||
|
FolderIden::SettingRequestMessageSize,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,6 +1056,7 @@ impl UpsertModelInfo for Folder {
|
|||||||
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
|
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
|
||||||
let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
|
let setting_follow_redirects: String = row.get("setting_follow_redirects")?;
|
||||||
let setting_request_timeout: String = row.get("setting_request_timeout")?;
|
let setting_request_timeout: String = row.get("setting_request_timeout")?;
|
||||||
|
let setting_request_message_size: String = row.get("setting_request_message_size")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -1062,6 +1078,8 @@ impl UpsertModelInfo for Folder {
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
setting_request_timeout: serde_json::from_str(&setting_request_timeout)
|
setting_request_timeout: serde_json::from_str(&setting_request_timeout)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
setting_request_message_size: serde_json::from_str(&setting_request_message_size)
|
||||||
|
.unwrap_or_else(|_| default_request_message_size_setting()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1398,6 +1416,8 @@ pub struct WebsocketRequest {
|
|||||||
pub setting_send_cookies: InheritedBoolSetting,
|
pub setting_send_cookies: InheritedBoolSetting,
|
||||||
pub setting_store_cookies: InheritedBoolSetting,
|
pub setting_store_cookies: InheritedBoolSetting,
|
||||||
pub setting_validate_certificates: InheritedBoolSetting,
|
pub setting_validate_certificates: InheritedBoolSetting,
|
||||||
|
#[serde(default = "default_request_message_size_setting")]
|
||||||
|
pub setting_request_message_size: InheritedIntSetting,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for WebsocketRequest {
|
impl UpsertModelInfo for WebsocketRequest {
|
||||||
@@ -1446,6 +1466,10 @@ impl UpsertModelInfo for WebsocketRequest {
|
|||||||
SettingValidateCertificates,
|
SettingValidateCertificates,
|
||||||
serde_json::to_string(&self.setting_validate_certificates)?.into(),
|
serde_json::to_string(&self.setting_validate_certificates)?.into(),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
SettingRequestMessageSize,
|
||||||
|
serde_json::to_string(&self.setting_request_message_size)?.into(),
|
||||||
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1466,6 +1490,7 @@ impl UpsertModelInfo for WebsocketRequest {
|
|||||||
WebsocketRequestIden::SettingSendCookies,
|
WebsocketRequestIden::SettingSendCookies,
|
||||||
WebsocketRequestIden::SettingStoreCookies,
|
WebsocketRequestIden::SettingStoreCookies,
|
||||||
WebsocketRequestIden::SettingValidateCertificates,
|
WebsocketRequestIden::SettingValidateCertificates,
|
||||||
|
WebsocketRequestIden::SettingRequestMessageSize,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1479,6 +1504,7 @@ impl UpsertModelInfo for WebsocketRequest {
|
|||||||
let setting_send_cookies: String = row.get("setting_send_cookies")?;
|
let setting_send_cookies: String = row.get("setting_send_cookies")?;
|
||||||
let setting_store_cookies: String = row.get("setting_store_cookies")?;
|
let setting_store_cookies: String = row.get("setting_store_cookies")?;
|
||||||
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
|
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
|
||||||
|
let setting_request_message_size: String = row.get("setting_request_message_size")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -1499,6 +1525,8 @@ impl UpsertModelInfo for WebsocketRequest {
|
|||||||
setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
|
setting_store_cookies: serde_json::from_str(&setting_store_cookies).unwrap_or_default(),
|
||||||
setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
|
setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
setting_request_message_size: serde_json::from_str(&setting_request_message_size)
|
||||||
|
.unwrap_or_else(|_| default_request_message_size_setting()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1509,6 +1537,7 @@ impl UpsertModelInfo for WebsocketRequest {
|
|||||||
pub enum WebsocketEventType {
|
pub enum WebsocketEventType {
|
||||||
Binary,
|
Binary,
|
||||||
Close,
|
Close,
|
||||||
|
Error,
|
||||||
Frame,
|
Frame,
|
||||||
Open,
|
Open,
|
||||||
Ping,
|
Ping,
|
||||||
@@ -2039,6 +2068,8 @@ pub struct GrpcRequest {
|
|||||||
/// Server URL (http for plaintext or https for secure)
|
/// Server URL (http for plaintext or https for secure)
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub setting_validate_certificates: InheritedBoolSetting,
|
pub setting_validate_certificates: InheritedBoolSetting,
|
||||||
|
#[serde(default = "default_request_message_size_setting")]
|
||||||
|
pub setting_request_message_size: InheritedIntSetting,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for GrpcRequest {
|
impl UpsertModelInfo for GrpcRequest {
|
||||||
@@ -2086,6 +2117,10 @@ impl UpsertModelInfo for GrpcRequest {
|
|||||||
SettingValidateCertificates,
|
SettingValidateCertificates,
|
||||||
serde_json::to_string(&self.setting_validate_certificates)?.into(),
|
serde_json::to_string(&self.setting_validate_certificates)?.into(),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
SettingRequestMessageSize,
|
||||||
|
serde_json::to_string(&self.setting_request_message_size)?.into(),
|
||||||
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2105,6 +2140,7 @@ impl UpsertModelInfo for GrpcRequest {
|
|||||||
GrpcRequestIden::Authentication,
|
GrpcRequestIden::Authentication,
|
||||||
GrpcRequestIden::Metadata,
|
GrpcRequestIden::Metadata,
|
||||||
GrpcRequestIden::SettingValidateCertificates,
|
GrpcRequestIden::SettingValidateCertificates,
|
||||||
|
GrpcRequestIden::SettingRequestMessageSize,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2115,6 +2151,7 @@ impl UpsertModelInfo for GrpcRequest {
|
|||||||
let authentication: String = row.get("authentication")?;
|
let authentication: String = row.get("authentication")?;
|
||||||
let metadata: String = row.get("metadata")?;
|
let metadata: String = row.get("metadata")?;
|
||||||
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
|
let setting_validate_certificates: String = row.get("setting_validate_certificates")?;
|
||||||
|
let setting_request_message_size: String = row.get("setting_request_message_size")?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
model: row.get("model")?,
|
model: row.get("model")?,
|
||||||
@@ -2134,6 +2171,8 @@ impl UpsertModelInfo for GrpcRequest {
|
|||||||
metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(),
|
metadata: serde_json::from_str(metadata.as_str()).unwrap_or_default(),
|
||||||
setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
|
setting_validate_certificates: serde_json::from_str(&setting_validate_certificates)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
setting_request_message_size: serde_json::from_str(&setting_request_message_size)
|
||||||
|
.unwrap_or_else(|_| default_request_message_size_setting()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2684,6 +2723,14 @@ fn default_true() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_request_message_size() -> i32 {
|
||||||
|
DEFAULT_REQUEST_MESSAGE_SIZE
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_request_message_size_setting() -> InheritedIntSetting {
|
||||||
|
InheritedIntSetting { enabled: false, value: DEFAULT_REQUEST_MESSAGE_SIZE }
|
||||||
|
}
|
||||||
|
|
||||||
fn default_http_method() -> String {
|
fn default_http_method() -> String {
|
||||||
"GET".to_string()
|
"GET".to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,14 @@ impl<'a> ClientDb<'a> {
|
|||||||
} else {
|
} else {
|
||||||
parent.request_timeout
|
parent.request_timeout
|
||||||
},
|
},
|
||||||
|
request_message_size: if folder.setting_request_message_size.enabled {
|
||||||
|
ResolvedSetting::from_model(
|
||||||
|
folder.setting_request_message_size.value,
|
||||||
|
AnyModel::Folder(folder.clone()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
parent.request_message_size
|
||||||
|
},
|
||||||
send_cookies: if folder.setting_send_cookies.enabled {
|
send_cookies: if folder.setting_send_cookies.enabled {
|
||||||
ResolvedSetting::from_model(
|
ResolvedSetting::from_model(
|
||||||
folder.setting_send_cookies.value,
|
folder.setting_send_cookies.value,
|
||||||
|
|||||||
@@ -129,6 +129,14 @@ impl<'a> ClientDb<'a> {
|
|||||||
} else {
|
} else {
|
||||||
parent.validate_certificates
|
parent.validate_certificates
|
||||||
},
|
},
|
||||||
|
request_message_size: if grpc_request.setting_request_message_size.enabled {
|
||||||
|
ResolvedSetting::from_model(
|
||||||
|
grpc_request.setting_request_message_size.value,
|
||||||
|
AnyModel::GrpcRequest(grpc_request.clone()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
parent.request_message_size
|
||||||
|
},
|
||||||
..parent
|
..parent
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ impl<'a> ClientDb<'a> {
|
|||||||
} else {
|
} else {
|
||||||
parent.request_timeout
|
parent.request_timeout
|
||||||
},
|
},
|
||||||
|
request_message_size: parent.request_message_size,
|
||||||
send_cookies: if http_request.setting_send_cookies.enabled {
|
send_cookies: if http_request.setting_send_cookies.enabled {
|
||||||
ResolvedSetting::from_model(
|
ResolvedSetting::from_model(
|
||||||
http_request.setting_send_cookies.value,
|
http_request.setting_send_cookies.value,
|
||||||
|
|||||||
@@ -139,6 +139,14 @@ impl<'a> ClientDb<'a> {
|
|||||||
} else {
|
} else {
|
||||||
parent.validate_certificates
|
parent.validate_certificates
|
||||||
},
|
},
|
||||||
|
request_message_size: if websocket_request.setting_request_message_size.enabled {
|
||||||
|
ResolvedSetting::from_model(
|
||||||
|
websocket_request.setting_request_message_size.value,
|
||||||
|
AnyModel::WebsocketRequest(websocket_request.clone()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
parent.request_message_size
|
||||||
|
},
|
||||||
send_cookies: if websocket_request.setting_send_cookies.enabled {
|
send_cookies: if websocket_request.setting_send_cookies.enabled {
|
||||||
ResolvedSetting::from_model(
|
ResolvedSetting::from_model(
|
||||||
websocket_request.setting_send_cookies.value,
|
websocket_request.setting_send_cookies.value,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ impl<'a> ClientDb<'a> {
|
|||||||
&Workspace {
|
&Workspace {
|
||||||
name: "Yaak".to_string(),
|
name: "Yaak".to_string(),
|
||||||
setting_follow_redirects: true,
|
setting_follow_redirects: true,
|
||||||
|
setting_request_message_size: crate::models::DEFAULT_REQUEST_MESSAGE_SIZE,
|
||||||
setting_validate_certificates: true,
|
setting_validate_certificates: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
@@ -102,6 +103,10 @@ impl<'a> ClientDb<'a> {
|
|||||||
workspace.setting_request_timeout,
|
workspace.setting_request_timeout,
|
||||||
AnyModel::Workspace(workspace.clone()),
|
AnyModel::Workspace(workspace.clone()),
|
||||||
),
|
),
|
||||||
|
request_message_size: ResolvedSetting::from_model(
|
||||||
|
workspace.setting_request_message_size,
|
||||||
|
AnyModel::Workspace(workspace.clone()),
|
||||||
|
),
|
||||||
send_cookies: ResolvedSetting::from_model(
|
send_cookies: ResolvedSetting::from_model(
|
||||||
workspace.setting_send_cookies,
|
workspace.setting_send_cookies,
|
||||||
AnyModel::Workspace(workspace.clone()),
|
AnyModel::Workspace(workspace.clone()),
|
||||||
|
|||||||
+6
-1
@@ -108,6 +108,7 @@ export type Folder = {
|
|||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
settingFollowRedirects: InheritedBoolSetting;
|
settingFollowRedirects: InheritedBoolSetting;
|
||||||
settingRequestTimeout: InheritedIntSetting;
|
settingRequestTimeout: InheritedIntSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GraphQlIntrospection = {
|
export type GraphQlIntrospection = {
|
||||||
@@ -183,6 +184,7 @@ export type GrpcRequest = {
|
|||||||
*/
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HttpRequest = {
|
export type HttpRequest = {
|
||||||
@@ -426,7 +428,8 @@ export type WebsocketEvent = {
|
|||||||
messageType: WebsocketEventType;
|
messageType: WebsocketEventType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
export type WebsocketEventType =
|
||||||
|
"binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text";
|
||||||
|
|
||||||
export type WebsocketRequest = {
|
export type WebsocketRequest = {
|
||||||
model: "websocket_request";
|
model: "websocket_request";
|
||||||
@@ -450,6 +453,7 @@ export type WebsocketRequest = {
|
|||||||
settingSendCookies: InheritedBoolSetting;
|
settingSendCookies: InheritedBoolSetting;
|
||||||
settingStoreCookies: InheritedBoolSetting;
|
settingStoreCookies: InheritedBoolSetting;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Workspace = {
|
export type Workspace = {
|
||||||
@@ -466,6 +470,7 @@ export type Workspace = {
|
|||||||
settingValidateCertificates: boolean;
|
settingValidateCertificates: boolean;
|
||||||
settingFollowRedirects: boolean;
|
settingFollowRedirects: boolean;
|
||||||
settingRequestTimeout: number;
|
settingRequestTimeout: number;
|
||||||
|
settingRequestMessageSize: number;
|
||||||
settingDnsOverrides: Array<DnsOverride>;
|
settingDnsOverrides: Array<DnsOverride>;
|
||||||
settingSendCookies: boolean;
|
settingSendCookies: boolean;
|
||||||
settingStoreCookies: boolean;
|
settingStoreCookies: boolean;
|
||||||
|
|||||||
Generated
+4
@@ -46,6 +46,7 @@ export type Folder = {
|
|||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
settingFollowRedirects: InheritedBoolSetting;
|
settingFollowRedirects: InheritedBoolSetting;
|
||||||
settingRequestTimeout: InheritedIntSetting;
|
settingRequestTimeout: InheritedIntSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GrpcRequest = {
|
export type GrpcRequest = {
|
||||||
@@ -69,6 +70,7 @@ export type GrpcRequest = {
|
|||||||
*/
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HttpRequest = {
|
export type HttpRequest = {
|
||||||
@@ -159,6 +161,7 @@ export type WebsocketRequest = {
|
|||||||
settingSendCookies: InheritedBoolSetting;
|
settingSendCookies: InheritedBoolSetting;
|
||||||
settingStoreCookies: InheritedBoolSetting;
|
settingStoreCookies: InheritedBoolSetting;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Workspace = {
|
export type Workspace = {
|
||||||
@@ -175,6 +178,7 @@ export type Workspace = {
|
|||||||
settingValidateCertificates: boolean;
|
settingValidateCertificates: boolean;
|
||||||
settingFollowRedirects: boolean;
|
settingFollowRedirects: boolean;
|
||||||
settingRequestTimeout: number;
|
settingRequestTimeout: number;
|
||||||
|
settingRequestMessageSize: number;
|
||||||
settingDnsOverrides: Array<DnsOverride>;
|
settingDnsOverrides: Array<DnsOverride>;
|
||||||
settingSendCookies: boolean;
|
settingSendCookies: boolean;
|
||||||
settingStoreCookies: boolean;
|
settingStoreCookies: boolean;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub async fn ws_connect(
|
|||||||
headers: HeaderMap<HeaderValue>,
|
headers: HeaderMap<HeaderValue>,
|
||||||
validate_certificates: bool,
|
validate_certificates: bool,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
request_message_size: i32,
|
||||||
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response)> {
|
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response)> {
|
||||||
info!("Connecting to WS {url}");
|
info!("Connecting to WS {url}");
|
||||||
let tls_config = get_tls_config(validate_certificates, WITH_ALPN, client_cert.clone())?;
|
let tls_config = get_tls_config(validate_certificates, WITH_ALPN, client_cert.clone())?;
|
||||||
@@ -34,7 +35,7 @@ pub async fn ws_connect(
|
|||||||
|
|
||||||
let (stream, response) = connect_async_tls_with_config(
|
let (stream, response) = connect_async_tls_with_config(
|
||||||
req,
|
req,
|
||||||
Some(WebSocketConfig::default()),
|
Some(websocket_config(request_message_size)),
|
||||||
false,
|
false,
|
||||||
Some(Connector::Rustls(Arc::new(tls_config))),
|
Some(Connector::Rustls(Arc::new(tls_config))),
|
||||||
)
|
)
|
||||||
@@ -48,3 +49,12 @@ pub async fn ws_connect(
|
|||||||
|
|
||||||
Ok((stream, response))
|
Ok((stream, response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn websocket_config(request_message_size: i32) -> WebSocketConfig {
|
||||||
|
let max_message_size = message_size_limit(request_message_size);
|
||||||
|
WebSocketConfig::default().max_message_size(max_message_size).max_frame_size(max_message_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn message_size_limit(setting: i32) -> Option<usize> {
|
||||||
|
setting.try_into().ok().filter(|limit| *limit > 0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use tokio_tungstenite::tungstenite;
|
|||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("WebSocket error: {0}")]
|
#[error("{0}")]
|
||||||
WebSocketErr(#[from] tungstenite::Error),
|
WebSocketErr(#[from] tungstenite::Error),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
@@ -16,7 +16,7 @@ pub enum Error {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
TlsError(#[from] yaak_tls::error::Error),
|
TlsError(#[from] yaak_tls::error::Error),
|
||||||
|
|
||||||
#[error("WebSocket error: {0}")]
|
#[error("{0}")]
|
||||||
GenericError(String),
|
GenericError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::connect::ws_connect;
|
use crate::connect::{message_size_limit, ws_connect};
|
||||||
|
use crate::error::Error::GenericError;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use futures_util::stream::SplitSink;
|
use futures_util::stream::SplitSink;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
@@ -15,10 +16,16 @@ use tokio_tungstenite::tungstenite::http::HeaderValue;
|
|||||||
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
|
||||||
use yaak_tls::ClientCertificateConfig;
|
use yaak_tls::ClientCertificateConfig;
|
||||||
|
|
||||||
|
type WebsocketSink = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
|
||||||
|
|
||||||
|
struct WebsocketConnection {
|
||||||
|
max_message_size: Option<usize>,
|
||||||
|
sink: WebsocketSink,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WebsocketManager {
|
pub struct WebsocketManager {
|
||||||
connections:
|
connections: Arc<Mutex<HashMap<String, WebsocketConnection>>>,
|
||||||
Arc<Mutex<HashMap<String, SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>>>,
|
|
||||||
read_tasks: Arc<Mutex<HashMap<String, tokio::task::JoinHandle<()>>>>,
|
read_tasks: Arc<Mutex<HashMap<String, tokio::task::JoinHandle<()>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,14 +42,20 @@ impl WebsocketManager {
|
|||||||
receive_tx: mpsc::Sender<Message>,
|
receive_tx: mpsc::Sender<Message>,
|
||||||
validate_certificates: bool,
|
validate_certificates: bool,
|
||||||
client_cert: Option<ClientCertificateConfig>,
|
client_cert: Option<ClientCertificateConfig>,
|
||||||
|
request_message_size: i32,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
let tx = receive_tx.clone();
|
let tx = receive_tx.clone();
|
||||||
|
let max_message_size = message_size_limit(request_message_size);
|
||||||
|
|
||||||
let (stream, response) =
|
let (stream, response) =
|
||||||
ws_connect(url, headers, validate_certificates, client_cert).await?;
|
ws_connect(url, headers, validate_certificates, client_cert, request_message_size)
|
||||||
|
.await?;
|
||||||
let (write, mut read) = stream.split();
|
let (write, mut read) = stream.split();
|
||||||
|
|
||||||
self.connections.lock().await.insert(id.to_string(), write);
|
self.connections
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.insert(id.to_string(), WebsocketConnection { max_message_size, sink: write });
|
||||||
|
|
||||||
let handle = {
|
let handle = {
|
||||||
let connection_id = id.to_string();
|
let connection_id = id.to_string();
|
||||||
@@ -70,13 +83,20 @@ impl WebsocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send(&mut self, id: &str, msg: Message) -> Result<()> {
|
pub async fn send(&mut self, id: &str, msg: Message) -> Result<()> {
|
||||||
debug!("Send websocket message {msg:?}");
|
|
||||||
let mut connections = self.connections.lock().await;
|
let mut connections = self.connections.lock().await;
|
||||||
let connection = match connections.get_mut(id) {
|
let connection = match connections.get_mut(id) {
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
};
|
};
|
||||||
connection.send(msg).await?;
|
if let Some(limit) = connection.max_message_size {
|
||||||
|
let message_size = msg.len();
|
||||||
|
if message_size > limit {
|
||||||
|
return Err(GenericError(format!(
|
||||||
|
"WebSocket message too large: found {message_size} bytes, the limit is {limit} bytes"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connection.sink.send(msg).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +104,7 @@ impl WebsocketManager {
|
|||||||
info!("Closing websocket");
|
info!("Closing websocket");
|
||||||
if let Some(mut connection) = self.connections.lock().await.remove(id) {
|
if let Some(mut connection) = self.connections.lock().await.remove(id) {
|
||||||
// Wait a maximum of 1 second for the connection to close
|
// Wait a maximum of 1 second for the connection to close
|
||||||
if let Err(e) = connection.close().await {
|
if let Err(e) = connection.sink.close().await {
|
||||||
warn!("Failed to close websocket connection {e:?}");
|
warn!("Failed to close websocket connection {e:?}");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+874
-535
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -121,14 +121,13 @@
|
|||||||
"nodejs-file-downloader": "^4.13.0",
|
"nodejs-file-downloader": "^4.13.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.20",
|
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||||
"vite-plus": "^0.1.20",
|
"vite-plus": "^0.2.1",
|
||||||
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.20"
|
"vitest": "^4.1.9"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"js-yaml": "^4.1.1",
|
"js-yaml": "^4.1.1",
|
||||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.20",
|
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1"
|
||||||
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.20"
|
|
||||||
},
|
},
|
||||||
"packageManager": "npm@11.11.1"
|
"packageManager": "npm@11.11.1"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -108,6 +108,7 @@ export type Folder = {
|
|||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
settingFollowRedirects: InheritedBoolSetting;
|
settingFollowRedirects: InheritedBoolSetting;
|
||||||
settingRequestTimeout: InheritedIntSetting;
|
settingRequestTimeout: InheritedIntSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GraphQlIntrospection = {
|
export type GraphQlIntrospection = {
|
||||||
@@ -183,6 +184,7 @@ export type GrpcRequest = {
|
|||||||
*/
|
*/
|
||||||
url: string;
|
url: string;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HttpRequest = {
|
export type HttpRequest = {
|
||||||
@@ -426,7 +428,8 @@ export type WebsocketEvent = {
|
|||||||
messageType: WebsocketEventType;
|
messageType: WebsocketEventType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
export type WebsocketEventType =
|
||||||
|
"binary" | "close" | "error" | "frame" | "open" | "ping" | "pong" | "text";
|
||||||
|
|
||||||
export type WebsocketRequest = {
|
export type WebsocketRequest = {
|
||||||
model: "websocket_request";
|
model: "websocket_request";
|
||||||
@@ -450,6 +453,7 @@ export type WebsocketRequest = {
|
|||||||
settingSendCookies: InheritedBoolSetting;
|
settingSendCookies: InheritedBoolSetting;
|
||||||
settingStoreCookies: InheritedBoolSetting;
|
settingStoreCookies: InheritedBoolSetting;
|
||||||
settingValidateCertificates: InheritedBoolSetting;
|
settingValidateCertificates: InheritedBoolSetting;
|
||||||
|
settingRequestMessageSize: InheritedIntSetting;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Workspace = {
|
export type Workspace = {
|
||||||
@@ -466,6 +470,7 @@ export type Workspace = {
|
|||||||
settingValidateCertificates: boolean;
|
settingValidateCertificates: boolean;
|
||||||
settingFollowRedirects: boolean;
|
settingFollowRedirects: boolean;
|
||||||
settingRequestTimeout: number;
|
settingRequestTimeout: number;
|
||||||
|
settingRequestMessageSize: number;
|
||||||
settingDnsOverrides: Array<DnsOverride>;
|
settingDnsOverrides: Array<DnsOverride>;
|
||||||
settingSendCookies: boolean;
|
settingSendCookies: boolean;
|
||||||
settingStoreCookies: boolean;
|
settingStoreCookies: boolean;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs"
|
"build:main": "esbuild src/index.ts --bundle --platform=node --outfile=../../crates-tauri/yaak-app-client/vendored/plugin-runtime/index.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.20.1"
|
"ws": "^8.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.0.13",
|
"@types/node": "^24.0.13",
|
||||||
|
|||||||
@@ -166,6 +166,9 @@ function toastColorVariables(theme: Theme, color: YaakColor): CSSVariables {
|
|||||||
|
|
||||||
function bannerColorVariables(theme: Theme, color: YaakColor): CSSVariables {
|
function bannerColorVariables(theme: Theme, color: YaakColor): CSSVariables {
|
||||||
return completeFullColorVariables(theme, {
|
return completeFullColorVariables(theme, {
|
||||||
|
text: color.desaturate(0.5).lift(0.12).css(),
|
||||||
|
textSubtle: color.desaturate(0.58).lift(0.04).translucify(0.04).css(),
|
||||||
|
textSubtlest: color.desaturate(0.65).translucify(0.18).css(),
|
||||||
surface: color.translucify(0.95).css(),
|
surface: color.translucify(0.95).css(),
|
||||||
surfaceHighlight: color.translucify(0.85).css(),
|
surfaceHighlight: color.translucify(0.85).css(),
|
||||||
border: color.lift(0.3).translucify(0.8).css(),
|
border: color.lift(0.3).translucify(0.8).css(),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"@hono/mcp": "^0.2.3",
|
"@hono/mcp": "^0.2.3",
|
||||||
"@hono/node-server": "^1.19.13",
|
"@hono/node-server": "^1.19.13",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"hono": "^4.12.14",
|
"hono": "^4.12.25",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ const config = JSON.stringify({
|
|||||||
const normalizedAdditionalArgs = [];
|
const normalizedAdditionalArgs = [];
|
||||||
for (let i = 0; i < additionalArgs.length; i++) {
|
for (let i = 0; i < additionalArgs.length; i++) {
|
||||||
const arg = additionalArgs[i];
|
const arg = additionalArgs[i];
|
||||||
|
if (arg === "--") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (arg === "--config" && i + 1 < additionalArgs.length) {
|
if (arg === "--config" && i + 1 < additionalArgs.length) {
|
||||||
const value = additionalArgs[i + 1];
|
const value = additionalArgs[i + 1];
|
||||||
const isInlineJson = value.trimStart().startsWith("{");
|
const isInlineJson = value.trimStart().startsWith("{");
|
||||||
|
|||||||
Reference in New Issue
Block a user