Skip to content

Commit

Permalink
feat: implement sharing of document through netlify blobs (#1135)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shurtu-gal authored Sep 20, 2024
1 parent 9833c64 commit 1608159
Show file tree
Hide file tree
Showing 15 changed files with 348 additions and 126 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ build
dist
.turbo
.env
apps/design-system/src/styles/tailwind.output.css
apps/design-system/src/styles/tailwind.output.css
# Local Netlify folder for testing purposes
.netlify
6 changes: 6 additions & 0 deletions apps/studio-next/netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[devs]
functions = "apps/studio-next/src/netlify/functions"
targetPort = 3001

[build]
functions = "apps/studio-next/src/netlify/functions"
22 changes: 13 additions & 9 deletions apps/studio-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"cy:e2e:chrome": "cypress run --e2e --browser chrome",
"cy:e2e:chromium": "cypress run --e2e --browser chromium",
"cy:e2e:edge": "cypress run --e2e --browser edge",
Expand All @@ -22,29 +23,32 @@
"@asyncapi/protobuf-schema-parser": "^3.2.8",
"@asyncapi/react-component": "^1.2.2",
"@asyncapi/specs": "^6.5.4",
"@codemirror/view": "^6.26.3",
"@ebay/nice-modal-react": "^1.2.10",
"@headlessui/react": "^1.7.4",
"@hookstate/core": "^4.0.0-rc21",
"@monaco-editor/react": "^4.4.6",
"@tippyjs/react": "^4.2.6",
"js-base64": "^3.7.3",
"js-file-download": "^0.4.12",
"js-yaml": "^4.1.0",
"monaco-editor": "0.34.1",
"monaco-yaml": "4.0.2",
"react-hot-toast": "2.4.0",
"react-icons": "^4.6.0",
"reactflow": "^11.2.0",
"@netlify/blobs": "^8.0.1",
"@netlify/functions": "^2.8.1",
"@stoplight/yaml": "^4.3.0",
"@tippyjs/react": "^4.2.6",
"@types/node": "20.4.6",
"@types/react": "18.2.18",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.14",
"eslint-config-next": "13.4.12",
"js-base64": "^3.7.3",
"js-file-download": "^0.4.12",
"js-yaml": "^4.1.0",
"monaco-editor": "0.34.1",
"monaco-yaml": "4.0.2",
"next": "14.2.3",
"postcss": "8.4.31",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hot-toast": "2.4.0",
"react-icons": "^4.6.0",
"reactflow": "^11.2.0",
"tailwindcss": "3.3.3",
"tippy.js": "^6.3.7",
"typescript": "5.1.6",
Expand Down
45 changes: 45 additions & 0 deletions apps/studio-next/src/components/Editor/EditorDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ImportBase64Modal,
GeneratorModal,
ConvertModal,
ImportUUIDModal,
} from '../Modals';
import { Dropdown } from '../common';

Expand All @@ -34,6 +35,17 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
</button>
);

const importShareIdButton = (
<button
type="button"
className="px-4 py-1 w-full text-left text-sm rounded-md focus:outline-none transition ease-in-out duration-150"
title="Import from UUID"
onClick={() => show(ImportUUIDModal)}
>
Import from UUID
</button>
);

const importFileButton = (
<label
className="block px-4 py-1 w-full text-left text-sm rounded-md focus:outline-none transition ease-in-out duration-150 cursor-pointer"
Expand Down Expand Up @@ -208,6 +220,31 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
</button>
);

const shareButtonBase64 = (
<button
type="button"
className="px-4 py-1 w-full text-left text-sm rounded-md focus:outline-none transition ease-in-out duration-150 disabled:cursor-not-allowed"
title='Share as Base64'
onClick={() => {
toast.promise(
(async function () {
const base64 = await editorSvc.exportAsBase64();
const url = `${window.location.origin}/?base64=${encodeURIComponent(
base64
)}`;
await navigator.clipboard.writeText(url);
}()),
{
loading: 'Copying URL to clipboard...',
success: 'URL copied to clipboard!',
error: 'Failed to copy URL to clipboard.',
}
);
}}>
Share as Base64
</button>
);

