Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collaborative projects #5033

Merged
merged 63 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
9f6490f
initial commit
liady Mar 12, 2024
184deda
fix order
liady Mar 12, 2024
b1df39e
Merge branch 'master' into refactor/local-fga
liady Mar 12, 2024
63f2b06
fix lint issue
liady Mar 12, 2024
af1540b
fix CR
liady Mar 13, 2024
0c62f8c
fix CR
liady Mar 13, 2024
efcfe7a
fix
liady Mar 13, 2024
7b086d4
Merge branch 'master' into refactor/local-fga
liady Mar 13, 2024
ee07423
log FGA client mode
liady Mar 13, 2024
63a5956
add more relations
liady Mar 13, 2024
4aa647c
refactor
liady Mar 13, 2024
5cae347
return 403 when unauthorized
liady Mar 13, 2024
9003463
Merge branch 'master' into feat/collaborative-project
liady Mar 13, 2024
cf76657
fix eslint
liady Mar 13, 2024
7cffb48
grant role
liady Mar 13, 2024
df05c66
fix eslint
liady Mar 13, 2024
af1626f
Update utopia-remix/app/handlers/validators.ts
liady Mar 13, 2024
1621d51
use info
liady Mar 13, 2024
31260b6
remove filter Boolean
liady Mar 13, 2024
555ea30
add requests table
ruggi Mar 13, 2024
0d68f59
add uuid
ruggi Mar 13, 2024
8493636
create/update access requests
ruggi Mar 13, 2024
fc2961d
routes
ruggi Mar 13, 2024
8fbecc9
add status type
ruggi Mar 13, 2024
cf6b9f4
test db operations
ruggi Mar 13, 2024
29d77fd
update schema
ruggi Mar 13, 2024
a9edbfd
comment
ruggi Mar 13, 2024
bd90c9d
Merge branch 'master' into feat/collaborative-project
liady Mar 13, 2024
f9f894b
Merge branch 'master' into feat/project-access-request
liady Mar 13, 2024
1ede489
Merge branch 'feat/project-access-request' into feat/collaborative-pr…
liady Mar 13, 2024
65c4eba
access approval flow
liady Mar 14, 2024
028ba57
remove unneeded function
liady Mar 14, 2024
212600f
add popup
liady Mar 14, 2024
d490497
Merge branch 'master' into feat/collaborative-project
ruggi Mar 14, 2024
aab7c8a
fix updateAccessRequestStatus
ruggi Mar 14, 2024
e5e5570
popup
liady Mar 14, 2024
5f41a70
Merge branch 'master' into feat/collaborative-project
liady Mar 14, 2024
fdec37b
Merge branch 'feat/collaborative-project' into feat/share-popup
liady Mar 14, 2024
bac91c8
use 403
ruggi Mar 14, 2024
21631a8
fix components
ruggi Mar 14, 2024
7944af7
popup
liady Mar 14, 2024
1474a6f
do not actually throw on NaN array access (#5041)
balazsbajorics Mar 14, 2024
955d4a1
sub-cartouches (#5040)
balazsbajorics Mar 14, 2024
f142273
Merge branch 'master' into feat/collaborative-project
liady Mar 14, 2024
d074e34
add a dot for pending
liady Mar 14, 2024
b218be1
Merge branch 'master' into feat/collaborative-project
liady Mar 14, 2024
b6cda6e
Merge branch 'feat/collaborative-project' into feat/share-popup
liady Mar 14, 2024
c3ee65a
lazy load requests
ruggi Mar 14, 2024
412c444
fix changeProjectUserRole
ruggi Mar 14, 2024
881750f
Merge branch 'feat/collaborative-project' into feat/share-popup
liady Mar 14, 2024
cd01f24
remove uneeded code
liady Mar 14, 2024
66068da
fix cr
liady Mar 14, 2024
5805f6c
remove arrow functions
liady Mar 14, 2024
84a0129
remove unneeded null
liady Mar 14, 2024
173e438
Merge branch 'feat/collaborative-project' into feat/share-popup
liady Mar 14, 2024
fb2a8ad
include 'rejected' status
liady Mar 14, 2024
b09abb7
fix click handler
liady Mar 14, 2024
6db73a5
Merge branch 'feat/collaborative-project' into feat/share-popup
liady Mar 14, 2024
0f09e2e
return all requests, because needed by the ui
ruggi Mar 14, 2024
110d8cc
Merge branch 'feat/collaborative-project' into feat/share-popup
liady Mar 14, 2024
5809125
Update utopia-remix/app/components/SharePopup.tsx
liady Mar 14, 2024
68e9a4c
fix when
liady Mar 14, 2024
ad784ad
sort requests
ruggi Mar 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ function loadFailedEvent(): LoadFailedEvent {
}
}

interface LoadFailedNotAuthorizedEvent {
type: 'LOAD_FAILED_NOT_AUTHORIZED'
projectId: string
}

function loadFailedNotAuthorizedEvent(projectId: string): LoadFailedNotAuthorizedEvent {
return {
type: 'LOAD_FAILED_NOT_AUTHORIZED',
projectId: projectId,
}
}

export interface SaveEvent<ModelType> {
type: 'SAVE'
projectModel: ProjectModel<ModelType>
Expand Down Expand Up @@ -180,6 +192,7 @@ type CoreEvent<ModelType, FileType> =
| LoadEvent
| LoadCompleteEvent<ModelType>
| LoadFailedEvent
| LoadFailedNotAuthorizedEvent
| CheckOwnershipCompleteEvent
| SaveEvent<ModelType>
| SaveCompleteEvent<ModelType, FileType>
Expand Down Expand Up @@ -525,6 +538,8 @@ export function createPersistenceMachine<ModelType, FileType>(
send((_, event: DoneInvokeEvent<ProjectLoadResult<ModelType>>) => {
if (event.data.type === 'PROJECT_LOAD_SUCCESS') {
return loadCompleteEvent(event.data.projectId, event.data.projectModel)
} else if (event.data.type === 'PROJECT_NOT_AUTHORIZED') {
return loadFailedNotAuthorizedEvent(event.data.projectId)
} else {
return loadFailedEvent()
}
Expand Down Expand Up @@ -703,6 +718,20 @@ export function createPersistenceMachine<ModelType, FileType>(
}
}),
},
LOAD_FAILED_NOT_AUTHORIZED: {
target: Empty,
actions: assign((_context, _event) => {
return {
projectId: undefined,
project: undefined,
queuedSave: undefined,
projectOwnership: {
ownerId: null,
isOwner: false,
},
}
}),
},
},
},
[Saving]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@ export interface ProjectLoadSuccess<ModelType> extends ProjectModelWithId<ModelT
type: 'PROJECT_LOAD_SUCCESS'
}

