Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ export default function ServicePreview() {
console.log('Service data updated:', event.detail.service);
};

// `baseUrl` rather than `provider`: the component reads `provider` only on
// its first update, which runs before React sets it on the server-rendered
// element, but it reloads when `baseUrl` changes
return (
<PreviewLayout>
<ECCClientGa4ghServiceRegistryService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ export default function ServicesPreview() {
console.log('Services data updated:', event.detail.services);
};

// `baseUrl` rather than `provider`: the component reads `provider` only on
// its first update, which runs before React sets it on the server-rendered
// element, but it reloads when `baseUrl` changes
return (
<PreviewLayout>
<ECCClientGa4ghServiceRegistryServices
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ export default function ToolCreatePreview() {
return <MissingEndpoint service='TRS-Filer' variable='NEXT_PUBLIC_TRS_FILER_URL' />;
}

// `baseUrl` rather than `provider`: the component reads `provider` only on
// its first update, which runs before React sets it on the server-rendered
// element, but it reloads when `baseUrl` changes
return (
<PreviewLayout>
<ECCClientElixirTrsToolCreate baseUrl={endpoints.trsFiler} />
Expand Down
3 changes: 0 additions & 3 deletions apps/documentation/components/packages/trs/ToolsPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import PreviewLayout from '@/components/common/PreviewLayout';
import { endpoints } from '@/utils/endpoints';

export default function ToolsPreview() {
// `baseUrl` rather than `provider`: the component reads `provider` only on
// its first update, which runs before React sets it on the server-rendered
// element, but it reloads when `baseUrl` changes
return (
<PreviewLayout>
<ECCClientGa4ghTrsTools baseUrl={endpoints.trs} />
Expand Down
7 changes: 7 additions & 0 deletions apps/documentation/pages/docs/providers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ const provider = new RestProvider('https://api.example.com');
// 4. Handle events/states as needed
```

You can set or replace a component's provider at any time, also after the
component has rendered (as with React, which sets it on a server-rendered
element only after the first update): the component then loads its data from
the new provider. Create the provider once and pass the same instance on every
render (e.g. with `useMemo` in React), as each new instance makes the component
load its data again.
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved

## Common Use Cases

### Environment-Based Configuration
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"lint:fix": "eslint . --fix && turbo run lint:fix && prettier .",
"new": "turbo gen",
"prepare": "husky install",
"test": "turbo run test --parallel",
"test": "turbo run test --concurrency=3",
"test:watch": "turbo run test --watch"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/ecc-client-elixir-trs-filer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"build": "node ../../scripts/tailwind/index.js && node ../../scripts/build.js -p ecc-client-elixir-trs-filer-",
"dev": "concurrently -r \"node ../../scripts/tailwind/index.js --watch\" \"node ../../scripts/build.js -p ecc-client-elixir-trs-filer- --watch\" \"wds\"",
"clean": "rm -rf dist node_modules custom-elements-manifest.config.js src/output.css src/tailwind.ts",
"test": "web-test-runner --group default --config ../../web-test.runner-config.mjs --port 8006",
"test:watch": "wtr --watch",
"lint": "npx eslint .",
"lint:fix": "npm run lint -- --fix",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* eslint-disable import/no-extraneous-dependencies */
import { expect, fixture, html, oneEvent, waitUntil } from "@open-wc/testing";
import sinon from "sinon";
import "../index.js";
import type { ECCClientElixirTrsToolCreate } from "../tool-create.js";
import type { TrsFilerProvider } from "../../../providers/index.js";

const BASE_URL = "https://trs.example.org/ga4gh/trs/v2";
const OTHER_URL = "https://other.example.org/ga4gh/trs/v2";

type ToolClass = { id: string; name: string };

/**
* A provider whose tool class list is a stub; the form loads nothing else
* before a submit.
*/
const stubProvider = (
classes: ToolClass[] | Promise<ToolClass[]> = [
{ id: "workflow", name: "Workflow" },
]
) => {
const stubs = {
getToolClasses: sinon.stub().callsFake(async () => classes),
};
return { stubs, provider: stubs as unknown as TrsFilerProvider };
};

/** A promise and the function that resolves it. */
const deferred = <T>() => {
let settleWith: (value: T) => void = () => undefined;
const promise = new Promise<T>((resolve) => {
settleWith = resolve;
});
return { promise, resolve: settleWith };
};

/** The tool classes the form offers. */
const offered = (el: ECCClientElixirTrsToolCreate) =>
(el as unknown as { toolClasses: ToolClass[] }).toolClasses;

describe("ecc-client-elixir-trs-tool-create provider", () => {
let fetchStub: sinon.SinonStub;

beforeEach(() => {
fetchStub = sinon.stub(window, "fetch").callsFake(
async () =>
new Response(JSON.stringify([]), {
headers: { "Content-Type": "application/json" },
})
);
});

afterEach(() => {
sinon.restore();
});

it("loads from a provider set after the first update", async () => {
const el = await fixture<ECCClientElixirTrsToolCreate>(
html`<ecc-client-elixir-trs-tool-create></ecc-client-elixir-trs-tool-create>`
);
const { stubs, provider } = stubProvider();

el.provider = provider;
await waitUntil(() => offered(el)[0]?.id === "workflow");

expect(stubs.getToolClasses.callCount).to.equal(1);
expect(fetchStub.callCount).to.equal(0);
});

it("loads from a provider that replaces the first one", async () => {
const first = stubProvider();
const second = stubProvider([{ id: "tool", name: "Tool" }]);
const el = await fixture<ECCClientElixirTrsToolCreate>(
html`<ecc-client-elixir-trs-tool-create
.provider=${first.provider}
></ecc-client-elixir-trs-tool-create>`
);
await waitUntil(() => offered(el)[0]?.id === "workflow");

el.provider = second.provider;
await waitUntil(() => offered(el)[0]?.id === "tool");

expect(first.stubs.getToolClasses.callCount).to.equal(1);
});

it("drops a slower load from a replaced provider", async () => {
const slow = deferred<ToolClass[]>();
const first = stubProvider(slow.promise);
const second = stubProvider([{ id: "tool", name: "Tool" }]);
const el = await fixture<ECCClientElixirTrsToolCreate>(
html`<ecc-client-elixir-trs-tool-create
.provider=${first.provider}
></ecc-client-elixir-trs-tool-create>`
);

el.provider = second.provider;
await waitUntil(() => offered(el)[0]?.id === "tool");
slow.resolve([{ id: "workflow", name: "Workflow" }]);
await slow.promise;
await el.updateComplete;

expect(offered(el)[0].id).to.equal("tool");
});

it("keeps a provider when the base URL is set too", async () => {
const { stubs, provider } = stubProvider();
const el = await fixture<ECCClientElixirTrsToolCreate>(
html`<ecc-client-elixir-trs-tool-create
.provider=${provider}
></ecc-client-elixir-trs-tool-create>`
);
await waitUntil(() => stubs.getToolClasses.callCount === 1);

el.baseUrl = BASE_URL;
await el.updateComplete;

expect(stubs.getToolClasses.callCount).to.equal(1);
expect(fetchStub.callCount).to.equal(0);
});

it("loads from the base URL and reloads when it changes", async () => {
const el = await fixture<ECCClientElixirTrsToolCreate>(
html`<ecc-client-elixir-trs-tool-create
.baseUrl=${BASE_URL}
></ecc-client-elixir-trs-tool-create>`
);
await waitUntil(() => fetchStub.callCount === 1);

el.baseUrl = OTHER_URL;
await waitUntil(() => fetchStub.callCount === 2);

expect(fetchStub.firstCall.args[0]).to.equal(`${BASE_URL}/toolClasses`);
expect(fetchStub.secondCall.args[0]).to.equal(`${OTHER_URL}/toolClasses`);
});

it("offers no tool classes once the provider is removed without a base URL", async () => {
const { provider } = stubProvider();
const el = await fixture<ECCClientElixirTrsToolCreate>(
html`<ecc-client-elixir-trs-tool-create
.provider=${provider}
></ecc-client-elixir-trs-tool-create>`
);
await waitUntil(() => offered(el).length === 1);
const failed = oneEvent(el, "ecc-tool-create-validation-failed");

el.provider = undefined;
await el.updateComplete;

expect(offered(el)).to.deep.equal([]);
expect((await failed).detail.error).to.contain("Please provide either");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ export class ECCClientElixirTrsToolCreate extends LitElement {

private _provider: TrsFilerProvider | null = null;

/** The REST provider for `baseUrl`, used without a custom provider */
private restProvider: RestTrsFilerProvider | null = null;

@state() private activeFileIndex: { [versionIndex: number]: number } = {};
@state() private activeDescriptorType: {
[versionIndex: number]: DescriptorType | "ALL";
Expand All @@ -253,7 +256,8 @@ export class ECCClientElixirTrsToolCreate extends LitElement {
if (this.provider) {
this._provider = this.provider;
} else if (this.baseUrl) {
this._provider = new RestTrsFilerProvider(this.baseUrl);
this.restProvider = new RestTrsFilerProvider(this.baseUrl);
this._provider = this.restProvider;
} else {
this._provider = null;
}
Expand Down Expand Up @@ -337,18 +341,58 @@ export class ECCClientElixirTrsToolCreate extends LitElement {
this.initializeFormWithDefaults();
}

if (changedProperties.has("baseUrl") && this.baseUrl) {
this._provider = new RestTrsFilerProvider(this.baseUrl);
this.loadToolClasses();
if (
(changedProperties.has("provider") || changedProperties.has("baseUrl")) &&
this.useProvider()
Comment thread
uniqueg marked this conversation as resolved.
Resolved
) {
if (this._provider) {
Comment thread
uniqueg marked this conversation as resolved.
Resolved
this.loadToolClasses();
Comment thread
uniqueg marked this conversation as resolved.
Resolved
Comment thread
uniqueg marked this conversation as resolved.
Resolved
} else {
this.toolClasses = [];
Comment thread
uniqueg marked this conversation as resolved.
Resolved
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved
this.dispatchEvent(
new CustomEvent("ecc-tool-create-validation-failed", {
detail: {
error:
"Please provide either a base URL for the TRS API or a custom provider.",
},
bubbles: true,
composed: true,
})
);
}
}
}

/**
* Switches to the custom provider or, without one, to a REST provider for
* the base URL (to none without either); returns whether the provider in
* use changed
*/
private useProvider(): boolean {
let provider: TrsFilerProvider | null = this.provider ?? null;
if (!provider && this.baseUrl) {
if (this.restProvider?.baseUrl !== this.baseUrl) {
this.restProvider = new RestTrsFilerProvider(this.baseUrl);
}
provider = this.restProvider;
}
if (provider === this._provider) return false;
this._provider = provider;
return true;
}

private async loadToolClasses(): Promise<void> {
if (!this._provider) return;
const provider = this._provider;
if (!provider) return;

try {
this.toolClasses = await this._provider.getToolClasses();
const toolClasses = await provider.getToolClasses();
// a load from a provider that was replaced in the meantime is dropped
if (provider !== this._provider) return;
Comment thread
uniqueg marked this conversation as resolved.
Resolved
Comment thread
uniqueg marked this conversation as resolved.
Resolved
Comment thread
uniqueg marked this conversation as resolved.
Resolved
this.toolClasses = toolClasses;
} catch (error) {
if (provider !== this._provider) return;
this.toolClasses = [];
console.error("Failed to load tool classes:", error);
this.dispatchEvent(
new CustomEvent("ecc-tool-create-failed", {
Expand Down
Loading
Loading