Skip to content

Commit 9fccd56

Browse files
committed
add pickers for rule creation with control API and CLI examples
1 parent 64f0618 commit 9fccd56

23 files changed

Lines changed: 1040 additions & 181 deletions

File tree

src/components/Layout/MDXWrapper.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Tiles } from './mdx/tiles';
2222
import { PageHeader } from './mdx/PageHeader';
2323
import Admonition from './mdx/Admonition';
2424
import { MethodSignature } from './mdx/MethodSignature';
25+
import { Tabs, Tab } from './mdx/Tabs';
2526

2627
import { Frontmatter, PageContextType } from './Layout';
2728
import { ActivePage } from './utils/nav';
@@ -339,6 +340,8 @@ const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, pageContext, location
339340
td: Table.Cell,
340341
Tiles,
341342
MethodSignature,
343+
Tabs,
344+
Tab,
342345
}}
343346
>
344347
<PageHeader title={title} intro={intro} />
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React from 'react';
2+
import { render, screen, fireEvent } from '@testing-library/react';
3+
import { Tabs, Tab } from './Tabs';
4+
5+
describe('Tabs', () => {
6+
it('renders tab buttons with author-defined labels', () => {
7+
render(
8+
<Tabs>
9+
<Tab value="a" label="Alpha">
10+
Content A
11+
</Tab>
12+
<Tab value="b" label="Beta">
13+
Content B
14+
</Tab>
15+
</Tabs>,
16+
);
17+
18+
expect(screen.getByRole('tab', { name: 'Alpha' })).toBeInTheDocument();
19+
expect(screen.getByRole('tab', { name: 'Beta' })).toBeInTheDocument();
20+
});
21+
22+
it('shows the first tab content by default', () => {
23+
render(
24+
<Tabs>
25+
<Tab value="a" label="Alpha">
26+
Content A
27+
</Tab>
28+
<Tab value="b" label="Beta">
29+
Content B
30+
</Tab>
31+
</Tabs>,
32+
);
33+
34+
expect(screen.getByText('Content A')).toBeInTheDocument();
35+
expect(screen.queryByText('Content B')).not.toBeInTheDocument();
36+
});
37+
38+
it('switches content when a tab is clicked', () => {
39+
render(
40+
<Tabs>
41+
<Tab value="a" label="Alpha">
42+
Content A
43+
</Tab>
44+
<Tab value="b" label="Beta">
45+
Content B
46+
</Tab>
47+
</Tabs>,
48+
);
49+
50+
fireEvent.click(screen.getByRole('tab', { name: 'Beta' }));
51+
52+
expect(screen.queryByText('Content A')).not.toBeInTheDocument();
53+
expect(screen.getByText('Content B')).toBeInTheDocument();
54+
});
55+
56+
it('sets aria-selected correctly', () => {
57+
render(
58+
<Tabs>
59+
<Tab value="a" label="Alpha">
60+
Content A
61+
</Tab>
62+
<Tab value="b" label="Beta">
63+
Content B
64+
</Tab>
65+
</Tabs>,
66+
);
67+
68+
expect(screen.getByRole('tab', { name: 'Alpha' })).toHaveAttribute('aria-selected', 'true');
69+
expect(screen.getByRole('tab', { name: 'Beta' })).toHaveAttribute('aria-selected', 'false');
70+
71+
fireEvent.click(screen.getByRole('tab', { name: 'Beta' }));
72+
73+
expect(screen.getByRole('tab', { name: 'Alpha' })).toHaveAttribute('aria-selected', 'false');
74+
expect(screen.getByRole('tab', { name: 'Beta' })).toHaveAttribute('aria-selected', 'true');
75+
});
76+
77+
it('renders a tabpanel for the active tab', () => {
78+
render(
79+
<Tabs>
80+
<Tab value="a" label="Alpha">
81+
Content A
82+
</Tab>
83+
<Tab value="b" label="Beta">
84+
Content B
85+
</Tab>
86+
</Tabs>,
87+
);
88+
89+
expect(screen.getByRole('tabpanel')).toHaveTextContent('Content A');
90+
});
91+
92+
it('renders nothing for Tab used outside of Tabs', () => {
93+
const { container } = render(
94+
<Tab value="a" label="Alpha">
95+
Orphan
96+
</Tab>,
97+
);
98+
expect(container).toBeEmptyDOMElement();
99+
});
100+
});