export interface ProjectNotFount {
export interface ProjectNotFound {
type: 'PROJECT_NOT_FOUND'
}

export type ProjectLoadResult<ModelType> = ProjectLoadSuccess<ModelType> | ProjectNotFount
export interface ProjectNotAuthorized {
type: 'PROJECT_NOT_AUTHORIZED'
projectId: string
}

export type ProjectLoadResult<ModelType> =
| ProjectLoadSuccess<ModelType>
| ProjectNotFound
| ProjectNotAuthorized

export interface FileWithFileName<FileType> {
fileName: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ async function loadProject(projectId: string): Promise<ProjectLoadResult<Persist
type: 'PROJECT_NOT_FOUND',
}

case 'ProjectNotAuthorized':
return {
type: 'PROJECT_NOT_AUTHORIZED',
projectId: projectId,
}

default:
throw new Error(`Invalid project load response: ${JSON.stringify(serverProject)}`)
}
Expand Down
3 changes: 3 additions & 0 deletions editor/src/components/editor/persistence/persistence.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ function setupTest(saveThrottle: number = 0) {
updatedFiles: {} as { [fileName: string]: AssetFile },
dispatchedActions: [] as Array<EditorAction>,
projectNotFound: false,
projectNotAuthorized: false,
createdOrLoadedProject: undefined as PersistentModel | undefined,
latestContext: {
projectOwnership: { ownerId: null, isOwner: false },
Expand All @@ -192,6 +193,7 @@ function setupTest(saveThrottle: number = 0) {
})
}
const onProjectNotFound = () => (capturedData.projectNotFound = true)
const onProjectNotAuthorized = () => (capturedData.projectNotFound = true)
const onCreatedOrLoadedProject = (
_projectId: string,
_projectName: string,
Expand All @@ -206,6 +208,7 @@ function setupTest(saveThrottle: number = 0) {
PersistenceBackend,
testDispatch,
onProjectNotFound,
onProjectNotAuthorized,
onCreatedOrLoadedProject,
onContextChange,
saveThrottle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export const DummyPersistenceMachine: PersistenceMachine = new PersistenceMachin
NO_OP,
NO_OP,
NO_OP,
NO_OP,
)
4 changes: 4 additions & 0 deletions editor/src/components/editor/persistence/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export class PersistenceMachine {
backendAPI: PersistenceBackendAPI<PersistentModel, ProjectFile>,
dispatch: EditorDispatch,
onProjectNotFound: () => void,
onProjectNotAuthorized: (projectId: string) => void,
onCreatedOrLoadedProject: (
projectId: string,
projectName: string,
Expand Down Expand Up @@ -85,6 +86,9 @@ export class PersistenceMachine {
case 'LOAD_FAILED':
onProjectNotFound()
break
case 'LOAD_FAILED_NOT_AUTHORIZED':
onProjectNotAuthorized(event.projectId)
break
case 'DOWNLOAD_ASSETS_COMPLETE': {
if (state.matches({ core: { [Forking]: CreatingProjectId } })) {
this.queuedActions.push(setForkedFromProjectID(state.context.projectId!))
Expand Down
26 changes: 25 additions & 1 deletion editor/src/components/editor/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@ interface ProjectNotFound {
type: 'ProjectNotFound'
}

export type LoadProjectResponse = ProjectLoaded | ProjectUnchanged | ProjectNotFound
interface ProjectNotAuthorized {
type: 'ProjectNotAuthorized'
}

export type LoadProjectResponse =
| ProjectLoaded
| ProjectUnchanged
| ProjectNotFound
| ProjectNotAuthorized

interface SaveAssetResponse {
id: string
Expand Down Expand Up @@ -176,6 +184,8 @@ export async function loadProject(
return response.json()
} else if (response.status === 404) {
return { type: 'ProjectNotFound' }
} else if (response.status === 403) {
return { type: 'ProjectNotAuthorized' }
} else {
// FIXME Client should show an error if server requests fail
throw new Error(`server responded with ${response.status} ${response.statusText}`)
Expand Down Expand Up @@ -548,3 +558,17 @@ async function getCollaboratorsFromLiveblocks(projectId: string): Promise<Collab
}
return Object.values(collabs.toObject()).map((u) => u.toObject())
}

export async function requestProjectAccess(projectId: string): Promise<void> {
if (!isBackendBFF()) {
return
}
const response = await fetch(`/internal/projects/${projectId}/access/request`, {
method: 'POST',
credentials: 'include',
mode: MODE,
})
if (!response.ok) {
throw new Error(`Request project access failed (${response.status}): ${response.statusText}`)
}
}
9 changes: 9 additions & 0 deletions editor/src/templates/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export class Editor {
PersistenceBackend,
this.boundDispatch,
renderProjectNotFound,
renderProjectNotAuthorized,
onCreatedOrLoadedProject,
),
builtInDependencies: builtInDependencies,
Expand Down Expand Up @@ -826,3 +827,11 @@ async function renderProjectNotFound(): Promise<void> {
root.render(<ProjectNotFound />)
}
}

async function renderProjectNotAuthorized(projectId: string): Promise<void> {
const rootElement = document.getElementById(EditorID)
if (rootElement != null) {
const root = createRoot(rootElement)
root.render(<ProjectNotFound projectId={projectId} />)
}
}
87 changes: 61 additions & 26 deletions editor/src/templates/project-not-found/ProjectNotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ import React from 'react'
import { jsx } from '@emotion/react'
import { Button } from '../../uuiui/button'
import { UTOPIA_BACKEND_BASE_URL } from '../../common/env-vars'
import { when } from '../../utils/react-conditionals'
import { requestProjectAccess } from '../../components/editor/server'

const PyramidLight404 = `${process.env.UTOPIA_DOMAIN}/editor/404_pyramid_light.png?hash=${process.env.UTOPIA_SHA}`

export default function ProjectNotFound() {
export default function ProjectNotFound({ projectId }: { projectId?: string }) {
const [accessRequested, setAccessRequested] = React.useState(false)
const requestAccess = React.useCallback(async () => {
if (projectId != null) {
void requestProjectAccess(projectId)
setAccessRequested(true)
}
}, [projectId])
return (
<div
style={{
Expand Down Expand Up @@ -45,33 +54,59 @@ export default function ProjectNotFound() {
<div style={{ fontSize: '22px', width: '430px', textAlign: 'center', lineHeight: '40px' }}>
Either this project does not exist, or you do not have access to it.
</div>
<a
href={`${UTOPIA_BACKEND_BASE_URL}projects`}
rel='noopener noreferrer'
style={{ textDecoration: 'none' }}
>
<Button
css={{
boxSizing: 'border-box',
height: 'auto',
fontSize: '18px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
backgroundColor: '#0075f9',
borderRadius: '90px',
padding: '10px 30px',
transition: 'background-color 0.3s',
'&:hover': {
backgroundColor: '#5DA9FF',
},
}}
<div style={{ display: 'flex', gap: '20px' }}>
<a
href={`${UTOPIA_BACKEND_BASE_URL}projects`}
rel='noopener noreferrer'
style={{ textDecoration: 'none' }}
>
Return Home
</Button>
</a>
<ActionButton text='Return Home' />
</a>
{when(
projectId != null,
<ActionButton
text={accessRequested ? 'Access Requested' : 'Request Access'}
onClick={requestAccess}
disabled={accessRequested}
/>,
)}
</div>
</div>
</div>
)
}

function ActionButton({
text,
onClick,
disabled,
}: {
text: string
onClick?: () => void
disabled?: boolean
}) {
return (
<Button
disabled={disabled}
onClick={onClick}
css={{
boxSizing: 'border-box',
height: 'auto',
fontSize: '18px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
backgroundColor: '#0075f9',
borderRadius: '90px',
padding: '10px 30px',
transition: 'background-color 0.3s',
'&:hover': {
backgroundColor: '#5DA9FF',
},
}}
>
{text}
</Button>
)
}
12 changes: 6 additions & 6 deletions utopia-remix/__mocks__/@openfga/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ export function OpenFgaClient() {

export const CredentialsMethod = {}
export const ErrorCode = {}
export type ClientRequestOptsWithAuthZModelId = {}
export type ClientWriteRequest = {}
export type ClientWriteRequestOpts = {}
export type ClientWriteResponse = {}
export type FgaApiValidationError = {}
export type ClientConfiguration = {}
export type ClientRequestOptsWithAuthZModelId = ''
export type ClientWriteRequest = ''
export type ClientWriteRequestOpts = ''
export type ClientWriteResponse = ''
export type FgaApiValidationError = ''
export type ClientConfiguration = ''
Loading
Loading