Skip to content

Commit d20da24

Browse files
authored
export tunnel tool's client section to be our quick try static web page (#863)
* Update tunnel client section to be able to clientonly and export to website page running publish:manual * Remove no need changes * update the wording for hint section * Fix CI failures
1 parent 51a366c commit d20da24

File tree

55 files changed

+4639
-1842
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+4639
-1842
lines changed

tools/awps-tunnel/client/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,18 @@
7676
"typescript": "^5.1.6"
7777
},
7878
"overrides": {
79-
"autoprefixer": "10.4.5"
79+
"autoprefixer": "10.4.5",
80+
"typescript": "5.1.6"
8081
},
8182
"scripts": {
8283
"clean": "rimraf ./build",
8384
"start": "cross-env REACT_APP_DATA_FETCHER=mock REACT_APP_API_VERSION=2023-07-01 react-scripts start",
85+
"start:manual": "cross-env REACT_APP_DATA_FETCHER=manual REACT_APP_API_VERSION=2023-07-01 react-scripts start",
8486
"prebuild": "node scripts/downloadExamples.js",
8587
"build": "npm run build:mock",
8688
"build:npm": "npm run prebuild && cross-env REACT_APP_DATA_FETCHER=npm REACT_APP_API_VERSION=2023-07-01 react-scripts build",
8789
"build:mock": "npm run prebuild && cross-env REACT_APP_DATA_FETCHER=mock REACT_APP_API_VERSION=2023-07-01 react-scripts build",
90+
"build:manual": "npm run prebuild && cross-env REACT_APP_DATA_FETCHER=manual REACT_APP_API_VERSION=2023-07-01 react-scripts build",
8891
"test": "cross-env CI=true REACT_APP_DATA_FETCHER=mock REACT_APP_API_VERSION=2023-07-01 react-scripts test",
8992
"eject": "react-scripts eject",
9093
"lint": "eslint ./src/"

tools/awps-tunnel/client/scripts/downloadExamples.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,40 @@ const ref = 'main';
2323
const examplesPath = `specification/webpubsub/data-plane/WebPubSub/stable/${version}/examples`;
2424
const apiFilePath = `specification/webpubsub/data-plane/WebPubSub/stable/${version}/webpubsub.json`;
2525

26+
const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_PAT;
27+
const axiosClient = axios.create({
28+
headers: {
29+
'User-Agent': 'awps-tunnel-download-examples',
30+
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
31+
},
32+
});
33+
2634
async function downloadFiles() {
2735
try {
2836
const examplesUrl = `https://api.github.com/repos/${repoPath}/contents/${examplesPath}?ref=${ref}`;
29-
const examplesResponse = await axios.get(examplesUrl);
37+
const examplesResponse = await axiosClient.get(examplesUrl);
3038
const files = examplesResponse.data;
31-
39+
3240
for (const file of files) {
3341
if (file.type === 'file') {
34-
const fileResponse = await axios.get(file.download_url, { responseType: 'arraybuffer' });
42+
const fileResponse = await axiosClient.get(file.download_url, { responseType: 'arraybuffer' });
3543
fs.writeFileSync(path.join(examplesDir, file.name), fileResponse.data);
3644
}
3745
}
38-
console.log("All example files downloaded successfully.")
39-
46+
console.log("All example files downloaded successfully.");
47+
4048
const apiUrl = `https://api.github.com/repos/${repoPath}/contents/${apiFilePath}?ref=${ref}`;
41-
const apiResponse = await axios.get(apiUrl, { responseType: 'json' });
42-
const apiFileResponse = await axios.get(apiResponse.data.download_url, { responseType: 'arraybuffer' });
49+
const apiResponse = await axiosClient.get(apiUrl, { responseType: 'json' });
50+
const apiFileResponse = await axiosClient.get(apiResponse.data.download_url, { responseType: 'arraybuffer' });
4351
fs.writeFileSync(path.join(apiDir, apiResponse.data.name), apiFileResponse.data);
4452
console.log('api spec downloaded successfully.');
4553
} catch (error) {
46-
console.error('Error downloading files:', error);
54+
if (error?.response?.status === 403 && error?.response?.data?.message?.includes('API rate limit exceeded')) {
55+
console.error('Error downloading files: GitHub API rate limit exceeded. Provide GITHUB_TOKEN to increase the limit.');
56+
} else {
57+
console.error('Error downloading files:', error);
58+
}
59+
process.exit(1);
4760
}
4861
}
4962

tools/awps-tunnel/client/src/components/Dashboard.tsx

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
22
import { RequestHistory } from "../panels/RequestHistory";
33
import "./Dashboard.css";
44
import { Panel, PanelType } from "@fluentui/react";
5-
import { Tab, TabList, Accordion, AccordionHeader, AccordionItem, AccordionPanel, ToggleButton, CounterBadge } from "@fluentui/react-components";
5+
import { Tab, TabList, Accordion, AccordionHeader, AccordionItem, AccordionPanel, ToggleButton, CounterBadge, Button } from "@fluentui/react-components";
66
import * as svg from "./icons";
77
import { DocumentOnePageMultiple24Regular, Link24Regular } from "@fluentui/react-icons";
88
import type { SelectTabData, SelectTabEvent } from "@fluentui/react-components";
@@ -46,6 +46,8 @@ export const Dashboard = () => {
4646
const [showPanel, setShowPanel] = useState(false);
4747
const [clientConnectionStatus, setClientConnectionStatus] = useState(ConnectionStatus.Disconnected);
4848
const { data, dataFetcher } = useDataContext();
49+
const isManual = dataFetcher.kind === "manual";
50+
const [isFullscreen, setIsFullscreen] = useState<boolean>(isManual);
4951
const [tunnelUnread, setTunnelUnread] = useState(0);
5052
const onStartUpstream = async (start: boolean) => {
5153
return start ? await dataFetcher.invoke("startEmbeddedUpstream") : await dataFetcher.invoke("stopEmbeddedUpstream");
@@ -127,13 +129,29 @@ export const Dashboard = () => {
127129
// read current tab from local storage
128130
const [selectedValue, setSelectedValue] = React.useState<string>(loadCurrentTab());
129131

132+
useEffect(() => {
133+
if (isManual && selectedValue !== "client") {
134+
setSelectedValue("client");
135+
}
136+
}, [isManual, selectedValue]);
137+
138+
useEffect(() => {
139+
const previousTitle = document.title;
140+
document.title = isManual ? "Azure Web PubSub Quick Try" : "Azure Web PubSub Local Development Dashboard";
141+
return () => {
142+
document.title = previousTitle;
143+
};
144+
}, [isManual]);
145+
130146
useEffect(() => {
131147
setCurrentTab(selectedValue);
132148
}, [selectedValue]);
133149

150+
const visibleWorkflows = isManual ? workflows.filter((w) => w.key === "client") : workflows;
151+
134152
const workflow = () => (
135153
<div className="workflow d-flex flex-row justify-content-center align-items-center m-2">
136-
{workflows.map((w, i) => (
154+
{visibleWorkflows.map((w, i) => (
137155
<React.Fragment key={i}>
138156
<WorkflowStep checked={selectedValue === w.key} unread={w.unread} onClick={() => setSelectedValue(w.key)} icon={w.icon(true)} text={w.title} />
139157
{w.status && <Connector status={w.status} />}
@@ -147,17 +165,21 @@ export const Dashboard = () => {
147165
setSelectedValue(data.value as string);
148166
};
149167

168+
const tabList = (
169+
<TabList size="large" className="m-2" selectedValue={selectedValue} onTabSelect={onTabSelect} vertical={!isFullscreen}>
170+
{visibleWorkflows.map((w, i) => (
171+
<React.Fragment key={i}>
172+
<Tab id={w.key} icon={<span>{w.icon()}</span>} value={w.key}>
173+
{w.title} {w.unread > 0 && <CounterBadge size="small" count={w.unread}></CounterBadge>}
174+
</Tab>
175+
</React.Fragment>
176+
))}
177+
</TabList>
178+
);
179+
150180
const tabSidebar = (
151181
<>
152-
<TabList size="large" className="m-2" selectedValue={selectedValue} onTabSelect={onTabSelect} vertical>
153-
{workflows.map((w, i) => (
154-
<React.Fragment key={i}>
155-
<Tab id={w.key} icon={<span>{w.icon()}</span>} value={w.key}>
156-
{w.title} {w.unread > 0 && <CounterBadge size="small" count={w.unread}></CounterBadge>}
157-
</Tab>
158-
</React.Fragment>
159-
))}
160-
</TabList>
182+
{tabList}
161183
<Accordion collapsible>
162184
<AccordionItem value="1">
163185
<AccordionHeader>Help center</AccordionHeader>
@@ -189,29 +211,51 @@ export const Dashboard = () => {
189211
);
190212
const connectPane = (
191213
<>
192-
{workflows.map((w, i) => (
214+
{visibleWorkflows.map((w, i) => (
193215
// Use hidden to prevent re-rendering
194216
<div key={i} hidden={selectedValue !== w.key} className="d-flex flex-column flex-fill overflow-auto">
195-
<Accordion className="" collapsible defaultOpenItems={"1"}>
196-
<AccordionItem value="1">
197-
<AccordionHeader size="large">{w.title}</AccordionHeader>
198-
<AccordionPanel>{paneOverview(w)}</AccordionPanel>
199-
</AccordionItem>
200-
</Accordion>
201-
<hr />
217+
{!isManual && (
218+
<>
219+
<Accordion className="" collapsible defaultOpenItems={"1"}>
220+
<AccordionItem value="1">
221+
<AccordionHeader size="large">{w.title}</AccordionHeader>
222+
<AccordionPanel>{paneOverview(w)}</AccordionPanel>
223+
</AccordionItem>
224+
</Accordion>
225+
<hr />
226+
</>
227+
)}
202228
{w.content}
203229
</div>
204230
))}
205231
</>
206232
);
207233

234+
const fullscreenNav = visibleWorkflows.length > 1 ? (
235+
<div className="d-flex flex-row align-items-center justify-content-start mx-2">{tabList}</div>
236+
) : null;
237+
208238
return (
209239
<div className="d-flex flex-column flex-fill overflow-auto">
210240
<Panel type={PanelType.medium} className="logPanel" isLightDismiss isOpen={showPanel} onDismiss={() => setShowPanel(false)} closeButtonAriaLabel="Close" headerText="Logs">
211241
<textarea className="flex-fill" disabled value={data.logs.map((log) => `${log.time.toISOString()} [${LogLevel[log.level]}] ${log.message}`).join("\n")} />
212242
</Panel>
213-
{workflow()}
214-
<ResizablePanel className="flex-fill" left={tabSidebar} right={connectPane} initialLeftWidth="200px"></ResizablePanel>
243+
{!isManual && (
244+
<div className="d-flex flex-row justify-content-end align-items-center mx-2 my-1">
245+
<Button size="small" appearance={isFullscreen ? "secondary" : "primary"} onClick={() => setIsFullscreen((f) => !f)}>
246+
{isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
247+
</Button>
248+
</div>
249+
)}
250+
{!isFullscreen && workflow()}
251+
{isFullscreen ? (
252+
<div className="d-flex flex-column flex-fill overflow-auto">
253+
{fullscreenNav}
254+
{connectPane}
255+
</div>
256+
) : (
257+
<ResizablePanel className="flex-fill" left={tabSidebar} right={connectPane} initialLeftWidth="200px"></ResizablePanel>
258+
)}
215259
</div>
216260
);
217261
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { MessageBar, MessageBarBody } from "@fluentui/react-components";
2+
3+
export const HintSection = () => (
4+
<div className="d-flex flex-column mt-1 flex-fill">
5+
<MessageBar intent="info">
6+
<MessageBarBody>Get a Client Access URL from Azure portal then use it with the client above.</MessageBarBody>
7+
</MessageBar>
8+
<div className="flex-fill">
9+
<div className="d-flex flex-column websocket-client-container m-2 flex-fill gap-2">
10+
<p className="mb-0">
11+
<b>Hints</b>
12+
</p>
13+
<p className="mb-0">
14+
In portal, open <b>Keys</b> and use <b>Client URL Generator</b> to produce a Client Access URL for quick testing. In real apps, please follow{" "}
15+
<a target="_blank" rel="noreferrer" href="https://aka.ms/awps/sdks">
16+
{" "}
17+
the SDK documents
18+
</a>
19+
.
20+
</p>
21+
</div>
22+
<div className="d-flex justify-content-center m-2">
23+
<img
24+
alt="Portal client URL generator"
25+
style={{ width: "100%", maxWidth: "900px", border: "1px solid #ddd", borderRadius: "4px" }}
26+
src="https://azure.github.io/azure-webpubsub/event-listener/webpubsub-client/images/portal_client_url.png"
27+
/>
28+
</div>
29+
<ul>
30+
<li>
31+
<strong>Connection</strong>: connection stands for a WebSocket client connection.
32+
</li>{" "}
33+
<li>
34+
<strong>Hub</strong>: hub is the logical isolation for connections. Connections always connect to a hub, connections can only send to those within the same hub.
35+
</li>{" "}
36+
<li>
37+
<strong>User ID</strong>: A connection can belong to a user when it is auth-ed.
38+
</li>{" "}
39+
<li>
40+
<strong>Token Lifetime</strong>: Specifies the lifetime of this client URL’s token. When the token expires, you get 401 Unauthorized when connecting to the service with this URL.
41+
</li>{" "}
42+
<li>
43+
<strong>Roles</strong>: Specifies the roles for the connection. It can be used when the connection connects with{" "}
44+
<code className="language-plaintext highlighter-rouge">json.webpubsub.azure.v1</code>{" "}
45+
subprotocol that empowers the client to join/leave/send to groups.
46+
</li>
47+
</ul>
48+
</div>
49+
</div>
50+
);

tools/awps-tunnel/client/src/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import App from './App';
66
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
77
import reportWebVitals from './reportWebVitals';
88
import { initializeIcons } from '@fluentui/react/lib/Icons';
9-
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
9+
const baseHref = document.getElementsByTagName('base')[0].getAttribute('href') || '/';
10+
const baseUrl = new URL(baseHref, window.location.href).pathname.replace(/\/+$/, '') || '/';
11+
const routerBase = baseUrl === '' ? '/' : baseUrl;
1012
const rootElement = document.getElementById('root');
1113
const root = createRoot(rootElement);
1214
initializeIcons(/* optional base url */);
1315
root.render(
14-
<BrowserRouter basename={baseUrl}>
16+
<BrowserRouter basename={routerBase}>
1517
<App />
1618
</BrowserRouter>
1719
);

tools/awps-tunnel/client/src/panels/ClientPanel.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { ConnectionStatus } from "../models";
22
import { Playground } from "./Playground";
3+
import { useDataContext } from "../providers/DataContext";
34
export interface ClientPanelProps {
45
onStatusChange: (status: ConnectionStatus) => void;
56
}
67
export const ClientPanel = ({ onStatusChange }: ClientPanelProps) => {
8+
const { dataFetcher } = useDataContext();
9+
const isManual = dataFetcher.kind === "manual";
10+
const title = isManual ? "Quick Try" : "Client";
711
return (
812
<div className="d-flex flex-column mx-4 flex-fill overflow-auto">
9-
<h5>Client</h5>
13+
<h5>{title}</h5>
1014
<p>
1115
Connect <b>your own client</b> following{" "}
1216
<a target="_blank" rel="noreferrer" href="https://aka.ms/awps/sdks">

0 commit comments

Comments
 (0)