src/components/Layout/mdx/Tabs.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { useState, createContext, useContext, isValidElement, ReactNode, useId } from 'react';
2+
import cn from '@ably/ui/core/utils/cn';
3+
4+
type TabsContextType = {
5+
activeTab: string;
6+
tabsId: string;
7+
};
8+
9+
const TabsContext = createContext<TabsContextType | undefined>(undefined);
10+
11+
interface TabProps {
12+
value: string;
13+
label: string;
14+
children: ReactNode;
15+
}
16+
17+
export const Tab: React.FC<TabProps> = ({ value, children }) => {
18+
const context = useContext(TabsContext);
19+
if (!context) {
20+
return null;
21+
}
22+
return context.activeTab === value ? (
23+
<div role="tabpanel" id={`${context.tabsId}-panel-${value}`} aria-labelledby={`${context.tabsId}-tab-${value}`}>
24+
{children}
25+
</div>
26+
) : null;
27+
};
28+
29+
interface TabsProps {
30+
children: ReactNode;
31+
}
32+
33+
export const Tabs: React.FC<TabsProps> = ({ children }) => {
34+
const tabsId = useId();
35+
36+
const tabs: { value: string; label: string }[] = [];
37+
React.Children.forEach(children, (child) => {
38+
if (isValidElement<TabProps>(child) && child.props.value) {
39+
tabs.push({ value: child.props.value, label: child.props.label ?? child.props.value });
40+
}
41+
});
42+
43+
const [activeTab, setActiveTab] = useState(tabs[0]?.value ?? '');
44+
45+
return (
46+
<TabsContext.Provider value={{ activeTab, tabsId }}>
47+
<div className="my-5 border border-neutral-300 dark:border-neutral-800 rounded-lg overflow-hidden">
48+
<div
49+
className="flex gap-1 border-b border-neutral-300 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-1100 px-2 pt-2"
50+
role="tablist"
51+
>
52+
{tabs.map(({ value, label }) => (
53+
<button
54+
key={value}
55+
id={`${tabsId}-tab-${value}`}
56+
role="tab"
57+
aria-selected={activeTab === value}
58+
aria-controls={`${tabsId}-panel-${value}`}
59+
onClick={() => setActiveTab(value)}
60+
className={cn(
61+
'px-4 py-2 text-sm font-medium rounded-t-md transition-colors cursor-pointer',
62+
activeTab === value
63+
? 'bg-white dark:bg-neutral-1300 text-neutral-1300 dark:text-neutral-000 border border-neutral-300 dark:border-neutral-800 border-b-white dark:border-b-neutral-1300 -mb-px'
64+
: 'text-neutral-700 dark:text-neutral-500 hover:text-neutral-1000 dark:hover:text-neutral-300',
65+
)}
66+
>
67+
{label}
68+
</button>
69+
))}
70+
</div>
71+
<div className="p-5">{children}</div>
72+
</div>
73+
</TabsContext.Provider>
74+
);
75+
};

