Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

experimental: Add System Resource instead of $resources url #3341

Merged
merged 4 commits into from
May 11, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add System Resource instead of $resources url
istarkov committed May 9, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 221ab3da3eef4cf5680177a5bfb9711f8513c56f
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import type { DataSource, Resource } from "@webstudio-is/sdk";
import {
encodeDataSourceVariable,
isLiteralExpression,
isLocalResource,
sitemapResourceUrl,
} from "@webstudio-is/sdk";
import {
Box,
@@ -446,10 +446,6 @@ export const ResourceForm = forwardRef<
try {
new URL(evaluatedValue);
} catch {
if (isLocalResource(evaluatedValue, "sitemap.xml")) {
return;
}

return "URL is invalid";
}
},
@@ -630,3 +626,96 @@ export const ResourceForm = forwardRef<
);
});
ResourceForm.displayName = "ResourceForm";

export const SystemResourceForm = forwardRef<
undefined | PanelApi,
{ variable?: DataSource; nameField: Field<string> }
>(({ variable, nameField }, ref) => {
const resources = useStore($resources);

const resource =
variable?.type === "resource"
? resources.get(variable.resourceId)
: undefined;

const method = "get";

const localResources = [
{
label: "Sitemap",
value: JSON.stringify(sitemapResourceUrl()),
description: "Resource that loads the sitemap data of the current site.",
},
];

const [localResource, setLocalResource] = useState(() => {
return (
localResources.find(
(localResource) => localResource.value === resource?.url
) ?? localResources[0]
);
});

const form = composeFields(nameField);

useImperativeHandle(ref, () => ({
...form,
save: () => {
const instanceSelector = $selectedInstanceSelector.get();
if (instanceSelector === undefined) {
return;
}
const [instanceId] = instanceSelector;

const newResource: Resource = {
id: resource?.id ?? nanoid(),
name: nameField.value,
url: localResource.value,
method,
headers: [],
};

const newVariable: DataSource = {
id: variable?.id ?? nanoid(),
// preserve existing instance scope when edit
scopeInstanceId: variable?.scopeInstanceId ?? instanceId,
name: nameField.value,
type: "resource",
resourceId: newResource.id,
};

serverSyncStore.createTransaction(
[$dataSources, $resources],
(dataSources, resources) => {
dataSources.set(newVariable.id, newVariable);
resources.set(newResource.id, newResource);
}
);
},
}));

const resourceId = useId();

return (
<>
<Flex direction="column" css={{ gap: theme.spacing[3] }}>
<Label htmlFor={resourceId}>Resource</Label>
<Select
options={localResources}
getLabel={(option) => option.label}
getValue={(option) => option.value}
getDescription={(option) => {
return (
<Box css={{ width: theme.spacing[25] }}>
{option?.description}
</Box>
);
}}
value={localResource}
onChange={setLocalResource}
/>
</Flex>
</>
);
});
SystemResourceForm.displayName = "ResourceForm";
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ import {
Tooltip,
theme,
} from "@webstudio-is/design-system";
import { transpileExpression } from "@webstudio-is/sdk";
import { isLocalResource, transpileExpression } from "@webstudio-is/sdk";
import type { DataSource } from "@webstudio-is/sdk";
import {
ExpressionEditor,
@@ -62,7 +62,7 @@ import {
EditorDialogButton,
EditorDialogControl,
} from "~/builder/shared/code-editor-base";
import { ResourceForm } from "./resource-panel";
import { ResourceForm, SystemResourceForm } from "./resource-panel";
import { generateCurl } from "./curl";

/**
@@ -108,6 +108,7 @@ type VariableType =
| "boolean"
| "json"
| "resource"
| "system-resource"
| "parameter";

type PanelApi = ComposedFields & {
@@ -359,6 +360,7 @@ const VariablePanel = forwardRef<
}
>(({ variable }, ref) => {
const { allowDynamicData } = useStore($userPlanFeatures);
const resources = useStore($resources);

const nameField = useField({
initialValue: variable?.name ?? "",
@@ -383,10 +385,18 @@ const VariablePanel = forwardRef<
</Flex>
);

const [type, setType] = useState<VariableType>(() => {
const [variableType, setVariableType] = useState<VariableType>(() => {
if (
variable?.type === "resource" &&
isLocalResource(JSON.parse(resources.get(variable.resourceId)?.url ?? ""))
) {
return "system-resource";
}

if (variable?.type === "parameter" || variable?.type === "resource") {
return variable.type;
}

if (variable?.type === "variable") {
const type = variable.value.type;
if (type === "string" || type === "number" || type === "boolean") {
@@ -427,7 +437,20 @@ const VariablePanel = forwardRef<
"A Resource is a configuration for secure data fetching. You can safely use secrets in any field.",
},
],
[
"system-resource",
{
label: (
<Flex direction="row" gap="2" align="center">
System Resource
{allowDynamicData === false && <ProBadge>Pro</ProBadge>}
</Flex>
),
description: "A System Resource is a configuration for system data.",
},
],
]);

const typeFieldElement = (
<Flex direction="column" gap="1">
<Label>Type</Label>
@@ -444,21 +467,21 @@ const VariablePanel = forwardRef<
</Box>
);
}}
value={type}
onChange={setType}
value={variableType}
onChange={setVariableType}
/>
</Flex>
);

if (type === "parameter") {
if (variableType === "parameter") {
return (
<>
{nameFieldElement}
<ParameterForm ref={ref} variable={variable} nameField={nameField} />
</>
);
}
if (type === "string") {
if (variableType === "string") {
return (
<>
{nameFieldElement}
@@ -467,7 +490,7 @@ const VariablePanel = forwardRef<
</>
);
}
if (type === "number") {
if (variableType === "number") {
return (
<>
{nameFieldElement}
@@ -476,7 +499,7 @@ const VariablePanel = forwardRef<
</>
);
}
if (type === "boolean") {
if (variableType === "boolean") {
return (
<>
{nameFieldElement}
@@ -485,7 +508,7 @@ const VariablePanel = forwardRef<
</>
);
}
if (type === "json") {
if (variableType === "json") {
return (
<>
{nameFieldElement}
@@ -494,7 +517,8 @@ const VariablePanel = forwardRef<
</>
);
}
if (type === "resource") {

if (variableType === "resource") {
return (
<>
{nameFieldElement}
@@ -503,6 +527,22 @@ const VariablePanel = forwardRef<
</>
);
}

if (variableType === "system-resource") {
return (
<>
{nameFieldElement}
{typeFieldElement}
<SystemResourceForm
ref={ref}
variable={variable}
nameField={nameField}
/>
</>
);
}

variableType satisfies never;
});
VariablePanel.displayName = "VariablePanel";

23 changes: 20 additions & 3 deletions apps/builder/app/routes/rest.resources-loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json, type ActionFunctionArgs } from "@remix-run/server-runtime";
import {
loadResource,
isLocalResource,
@@ -21,9 +21,26 @@ export const action = async ({ request }: ActionFunctionArgs) => {
return fetch(input, init);
};

const computedResources = z
const requestJson = await request.json();

const computedResourcesParsed = z
.array(ResourceRequest)
.parse(await request.json());
.safeParse(requestJson);

if (computedResourcesParsed.success === false) {
console.error(
"computedResources.parse",
computedResourcesParsed.error.toString()
);
console.error("data:", requestJson);

throw json(computedResourcesParsed.error, {
status: 400,
});
}

const computedResources = computedResourcesParsed.data;

const responses = await Promise.all(
computedResources.map((resource) => loadResource(customFetch, resource))
);
16 changes: 13 additions & 3 deletions packages/sdk/src/schema/resources.ts
Original file line number Diff line number Diff line change
@@ -49,6 +49,16 @@ const LOCAL_RESOURCE_PREFIX = "$resources";
/**
* Prevents fetch cycles by prefixing local resources.
*/
export const isLocalResource = (pathname: string, resourceName: string) =>
pathname.split("/").filter(Boolean).join("/") ===
`${LOCAL_RESOURCE_PREFIX}/${resourceName}`;
export const isLocalResource = (pathname: string, resourceName?: string) => {
const segments = pathname.split("/").filter(Boolean);

if (resourceName === undefined) {
return segments[0] === LOCAL_RESOURCE_PREFIX;
}

return segments.join("/") === `${LOCAL_RESOURCE_PREFIX}/${resourceName}`;
};

export const sitemapResourceUrl = () => {
return `/${LOCAL_RESOURCE_PREFIX}/sitemap.xml`;
};