Skip to content

Commit 5bcf13d

Browse files
authored
Adding SDK-created extensions support (#1052)
* Adding SDK-created extensions support
1 parent be50533 commit 5bcf13d

File tree

11 files changed

+210
-38
lines changed

11 files changed

+210
-38
lines changed

src/components/Extensions/Details/DetailCard/Tabs/ParamList.test.tsx

+43-5
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('ParamList', () => {
2727
const PARAM = 'pirojok-the-param';
2828
const LABEL = 'pirojok-the-label';
2929
const VALUE = 'pirojok-the-value';
30-
const extension: Extension = {
30+
const staticExtension: Extension = {
3131
id,
3232
params: [
3333
{
@@ -39,6 +39,28 @@ describe('ParamList', () => {
3939
],
4040
} as Extension;
4141

42+
const dynId = 'dyn-pirojok';
43+
const DYN_DESCRIPTION = 'dyn-pirojok-the-description';
44+
const DYN_PARAM = 'dyn-pirojok-the-param';
45+
const DYN_LABEL = 'dyn-pirojok-the-label';
46+
const DYN_VALUE = 'dyn-pirojok-the-value';
47+
const LABELS: Record<string, string> = {
48+
createdBy: 'SDK',
49+
codebase: 'default',
50+
};
51+
const dynamicExtension: Extension = {
52+
id: dynId,
53+
params: [
54+
{
55+
param: DYN_PARAM,
56+
label: DYN_LABEL,
57+
value: DYN_VALUE,
58+
description: DYN_DESCRIPTION,
59+
},
60+
],
61+
labels: LABELS,
62+
} as Extension;
63+
4264
function setup(extension: Extension, id: string) {
4365
return render(
4466
<TestExtensionsProvider extensions={[extension]} instanceId={id}>
@@ -47,20 +69,36 @@ describe('ParamList', () => {
4769
);
4870
}
4971

50-
it('renders list of parameters', () => {
51-
const { getByText } = setup(extension, id);
72+
it('renders list of parameters for static extensions', () => {
73+
const { getByText } = setup(staticExtension, id);
5274

5375
expect(getByText(new RegExp(LABEL))).not.toBeNull();
5476
expect(getByText(new RegExp(VALUE))).not.toBeNull();
5577
});
5678

57-
it('displays param description on expansion in markdown', () => {
58-
const { getByText } = setup(extension, id);
79+
it('renders list of parameters for dynamic extensions', () => {
80+
const { getByText } = setup(dynamicExtension, dynId);
81+
82+
expect(getByText(new RegExp(DYN_LABEL))).not.toBeNull();
83+
expect(getByText(new RegExp(DYN_VALUE))).not.toBeNull();
84+
});
85+
86+
it('displays param description on expansion in markdown for static', () => {
87+
const { getByText } = setup(staticExtension, id);
5988

6089
act(() => {
6190
getByText(new RegExp(LABEL)).click();
6291
});
6392
expect(getByText(new RegExp(DESCRIPTION))).not.toBeNull();
6493
expect(getByText(new RegExp(DESCRIPTION))).not.toBeNull();
6594
});
95+
96+
it('displays param description on expansion in markdown for dynamic', () => {
97+
const { getByText } = setup(dynamicExtension, dynId);
98+
99+
act(() => {
100+
getByText(new RegExp(DYN_LABEL)).click();
101+
});
102+
expect(getByText(new RegExp(DYN_DESCRIPTION))).not.toBeNull();
103+
});
66104
});

src/components/Extensions/Details/DetailCard/Tabs/ParamList.tsx

+35-20
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,51 @@ import { Callout } from '../../../../common/Callout';
2121
import { DOCS_BASE } from '../../../../common/links/DocsLink';
2222
import { Markdown } from '../../../../common/Markdown';
2323
import { useExtension } from '../../../api/useExtension';
24+
import { isDynamicExtension } from '../../../api/useExtensions';
2425
import { EventsConfig } from './EventsConfig';
2526
import styles from './ParamList.module.scss';
2627
import { ParamValue } from './ParamValue';
2728

2829
function ParamList() {
2930
const extension = useExtension()!;
31+
const isDynamic = isDynamicExtension(extension);
32+
const docSuffix = isDynamic
33+
? 'extensions/manage-installed-extensions?interface=sdk#reconfigure'
34+
: 'extensions/manifest';
35+
36+
const dynamicFragment = (
37+
<Typography use="body2">
38+
Reconfigure is not available in the emulator. You can reconfigure
39+
parameters by updating them in the code.
40+
</Typography>
41+
);
42+
43+
const staticFragment = (
44+
<>
45+
<Typography use="body2">
46+
Reconfigure is not available in the emulator. You can reconfigure
47+
parameters by updating your .env files with:
48+
</Typography>
49+
<br />
50+
<code>firebase ext:configure {extension.id} --local</code>)
51+
</>
52+
);
53+
3054
return (
3155
<div>
3256
<div className={styles.paramHeader}>
3357
<Callout aside type="note">
34-
<Accordion
35-
title={
36-
<Typography use="body2">
37-
Reconfigure is not available in the emulator. You can
38-
reconfigure parameters by <b>updating your .env files</b> with:
39-
</Typography>
40-
}
41-
>
42-
<code>firebase ext:configure {extension.id} --local</code>
43-
<div className={styles.learnButton}>
44-
<a
45-
href={DOCS_BASE + 'extensions/manifest'}
46-
target="_blank"
47-
rel="noopener noreferrer"
48-
tabIndex={-1}
49-
>
50-
<Button>Learn more</Button>
51-
</a>
52-
</div>
53-
</Accordion>
58+
{isDynamic ? dynamicFragment : staticFragment}
59+
<div className={styles.learnButton}>
60+
<a
61+
href={DOCS_BASE + docSuffix}
62+
target="_blank"
63+
rel="noopener noreferrer"
64+
tabIndex={-1}
65+
>
66+
<Button>Learn more</Button>
67+
</a>
68+
</div>
5469
</Callout>
5570
</div>
5671
{(extension.params || []).map((param) => {

src/components/Extensions/List/ExtensionsCallout.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ export const ExtensionCallout: React.FC<
3030
actions={
3131
<div className={styles.link}>
3232
<DocsLink
33-
href="extensions/manifest"
33+
href="extensions/manage-installed-extensions"
3434
target="_blank"
3535
rel="noopener noreferrer"
3636
>
3737
<Typography theme="primary" use="body2" className={styles.link}>
38-
Learn how to manage your extensions manifest
38+
Learn how to manage your extensions
3939
</Typography>
4040
</DocsLink>
4141
</div>

src/components/Extensions/List/ExtensionsTable/ExtensionTableRow.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export const ExtensionsTableRow: React.FC<
6565
{extension.ref}
6666
</Typography>
6767
</div>
68+
<div>
69+
<Typography use="body2" theme="textSecondaryOnBackground">
70+
{extension.id}
71+
</Typography>
72+
</div>
6873
</DataTableCell>
6974
<DataTableCell className={`${styles.actionCell} actionCell`}>
7075
<Button

src/components/Extensions/api/internal/useExtensionBackends.test.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ describe('useExtensionBackends', () => {
4343
await waitFor(() => result.current !== null);
4444
await waitFor(() => delay(100));
4545

46-
expect(result.current).toEqual([BACKEND_LIST[0], BACKEND_LIST[1]]);
46+
expect(result.current).toEqual([
47+
BACKEND_LIST[0],
48+
BACKEND_LIST[1],
49+
BACKEND_LIST[2],
50+
]);
4751
});
4852
});

src/components/Extensions/api/internal/useExtensionsData.test.tsx

+62-2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('useExtensionsData', () => {
4444

4545
expect(result.current).toEqual([
4646
{
47+
id: 'pirojok-the-published-extension',
4748
authorName: 'Awesome Inc',
4849
authorUrl: 'https://google.com/awesome',
4950
params: [],
@@ -93,14 +94,15 @@ describe('useExtensionsData', () => {
9394
sourceUrl: '',
9495
extensionDetailsUrl:
9596
'https://firebase.google.com/products/extensions/good-tool',
96-
id: 'pirojok-the-published-extension',
97+
9798
ref: 'awesome-inc/[email protected]',
9899
iconUri:
99100
'https://www.gstatic.com/mobilesdk/211001_mobilesdk/google-pay-logo.svg',
100101
publisherIconUri:
101102
'https://www.gstatic.com/mobilesdk/160503_mobilesdk/logo/2x/firebase_128dp.png',
102103
},
103104
{
105+
id: 'pirojok-the-local-extension',
104106
authorName: 'Awesome Inc',
105107
authorUrl: 'https://google.com/awesome',
106108
params: [],
@@ -150,7 +152,65 @@ describe('useExtensionsData', () => {
150152
sourceUrl: '',
151153
extensionDetailsUrl:
152154
'https://firebase.google.com/products/extensions/good-tool',
153-
id: 'pirojok-the-local-extension',
155+
},
156+
{
157+
id: 'pirojok-the-dynamic-extension',
158+
authorName: 'Awesome Inc',
159+
authorUrl: 'https://google.com/awesome',
160+
params: [],
161+
name: 'good-tool',
162+
displayName: 'Pirojok-the-tool',
163+
specVersion: 'v1beta',
164+
env: {
165+
ALLOWED_EVENT_TYPES: 'google.firebase.v1.custom-event-occurred',
166+
EVENTARC_CHANNEL:
167+
'projects/test-project/locations/us-west1/channels/firebase',
168+
},
169+
allowedEventTypes: ['google.firebase.v1.custom-event-occurred'],
170+
eventarcChannel:
171+
'projects/test-project/locations/us-west1/channels/firebase',
172+
events: [
173+
{
174+
type: 'google.firebase.v1.custom-event-occurred',
175+
description: 'A custom event occurred',
176+
},
177+
],
178+
apis: [
179+
{
180+
apiName: 'storage-component.googleapis.com',
181+
reason: 'Needed to use Cloud Storage',
182+
},
183+
],
184+
resources: [
185+
{
186+
type: 'firebaseextensions.v1beta.function',
187+
description:
188+
'Listens for new images uploaded to your specified Cloud Storage bucket, resizes the images, then stores the resized images in the same bucket. Optionally keeps or deletes the original images.',
189+
name: 'generateResizedImage',
190+
propertiesYaml:
191+
// eslint-disable-next-line no-template-curly-in-string
192+
'availableMemoryMb: 1024\neventTrigger:\n eventType: google.storage.object.finalize\n resource: projects/_/buckets/${param:IMG_BUCKET}\nlocation: ${param:LOCATION}\nruntime: nodejs14\n',
193+
},
194+
],
195+
roles: [
196+
{
197+
role: 'storage.admin',
198+
reason:
199+
'Allows the extension to store resized images in Cloud Storage',
200+
},
201+
],
202+
readmeContent: '',
203+
postinstallContent: '### See it in action',
204+
sourceUrl: '',
205+
extensionDetailsUrl:
206+
'https://firebase.google.com/products/extensions/good-tool',
207+
208+
ref: 'awesome-inc/[email protected]',
209+
iconUri:
210+
'https://www.gstatic.com/mobilesdk/211001_mobilesdk/google-pay-logo.svg',
211+
publisherIconUri:
212+
'https://www.gstatic.com/mobilesdk/160503_mobilesdk/logo/2x/firebase_128dp.png',
213+
labels: { createdBy: 'SDK', codebase: 'default' },
154214
},
155215
]);
156216
});

src/components/Extensions/api/internal/useExtensionsData.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
* limitations under the License.
1515
*/
1616
import { Extension, ExtensionResource, Resource } from '../../models';
17-
import { ExtensionBackend, isLocalExtension } from '../useExtensions';
17+
import {
18+
ExtensionBackend,
19+
isDynamicExtension,
20+
isLocalExtension,
21+
} from '../useExtensions';
1822
import { useExtensionBackends } from './useExtensionBackends';
1923

2024
const EXTENSION_DETAILS_URL_BASE =
@@ -46,7 +50,8 @@ export function convertBackendToExtension(
4650
: r;
4751
};
4852

49-
const shared = {
53+
const shared: Omit<Extension, 'authorName'> = {
54+
id: backend.extensionInstanceId,
5055
authorUrl: spec.author?.url ?? '',
5156
params: spec.params.map((p) => {
5257
return {
@@ -70,17 +75,19 @@ export function convertBackendToExtension(
7075
extensionDetailsUrl: EXTENSION_DETAILS_URL_BASE + spec.name,
7176
};
7277

78+
if (isDynamicExtension(backend)) {
79+
shared.labels = backend.labels;
80+
}
81+
7382
if (isLocalExtension(backend)) {
7483
return {
7584
...shared,
7685
authorName: spec.author?.authorName ?? '',
77-
id: backend.extensionInstanceId,
7886
};
7987
}
8088

8189
return {
8290
...shared,
83-
id: backend.extensionInstanceId,
8491
ref: backend.extensionVersion.ref,
8592
authorName:
8693
spec.author?.authorName ??

src/components/Extensions/api/useExtensions.test.tsx

+16-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
import { renderHook } from '@testing-library/react';
1818

19-
import { EXTENSION } from '../testing/utils';
19+
import { DYNAMIC_EXTENSION, EXTENSION } from '../testing/utils';
2020
import { ExtensionsProvider, useExtensions } from './useExtensions';
2121

2222
describe('useExtensions', () => {
23-
it('returns the list of extension backends', () => {
23+
it('returns the list of static extension backends', () => {
2424
const wrapper: React.FC<React.PropsWithChildren<unknown>> = ({
2525
children,
2626
}) => (
@@ -33,4 +33,18 @@ describe('useExtensions', () => {
3333

3434
expect(result.current).toEqual([EXTENSION]);
3535
});
36+
37+
it('returns the list of dynamic extension backends', () => {
38+
const wrapper: React.FC<React.PropsWithChildren<unknown>> = ({
39+
children,
40+
}) => (
41+
<ExtensionsProvider extensions={[DYNAMIC_EXTENSION]}>
42+
{children}
43+
</ExtensionsProvider>
44+
);
45+
46+
const { result } = renderHook(() => useExtensions(), { wrapper });
47+
48+
expect(result.current).toEqual([DYNAMIC_EXTENSION]);
49+
});
3650
});

src/components/Extensions/api/useExtensions.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface CommonExtensionBackend {
2828
env: Record<string, string>;
2929
extensionInstanceId: string;
3030
functionTriggers: FunctionTrigger[];
31+
labels?: Record<string, string>;
3132
}
3233

3334
interface PublishedExtensionBackend extends CommonExtensionBackend {
@@ -72,3 +73,9 @@ export function isLocalExtension(
7273
): backend is LocalExtensionBackend {
7374
return backend.hasOwnProperty('extensionSpec');
7475
}
76+
77+
export function isDynamicExtension(
78+
extension: Extension | ExtensionBackend
79+
): boolean {
80+
return extension.labels?.createdBy === 'SDK';
81+
}

src/components/Extensions/models.ts

+3
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ export interface Extension {
162162
sourceUrl: string;
163163
postinstallContent: string;
164164
extensionDetailsUrl: string;
165+
name?: string;
166+
env?: Record<string, string>;
167+
labels?: Record<string, string>;
165168
}
166169

167170
export interface ExternalService {

0 commit comments

Comments
 (0)