src/pages/docs/channels/index.mdx

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,56 @@ The rules related to enabling features are:
202202
| Message conflation | If enabled, messages are aggregated over a set period of time and evaluated against a conflation key. All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. [Message conflation](/docs/messages#conflation) reduces costs in high-throughput scenarios by removing redundant and outdated messages. |
203203
| Message annotations, updates, deletes, and appends | If enabled, allows message "annotations":/docs/messages/annotations to be used, as well as updates, deletes, and appends to be published to messages. Note that these features are currently in public preview. When this feature is enabled, messages will be "persisted":/docs/storage-history/storage#all-message-persistence (necessary in order from them later be annotated or updated), and "continuous history":/docs/storage-history/history#continuous-history features will not work.
204204

205-
To set a rule in the Ably dashboard:
206-
207-
1. Sign in to your Ably account.
208-
2. Select an app.
209-
3. Go to **Settings** tab.
210-
4. Click **Add new rule**.
211-
5. Select channel name or namespace to apply rules to.
212-
6. Check required rules.
205+
To set a rule:
206+
207+
<Tabs>
208+
<Tab value="dashboard" label="Dashboard">
209+
210+
In your app [settings](https://ably.com/accounts/any/apps/any/settings):
211+
212+
1. Click **Add new rule**.
213+
2. Select the channel name or namespace to apply rules to.
214+
3. Check the required rules.
215+
4. Click **Create rule** to save.
216+
217+
</Tab>
218+
<Tab value="control-api" label="Control API">
219+
220+
Create a rule using the Control API by sending a `POST` request to [`/apps/{app_id}/namespaces`](/docs/api/control-api):
221+
222+
<Code>
223+
```shell
224+
curl -X POST https://control.ably.net/v1/apps/{APP_ID}/namespaces \
225+
-H "Authorization: Bearer {ACCESS_TOKEN}" \
226+
-H "Content-Type: application/json" \
227+
-d '{
228+
"id": "my-namespace",
229+
"persisted": true,
230+
"persistLast": false,
231+
"pushEnabled": false,
232+
"tlsOnly": false,
233+
"authenticated": false
234+
}'
235+
```
236+
</Code>
237+
238+
</Tab>
239+
<Tab value="cli" label="Ably CLI">
240+
241+
Use the [Ably CLI](/docs/platform/tools/cli) to create a rule:
242+
243+
<Code>
244+
```shell
245+
ably apps rules create \
246+
--name "my-namespace" \
247+
--persisted
248+
```
249+
</Code>
250+
251+
Run `ably apps rules create --help` for a full list of available options.
252+
253+
</Tab>
254+
</Tabs>
213255

214256
## Channel history <a id="history"/>
215257

src/pages/docs/livesync/mongodb/index.mdx

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,42 @@ When a change event is received over the Change Streams API it is published to a
2424

2525
You first need to create an integration rule in order to sync your MongoDB instance with Ably.
2626

27-
There are two ways to create a MongoDB database connector rule:
27+
<Tabs>
28+
<Tab value="dashboard" label="Dashboard">
2829

29-
1. Using the [Ably Dashboard](https://ably.com/dashboard).
30-
2. Using the [Control API](/docs/platform/account/control-api).
30+
On the [Integrations](https://ably.com/accounts/any/apps/any/integrations) page of your app:
3131

32-
To create a rule in your [dashboard](https://ably.com/dashboard):
32+
1. Click **New Integration Rule**.
33+
2. Choose **MongoDB**.
34+
3. Configure the [rule settings](#config).
3335

34-
1. Log in and select the application you wish to use.
35-
2. Click the *Integrations* tab.
36-
3. Click the *New Integration Rule* button.
37-
4. Choose *MongoDB* from the list.
36+
</Tab>
37+
<Tab value="control-api" label="Control API">
38+
39+
Create a MongoDB rule using the Control API by sending a `POST` request to [`/apps/{app_id}/rules`](/docs/api/control-api):
40+
41+
<Code>
42+
```shell
43+
curl -X POST https://control.ably.net/v1/apps/{APP_ID}/rules \
44+
-H "Authorization: Bearer {ACCESS_TOKEN}" \
45+
-H "Content-Type: application/json" \
46+
-d '{
47+
"ruleType": "ingress/mongodb",
48+
"target": {
49+
"url": "mongodb://user:pass@myhost.com",
50+
"database": "my-database",
51+
"collection": "my-collection",
52+
"pipeline": "[{\"$set\": {\"_ablyChannel\": \"myDocuments\"}}]",
53+
"fullDocument": "off",
54+
"fullDocumentBeforeChange": "off",
55+
"primarySite": "us-east-1-A"
56+
}
57+
}'
58+
```
59+
</Code>
60+
61+
</Tab>
62+
</Tabs>
3863

3964
### Rule configuration <a id="config"/>
4065

@@ -51,8 +76,6 @@ Use the following fields to configure your MongoDB integration rule:
5176
| Pipeline | A MongoDB pipeline to pass to the Change Stream API. This field allows you to control which types of change events are published, and which channel the change event should be published to. The [pipeline](#pipeline) *must set the `_ablyChannel` field* on the root of the change event. It must also be a valid JSON array of [pipeline operations](https://www.mongodb.com/docs/v8.0/changeStreams/#modify-change-stream-output). |
5277
| Primary site | The primary site that the connector will run in. You should choose a site that is close to your database. |
5378
| Provisioned capacity | The provisioned capacity of the connector. It is always set to 1. |
54-
55-
5679
## Subscribe to change events <a id="subscribe"/>
5780

5881
Use the [Ably Pub/Sub SDKs](/docs/sdks) to subscribe to changes published by the MongoDB database connector.
@@ -132,8 +155,6 @@ You must provide a MongoDB pipeline when configuring the integration rule. The p
132155
</Code>
133156

134157
The pipeline also lets you filter and modify the change events published, as well as edit their structure.
135-
136-
137158
The following is an example of a pipeline that only matches certain operation types before sending the change events to the `myDocuments` channel:
138159

139160
<Code>

src/pages/docs/livesync/postgres/index.mdx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,42 @@ By writing change events to the outbox table within the same database transactio
3434

3535
### Creating the rule <a id="create"/>
3636

37-
There are two ways to create a Postgres integration rule:
38-
1. Using the [Ably Dashboard](https://ably.com/dashboard).
39-
2. Using the [Control API](/docs/platform/account/control-api).
40-
41-
To create a rule in your [dashboard](https://ably.com/dashboard):
42-
1. Login and select the application you wish to use.
43-
2. Click the *Integrations* tab.
44-
3. Click the *New Integration Rule* button.
45-
4. Choose *Postgres* from the list.
37+
<Tabs>
38+
<Tab value="dashboard" label="Dashboard">
39+
40+
On the [Integrations](https://ably.com/accounts/any/apps/any/integrations) page of your app:
41+
42+
1. Click **New Integration Rule**.
43+
2. Choose **Postgres**.
44+
3. Configure the [rule settings](#configure).
45+
46+
</Tab>
47+
<Tab value="control-api" label="Control API">
48+
49+
Create a Postgres rule using the Control API by sending a `POST` request to [`/apps/{app_id}/rules`](/docs/api/control-api):
50+
51+
<Code>
52+
```shell
53+
curl -X POST https://control.ably.net/v1/apps/{APP_ID}/rules \
54+
-H "Authorization: Bearer {ACCESS_TOKEN}" \
55+
-H "Content-Type: application/json" \
56+
-d '{
57+
"ruleType": "ingress-postgres-outbox",
58+
"target": {
59+
"url": "postgres://user:password@example.com:5432/your-database-name",
60+
"outboxTableSchema": "public",
61+
"outboxTableName": "outbox",
62+
"nodesTableSchema": "public",
63+
"nodesTableName": "nodes",
64+
"sslMode": "prefer",
65+
"primarySite": "us-east-1-A"
66+
}
67+
}'
68+
```
69+
</Code>
70+
71+
</Tab>
72+
</Tabs>
4673

4774
### Rule configuration <a id="configure"/>
4875

0 commit comments

Comments
 (0)