return (
<Dropdown
opener={<FaEllipsisH />}
Expand All @@ -224,12 +261,20 @@ export const EditorDropdown: React.FunctionComponent<EditorDropdownProps> = () =
<li className="hover:bg-gray-900">
{importBase64Button}
</li>
<li className="hover:bg-gray-900">
{importShareIdButton}
</li>
</div>
<div className="border-b border-gray-700">
<li className="hover:bg-gray-900">
{generateButton}
</li>
</div>
<div className="border-b border-gray-700">
<li className="hover:bg-gray-900">
{shareButtonBase64}
</li>
</div>
<div className="border-b border-gray-700">
<li className="hover:bg-gray-900">
{saveFileButton}
Expand Down
2 changes: 2 additions & 0 deletions apps/studio-next/src/components/Editor/EditorSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const EditorSidebar: React.FunctionComponent<
documentFromText = 'From localStorage';
} else if (from === 'base64') {
documentFromText = 'From Base64';
} else if (from === 'share') {
documentFromText = 'From Shared';
} else {
documentFromText = `From URL ${source}`;
}
Expand Down
7 changes: 2 additions & 5 deletions apps/studio-next/src/components/Editor/ShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,13 @@ export const ShareButton: React.FunctionComponent<ShareButtonProps> = () => {
const handleShare = () => {
toast.promise(
(async function () {
const base64 = await editorSvc.exportAsBase64();
const url = `${window.location.origin}/?base64=${encodeURIComponent(
base64
)}`;
const url = await editorSvc.exportAsURL();
await navigator.clipboard.writeText(url);
}()),
{
loading: 'Copying URL to clipboard...',
success: 'URL copied to clipboard!',
error: 'Failed to copy URL to clipboard.',
error: 'Failed to share the AsyncAPI document.',
}
);
};
Expand Down
57 changes: 57 additions & 0 deletions apps/studio-next/src/components/Modals/ImportUUIDModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { create } from '@ebay/nice-modal-react';

import { ConfirmModal } from './index';

import { useServices } from '../../services';

export const ImportUUIDModal = create(() => {
const [shareID, setShareID] = useState('');
const { editorSvc } = useServices();

const onSubmit = () => {
toast.promise(editorSvc.importFromShareID(shareID), {
loading: 'Importing...',
success: (
<div>
<span className="block text-bold">
Document succesfully imported!
</span>
</div>
),
error: (
<div>
<span className="block text-bold text-red-400">
Failed to import document.
</span>
</div>
),
});
};

return (
<ConfirmModal
title="Import AsyncAPI document from Shared UUID"
confirmText="Import"
confirmDisabled={!shareID}
onSubmit={onSubmit}
>
<div className="flex content-center justify-center">
<label
htmlFor="url"
className="flex justify-right items-center content-center text-sm font-medium text-gray-700 hidden"
>
Shared UUID
</label>
<input
type="url"
name="url"
placeholder="Paste UUID here"
className="shadow-sm focus:ring-pink-500 focus:border-pink-500 block w-full sm:text-sm border-gray-300 rounded-md py-2 px-3 text-gray-700 border-pink-300 border-2"
onChange={e => setShareID(e.target.value)}
/>
</div>
</ConfirmModal>
);
});
1 change: 1 addition & 0 deletions apps/studio-next/src/components/Modals/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './ConvertModal';
export * from './ConvertToLatestModal';
export * from './ImportBase64Modal';
export * from './ImportURLModal';
export * from './ImportUUIDModal';
export * from './NewFileModal';
export * from './RedirectedModal';
export * from './ConfirmNewFileModal';
27 changes: 27 additions & 0 deletions apps/studio-next/src/netlify/functions/share-retreive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getStore } from '@netlify/blobs';
import type { Config, Context } from '@netlify/functions';

export default async (req: Request, context: Context) => {
const share = getStore('share');
const { shareId } = context.params;

if (!shareId) {
return new Response('Not found', { status: 404 });
}

const shareData = await share.get(shareId);

if (!shareData) {
return new Response('Not found', { status: 404 });
}

return new Response(shareData, {
headers: {
'content-type': 'application/json',
},
});
}

export const config: Config = {
path: '/share/:shareId',
};
26 changes: 26 additions & 0 deletions apps/studio-next/src/netlify/functions/share-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getStore } from '@netlify/blobs';
import type { Config, Context } from '@netlify/functions';
import { randomUUID } from 'crypto';

export default async (req: Request, context: Context) => {
const share = getStore('share');
const shareId = randomUUID();

const state = await req.json();

await share.set(shareId, JSON.stringify({
URL: `${context.site.url }?share=${ shareId}`,
...state,
created: Date.now(),
}))

return new Response(shareId, {
headers: {
'content-type': 'text/plain',
},
});
};

export const config: Config = {
path: '/share',
}
30 changes: 21 additions & 9 deletions apps/studio-next/src/services/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ export class ApplicationService extends AbstractService {
// subscribe to state to hide preloader
this.hidePreloader();

const { readOnly, url, base64 } =
const { readOnly, url, base64, share } =
this.svcs.navigationSvc.getUrlParameters();
// readOnly state should be only set to true when someone pass also url or base64 parameter
const isStrictReadonly = Boolean(readOnly && (url || base64));
// readOnly state should be only set to true when someone pass also url or base64 or share parameter
const isStrictReadonly = Boolean(readOnly && (url || base64 || share));

let error: any;
try {
await this.fetchResource(url, base64);
await this.fetchResource(url, base64, share);
} catch (err) {
error = err;
console.error(err);
Expand All @@ -37,18 +37,18 @@ export class ApplicationService extends AbstractService {
}

public async afterAppInit() {
const { readOnly, url, base64, redirectedFrom } =
const { readOnly, url, base64, share, redirectedFrom } =
this.svcs.navigationSvc.getUrlParameters();
const isStrictReadonly = Boolean(readOnly && (url || base64));
const isStrictReadonly = Boolean(readOnly && (url || base64 || share));

// show RedirectedModal modal if the redirectedFrom is set (only when readOnly state is set to false)
if (!isStrictReadonly && redirectedFrom) {
show(RedirectedModal);
}
}

private async fetchResource(url: string | null, base64: string | null) {
if (!url && !base64) {
private async fetchResource(url: string | null, base64: string | null, share: string | null) {
if (!url && !base64 && !share) {
return;
}

Expand All @@ -58,15 +58,27 @@ export class ApplicationService extends AbstractService {
content = await fetch(url).then((res) => res.text());
} else if (base64) {
content = this.svcs.formatSvc.decodeBase64(base64);
} else if (share) {
const response = await fetch(`/share/${share}`);
const data = await response.json();
content = data.content;
}

const language = this.svcs.formatSvc.retrieveLangauge(content);
const source = url || undefined;
let from = 'url';

if (base64) {
from = 'base64';
} else if (share) {
from = 'share';
}

updateFile('asyncapi', {
content,
language,
source,
from: url ? 'url' : 'base64',
from: from as 'url' | 'base64' | 'share',
});
await this.svcs.parserSvc.parse('asyncapi', content, { source });
}
Expand Down
Loading

0 comments on commit 1608159

Please sign in to comment.