diff --git a/backend/btrixcloud/colls.py b/backend/btrixcloud/colls.py index 2cf8de71..39f8cf6f 100644 --- a/backend/btrixcloud/colls.py +++ b/backend/btrixcloud/colls.py @@ -7,7 +7,7 @@ import uuid from typing import Optional, List import pymongo -from fastapi import Depends, HTTPException +from fastapi import Depends, HTTPException, Response from fastapi.responses import StreamingResponse from .basecrawls import SUCCESSFUL_STATES @@ -148,10 +148,14 @@ class CollectionOps: return await self.get_collection(coll_id, org) async def get_collection( - self, coll_id: uuid.UUID, org: Organization, resources=False + self, coll_id: uuid.UUID, org: Organization, resources=False, public_only=False ): """Get collection by id""" - result = await self.collections.find_one({"_id": coll_id}) + query = {"_id": coll_id} + if public_only: + query["isPublic"] = True + + result = await self.collections.find_one(query) if resources: result["resources"] = await self.get_collection_crawl_resources( coll_id, org @@ -348,6 +352,7 @@ def init_collections_api(app, mdb, crawls, orgs, crawl_manager): org_crawl_dep = orgs.org_crawl_dep org_viewer_dep = orgs.org_viewer_dep + org_public = orgs.org_public @app.post("/orgs/{oid}/collections", tags=["collections"]) async def add_collection( @@ -428,8 +433,36 @@ def init_collections_api(app, mdb, crawls, orgs, crawl_manager): coll = await colls.get_collection(coll_id, org, resources=True) if not coll: raise HTTPException(status_code=404, detail="collection_not_found") + return coll + @app.get( + "/orgs/{oid}/collections/{coll_id}/public/replay.json", tags=["collections"] + ) + async def get_collection_public_replay( + response: Response, + coll_id: uuid.UUID, + org: Organization = Depends(org_public), + ): + coll = await colls.get_collection( + coll_id, org, resources=True, public_only=True + ) + if not coll: + raise HTTPException(status_code=404, detail="collection_not_found") + + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Headers"] = "*" + return coll + + @app.options( + "/orgs/{oid}/collections/{coll_id}/public/replay.json", tags=["collections"] + ) + async def get_replay_preflight(response: Response): + response.headers["Access-Control-Allow-Methods"] = "GET, HEAD, OPTIONS" + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Headers"] = "*" + return {} + @app.patch("/orgs/{oid}/collections/{coll_id}", tags=["collections"]) async def update_collection( coll_id: uuid.UUID, diff --git a/backend/btrixcloud/models.py b/backend/btrixcloud/models.py index c6f07eda..69b005eb 100644 --- a/backend/btrixcloud/models.py +++ b/backend/btrixcloud/models.py @@ -465,6 +465,8 @@ class Collection(BaseMongoModel): # Sorted by count, descending tags: Optional[List[str]] = [] + isPublic: Optional[bool] = False + # ============================================================================ class CollIn(BaseModel): @@ -474,6 +476,8 @@ class CollIn(BaseModel): description: Optional[str] crawlIds: Optional[List[str]] = [] + isPublic: Optional[bool] = False + # ============================================================================ class CollOut(Collection): @@ -488,6 +492,7 @@ class UpdateColl(BaseModel): name: Optional[str] description: Optional[str] + isPublic: Optional[bool] # ============================================================================ diff --git a/backend/btrixcloud/orgs.py b/backend/btrixcloud/orgs.py index fa48c7d9..c6f89af1 100644 --- a/backend/btrixcloud/orgs.py +++ b/backend/btrixcloud/orgs.py @@ -45,6 +45,7 @@ class OrgOps: self.org_viewer_dep = None self.org_crawl_dep = None self.org_owner_dep = None + self.org_public = None self.invites = invites @@ -300,6 +301,13 @@ def init_orgs_api(app, mdb, user_manager, invites, user_dep: User): return org + async def org_public(oid: str): + org = await ops.get_org_by_id(uuid.UUID(oid)) + if not org: + raise HTTPException(status_code=404, detail="org_not_found") + + return org + router = APIRouter( prefix="/orgs/{oid}", dependencies=[Depends(org_dep)], @@ -310,6 +318,7 @@ def init_orgs_api(app, mdb, user_manager, invites, user_dep: User): ops.org_viewer_dep = org_dep ops.org_crawl_dep = org_crawl_dep ops.org_owner_dep = org_owner_dep + ops.org_public = org_public @app.get("/orgs", tags=["organizations"], response_model=PaginatedResponse) async def get_orgs( diff --git a/backend/btrixcloud/users.py b/backend/btrixcloud/users.py index 9cd1b744..6154ba5c 100644 --- a/backend/btrixcloud/users.py +++ b/backend/btrixcloud/users.py @@ -402,7 +402,6 @@ def init_users_api(app, user_manager): } for org in user_orgs ] - print(f"user info with orgs: {user_info}", flush=True) return user_info @users_router.get("/invite/{token}", tags=["invites"]) diff --git a/frontend/src/pages/org/collection-detail.ts b/frontend/src/pages/org/collection-detail.ts index dd7347f1..f259f7ba 100644 --- a/frontend/src/pages/org/collection-detail.ts +++ b/frontend/src/pages/org/collection-detail.ts @@ -51,6 +51,9 @@ export class CollectionDetail extends LiteElement { @state() private isDescriptionExpanded = false; + @state() + private showEmbedInfo = false; + // Use to cancel requests private getArchivedItemsController: AbortController | null = null; @@ -89,12 +92,32 @@ export class CollectionDetail extends LiteElement { render() { return html`${this.renderHeader()}
-

- ${this.collection?.name || - html``} -

+
+ ${this.collection?.isPublic + ? html` + + + + ` + : html` + + + + `} +

+ ${this.collection?.name || + html``} +

+
+ ${when( + this.collection?.isPublic, + () => html` + (this.showEmbedInfo = true)}> + + View Embed Code + + ` + )} ${when(this.isCrawler, this.renderActions)}
${this.renderInfoBar()}
@@ -135,9 +158,85 @@ export class CollectionDetail extends LiteElement { >Delete Collection - `; + + ${this.renderShareInfo()}`; } + private getPublicReplayURL() { + return new URL( + `/api/orgs/${this.orgId}/collections/${this.collectionId}/public/replay.json`, + window.location.href + ).href; + } + + private renderShareInfo = () => { + if (!this.collection?.isPublic) { + return; + } + + const embedCode = ``; + const importCode = `importScripts("https://replayweb.page/sw.js");`; + + return html` (this.showEmbedInfo = false)} + > +
+

+ ${msg( + html`Embed this collection in another site using these + ReplayWeb.page code snippets.` + )} +

+

+ ${msg(html`Add the following embed code to your HTML page:`)} +

+
+
${embedCode}
+
+ embedCode} + content=${msg("Copy Embed Code")} + > +
+
+

+ ${msg( + html`Add the following JavaScript to + ./replay/sw.js:` + )} +

+
+
${importCode}
+
+ importCode} + content=${msg("Copy JS")} + > +
+
+

+ ${msg( + html`See + + our embedding guide + for more details.` + )} +

+
+
`; + }; + private renderHeader = () => html`