Skip to content

Commit

Permalink
feat: full default template
Browse files Browse the repository at this point in the history
  • Loading branch information
Keyrxng committed Nov 22, 2024
1 parent e9674ad commit 8fcb91a
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 19 deletions.
2 changes: 1 addition & 1 deletion static/manifest-gui.css
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ button#reset-to-default::before {
content: "♻️";
}
button#reset-to-default:hover::before {
content: "Use Defaults";
content: "Reset";
}
button#reset-to-default:active::before {
content: "♻️♻️♻️♻️♻️";
Expand Down
17 changes: 12 additions & 5 deletions static/scripts/config-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Plugin, PluginConfig } from "../types/plugins";
import { Octokit } from "@octokit/rest";
import { toastNotification } from "../utils/toaster";
import { CONFIG_FULL_PATH, CONFIG_ORG_REPO } from "@ubiquity-os/plugin-sdk/constants";
import { AuthService } from "./authentication";

export class ConfigParser {
repoConfig: string | null = null;
Expand Down Expand Up @@ -143,7 +144,11 @@ export class ConfigParser {
return this.createOrUpdateFileContents(org, repo, path, octokit);
}

async createOrUpdateFileContents(org: string, repo: string, path: string, octokit: Octokit) {
async createOrUpdateFileContents(org: string, repo: string, path: string, octokit: AuthService["octokit"]) {
if (!octokit) {
throw new Error("Octokit not found");
}

const recentSha = await octokit.repos.getContent({
owner: org,
repo: repo,
Expand Down Expand Up @@ -173,9 +178,7 @@ export class ConfigParser {
addPlugin(plugin: Plugin) {
const config = this.loadConfig();
const parsedConfig = YAML.parse(config);
if (!parsedConfig.plugins) {
parsedConfig.plugins = [];
}
parsedConfig.plugins ??= [];
parsedConfig.plugins.push(plugin);
this.newConfigYml = YAML.stringify(parsedConfig);
this.saveConfig();
Expand All @@ -193,7 +196,11 @@ export class ConfigParser {
this.saveConfig();
}

loadConfig(): string {
loadConfig(config?: string) {
if (config) {
this.saveConfig(config);
}

if (!this.newConfigYml) {
this.newConfigYml = localStorage.getItem("config") as string;
}
Expand Down
197 changes: 191 additions & 6 deletions static/scripts/predefined-configs/template-handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Octokit } from "@octokit/rest";
import { toastNotification } from "../../utils/toaster";
import { ManifestRenderer } from "../render-manifest";
import { CONFIG_FULL_PATH, CONFIG_ORG_REPO } from "@ubiquity-os/plugin-sdk/constants";
import { STRINGS } from "../../utils/strings";
import { Manifest, ManifestCache, ManifestPreDecode, Plugin, PluginConfig } from "../../types/plugins";
import { AnySchemaObject } from "ajv";
import { createInputRow } from "../../utils/element-helpers";
import { controlButtons } from "../rendering/control-buttons";
import { updateGuiTitle } from "../rendering/utils";
import { parseConfigInputs } from "../rendering/input-parsing";
import YAML from "yaml";
import { AuthService } from "../authentication";

type TemplateTypes = "minimal" | "full-defaults" | "custom";
type MinimalPredefinedConfig = {
Expand All @@ -24,7 +31,8 @@ export async function configTemplateHandler(type: TemplateTypes, renderer: Manif
if (type === "minimal") {
config = await handleMinimalTemplate();
} else if (type === "full-defaults") {
config = await handleFullDefaultsTemplate();
await handleFullDefaultsTemplate(renderer);
return;
} else {
throw new Error("Invalid template type");
}
Expand Down Expand Up @@ -65,7 +73,7 @@ export async function configTemplateHandler(type: TemplateTypes, renderer: Manif
}
}

async function writeTemplate(renderer: ManifestRenderer, config: string, type: TemplateTypes, octokit: Octokit, org: string) {
async function writeTemplate(renderer: ManifestRenderer, config: string, type: TemplateTypes, octokit: AuthService["octokit"], org: string) {
try {
renderer.configParser.saveConfig(config);
toastNotification(`Successfully loaded ${type} template. Do you want to push to GitHub? `, {
Expand Down Expand Up @@ -109,7 +117,184 @@ async function handleMinimalTemplate(): Promise<string> {
}
}

// requires more thought
async function handleFullDefaultsTemplate() {
return "";
async function handleFullDefaultsTemplate(renderer: ManifestRenderer) {
renderer.configParser.writeBlankConfig();

const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache;
const pluginUrls = Object.keys(manifestCache);
const cleanManifestCache = Object.keys(manifestCache).reduce((acc, key) => {
if (manifestCache[key]?.name) {
acc[key] = manifestCache[key];
}
return acc;
}, {} as ManifestCache);

const plugins: { name: string; defaults: ManifestPreDecode["configuration"] }[] = [];

pluginUrls.forEach((url) => {
if (!cleanManifestCache[url]?.name) {
return;
}

const defaultForInstalled: ManifestPreDecode | null = cleanManifestCache[url];
const manifestCache = JSON.parse(localStorage.getItem("manifestCache") || "{}") as ManifestCache;
const pluginUrls = Object.keys(manifestCache);
const pluginUrl = pluginUrls.find((url) => {
return manifestCache[url].name === defaultForInstalled.name;
});

const plugin = manifestCache[pluginUrl || ""];
const config = plugin?.configuration;

if (!config) {
return;
}

const defaults = buildDefaultValues<ManifestPreDecode["configuration"]>(config);

plugins.push({
name: plugin.name,
defaults,
});
});

renderRequiredFields(renderer, plugins).catch((error) => {
console.error("Error rendering required fields:", error);
toastNotification("An error occurred while rendering the required fields.", {
type: "error",
shouldAutoDismiss: true,
});
});
}

/**
* undefined === not a required field, can be omitted
* null === required field, but no default value
*
* for each null value, we need to render an input for that field
* referencing the plugin name. Expect there to be lots but in reality
* there are only a few.
*
*/

async function renderRequiredFields(renderer: ManifestRenderer, plugins: { name: string; defaults: ManifestPreDecode["configuration"] }[]) {
const configDefaults: Record<string, { type: string; value: unknown; items: { type: string } | null }> = {};
renderer.manifestGuiBody.innerHTML = null;

plugins.forEach((plugin) => {
const { defaults } = plugin;
const keys = Object.keys(defaults);
keys.forEach((key) => {
if (defaults[key as keyof typeof defaults] === null) {
createInputRow(key, defaults, configDefaults);
}
});
});

updateGuiTitle("Fill in required fields");
controlButtons({ hide: false });

const resetToDefaultButton = document.getElementById("reset-to-default") as HTMLButtonElement;
if (!resetToDefaultButton) {
throw new Error("Reset to default button not found");
}

resetToDefaultButton.addEventListener("click", () => {
renderRequiredFields(renderer, plugins).catch((error) => {
console.error("Error rendering required fields:", error);
toastNotification("An error occurred while rendering the required fields.", {
type: "error",
shouldAutoDismiss: true,
});
});
});

const add = document.getElementById("add") as HTMLButtonElement;
const remove = document.getElementById("remove") as HTMLButtonElement;
if (!add || !remove) {
throw new Error("Add or remove button not found");
}
remove.remove();

add.addEventListener("click", () => {
const configInputs = document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>(".config-input");
const newConfig = parseConfigInputs(configInputs, {} as Manifest, plugins);

const officialPluginConfig: Record<string, { actionUrl?: string; workerUrl?: string }> = JSON.parse(localStorage.getItem("officialPluginConfig") || "{}");

const pluginArr: Plugin[] = [];
for (const [name, config] of Object.entries(newConfig)) {
// this relies on the manifest matching the repo name
const normalizedPluginName = name
.toLowerCase()
.replace(/ /g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/-+/g, "-");

const pluginUrl = Object.keys(officialPluginConfig).find((url) => {
return url.includes(normalizedPluginName);
});

if (!pluginUrl) {
toastNotification(`No plugin URL found for ${normalizedPluginName}.`, {
type: "error",
shouldAutoDismiss: true,
});

return;
}

const plugin: Plugin = {
uses: [
{
plugin: pluginUrl,
with: config as Record<string, unknown>,
},
],
};

pluginArr.push(plugin);
}

const pluginConfig: PluginConfig = {
plugins: pluginArr,
};

const org = localStorage.getItem("selectedOrg");
if (!org) {
throw new Error("No selected org found");
}

writeTemplate(renderer, YAML.stringify(pluginConfig), "full-defaults", renderer.auth.octokit, localStorage.getItem("selectedOrg") || "").catch((error) => {
console.error("Error writing template:", error);
toastNotification("An error occurred while writing the template.", {
type: "error",
shouldAutoDismiss: true,
});
});
});
}

function buildDefaultValues<T>(schema: AnySchemaObject): T {
const defaults: Partial<T> = {};
const requiredProps = schema.required || [];

for (const key of Object.keys(schema.properties)) {
if (Reflect.has(schema.properties, key)) {
const hasDefault = "default" in schema.properties[key];
const value = schema.properties[key].default;

const _key = key as keyof T;

if (hasDefault && value) {
defaults[_key] = value;
} else if (requiredProps.includes(_key)) {
defaults[_key] = null as unknown as T[keyof T];
} else {
defaults[_key] = undefined;
}
}
}

return defaults as T;
}
61 changes: 58 additions & 3 deletions static/scripts/rendering/input-parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,71 @@ export function processProperties(renderer: ManifestRenderer, props: Record<stri
});
}

export function parseConfigInputs(configInputs: NodeListOf<HTMLInputElement | HTMLTextAreaElement>, manifest: Manifest): { [key: string]: unknown } {
export function parseConfigInputs(
configInputs: NodeListOf<HTMLInputElement | HTMLTextAreaElement>,
manifest: Manifest,
fullDefaultTemplate?: {
name: string;
defaults: Record<string, unknown>;
}[]
): { [key: string]: unknown } {
const config: Record<string, unknown> = {};
const schema = manifest.configuration;
if (!schema) {
const schema = manifest?.configuration;
if (!schema && !fullDefaultTemplate) {
throw new Error("No schema found in manifest");
}

if (fullDefaultTemplate) {
configInputs.forEach((input) => {
const key = input.getAttribute("data-config-key");
if (!key) {
throw new Error("Input key is required");
}

const template = fullDefaultTemplate.find((plugin) => Object.keys(plugin.defaults).includes(key));
if (!template) {
throw new Error(`No template found for key: ${key}`);
}

let value: unknown;
const expectedType = input.getAttribute("data-type");

if (expectedType === "boolean") {
value = (input as HTMLInputElement).checked;
} else if (expectedType === "object" || expectedType === "array") {
try {
value = JSON.parse((input as HTMLTextAreaElement).value);
} catch (e) {
console.error(e);
throw new Error(`Invalid JSON input for ${expectedType} at key "${key}": ${input.value}`);
}
} else {
value = (input as HTMLInputElement).value;
}

template.defaults[key] = value;

fullDefaultTemplate.forEach((plugin) => {
if (plugin.name === template.name) {
plugin.defaults = template.defaults;
}
});
});

return fullDefaultTemplate.reduce(
(acc, curr) => {
acc[curr.name] = curr.defaults;
return acc;
},
{} as Record<string, unknown>
);
}

const validate = ajv.compile(schema as AnySchemaObject);

configInputs.forEach((input) => {
const key = input.getAttribute("data-config-key");
console.log("key", key);
if (!key) {
throw new Error("Input key is required");
}
Expand Down
5 changes: 2 additions & 3 deletions static/scripts/rendering/write-add-remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ export function writeNewConfig(renderer: ManifestRenderer, option: "add" | "remo
});
throw new Error("No selected plugin manifest found");
}
const pluginManifest = JSON.parse(selectedManifest) as Manifest;
const configInputs = document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>(".config-input");

const pluginManifest = JSON.parse(selectedManifest || "{}") as Manifest;
const configInputs = document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>(".config-input");
const newConfig = parseConfigInputs(configInputs, pluginManifest);

renderer.configParser.loadConfig();

const officialPluginConfig: Record<string, { actionUrl?: string; workerUrl?: string }> = JSON.parse(localStorage.getItem("officialPluginConfig") || "{}");

const pluginName = pluginManifest.name;

// this relies on the manifest matching the repo name
Expand Down
1 change: 0 additions & 1 deletion static/utils/element-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export function createInputRow(
configDefaults: Record<string, { type: string; value: unknown; items: { type: string } | null }>
): void {
const row = document.createElement("tr");

const headerCell = document.createElement("td");
headerCell.className = "table-data-header";
headerCell.textContent = key.replace(/([A-Z])/g, " $1");
Expand Down

0 comments on commit 8fcb91a

Please sign in to comment.