Skip to content

Commit

Permalink
Projects page Github badges (#5094)
Browse files Browse the repository at this point in the history
* add column

* update github repo

* when updating settings, update the db repo

* test update

* test handler

* github limits

* remove copypasta

* test helper

* fix assert

* bff check
  • Loading branch information
ruggi authored Mar 22, 2024
1 parent 69b05c2 commit e83d8e7
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 25 deletions.
23 changes: 19 additions & 4 deletions editor/src/components/editor/actions/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ import {
saveAsset as saveAssetToServer,
saveUserConfiguration,
updateAssetFileName,
updateGithubRepository,
} from '../server'
import type {
CanvasBase64Blobs,
Expand All @@ -366,6 +367,7 @@ import type {
TrueUpTarget,
TrueUpHuggingElement,
CollaborativeEditingSupport,
ProjectGithubSettings,
} from '../store/editor-state'
import {
trueUpChildrenOfGroupChanged,
Expand Down Expand Up @@ -3612,12 +3614,25 @@ export const UPDATE_FNS = {
}
},
UPDATE_GITHUB_SETTINGS: (action: UpdateGithubSettings, editor: EditorModel): EditorModel => {
const newGithubSettings: ProjectGithubSettings = {
...editor.githubSettings,
...action.settings,
}
if (editor.id != null) {
void updateGithubRepository(
editor.id,
newGithubSettings.targetRepository == null
? null
: {
owner: newGithubSettings.targetRepository.owner,
repository: newGithubSettings.targetRepository.repository,
branch: newGithubSettings.branchName,
},
)
}
return normalizeGithubData({
...editor,
githubSettings: {
...editor.githubSettings,
...action.settings,
},
githubSettings: newGithubSettings,
})
},
UPDATE_GITHUB_DATA: (action: UpdateGithubData, editor: EditorModel): EditorModel => {
Expand Down
27 changes: 26 additions & 1 deletion editor/src/components/editor/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
thumbnailURL,
userConfigURL,
} from '../../common/server'
import type { PersistentModel, UserConfiguration, UserPermissions } from './store/editor-state'
import type {
GithubRepo,
PersistentModel,
UserConfiguration,
UserPermissions,
} from './store/editor-state'
import { emptyUserConfiguration, emptyUserPermissions } from './store/editor-state'
import type { LoginState } from '../../uuiui-deps'
import urljoin from 'url-join'
Expand Down Expand Up @@ -609,3 +614,23 @@ export async function requestProjectAccess(projectId: string): Promise<void> {
throw new Error(`Request project access failed (${response.status}): ${response.statusText}`)
}
}

export async function updateGithubRepository(
projectId: string,
githubRepository: (GithubRepo & { branch: string | null }) | null,
): Promise<void> {
if (!isBackendBFF()) {
return
}
const url = urljoin(`/internal/projects/${projectId}/github/repository/update`)
const response = await fetch(url, {
method: 'POST',
credentials: 'include',
headers: HEADERS,
mode: MODE,
body: JSON.stringify({ githubRepository: githubRepository }),
})
if (!response.ok) {
throw new Error(`Update Github repository failed (${response.status}): ${response.statusText}`)
}
}
3 changes: 3 additions & 0 deletions server/migrations/010.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE project
ADD COLUMN github_repository text;

1 change: 1 addition & 0 deletions server/src/Utopia/Web/Database/Migrations.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ migrateDatabase verbose includeInitial pool = withResource pool $ \connection ->
, MigrationFile "007.sql" "./migrations/007.sql"
, MigrationFile "008.sql" "./migrations/008.sql"
, MigrationFile "009.sql" "./migrations/009.sql"
, MigrationFile "010.sql" "./migrations/010.sql"
]
let initialMigrationCommand = if includeInitial
then [MigrationFile "initial.sql" "./migrations/initial.sql"]
Expand Down
107 changes: 106 additions & 1 deletion utopia-remix/app/models/project.server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ import {
renameProject,
restoreDeletedProject,
softDeleteProject,
updateGithubRepository,
} from './project.server'
import { AccessLevel, AccessRequestStatus } from '../types'
import {
AccessLevel,
MaxGithubBranchNameLength,
MaxGithubOwnerLength,
MaxGithubRepositoryLength,
AccessRequestStatus,
} from '../types'

describe('project model', () => {
afterEach(async () => {
Expand Down Expand Up @@ -419,4 +426,102 @@ describe('project model', () => {
expect(got.collaborators['four'].map((c) => c.id)[1]).toBe('carol')
})
})

describe('updateGithubRepository', () => {
beforeEach(async () => {
await createTestUser(prisma, { id: 'bob' })
await createTestUser(prisma, { id: 'alice' })
await createTestProject(prisma, { id: 'one', ownerId: 'bob' })
await prisma.project.update({
where: { proj_id: 'one' },
data: { github_repository: 'something' },
})
})

it('errors if the project is not found', async () => {
const fn = async () =>
updateGithubRepository({ projectId: 'unknown', userId: 'bob', repository: null })
await expect(fn).rejects.toThrow('not found')
})

it('errors if the user does not own the project', async () => {
const fn = async () =>
updateGithubRepository({ projectId: 'one', userId: 'alice', repository: null })
await expect(fn).rejects.toThrow('not found')
})

it('updates the repository string (null)', async () => {
await updateGithubRepository({ projectId: 'one', userId: 'bob', repository: null })
const project = await prisma.project.findUnique({
where: { proj_id: 'one' },
select: { github_repository: true },
})
if (project == null) {
throw new Error('expected project not to be null')
}
expect(project.github_repository).toBe(null)
})

it('updates the repository string (without branch)', async () => {
await updateGithubRepository({
projectId: 'one',
userId: 'bob',
repository: { owner: 'foo', repository: 'bar', branch: null },
})
const project = await prisma.project.findUnique({
where: { proj_id: 'one' },
select: { github_repository: true },
})
if (project == null) {
throw new Error('expected project not to be null')
}
expect(project.github_repository).toBe('foo/bar')
})

it('updates the repository string (with branch)', async () => {
await updateGithubRepository({
projectId: 'one',
userId: 'bob',
repository: { owner: 'foo', repository: 'bar', branch: 'baz' },
})
const project = await prisma.project.findUnique({
where: { proj_id: 'one' },
select: { github_repository: true },
})
if (project == null) {
throw new Error('expected project not to be null')
}
expect(project.github_repository).toBe('foo/bar:baz')
})

it('updates the repository string (with branch), trimming to max lengths', async () => {
const repo = {
owner:
'foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo',
repository:
'barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr',
branch:
'bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz',
}
await updateGithubRepository({
projectId: 'one',
userId: 'bob',
repository: repo,
})
const project = await prisma.project.findUnique({
where: { proj_id: 'one' },
select: { github_repository: true },
})
if (project == null) {
throw new Error('expected project not to be null')
}
expect(project.github_repository).toBe(
repo.owner.slice(0, MaxGithubOwnerLength) +
'/' +
repo.repository.slice(0, MaxGithubRepositoryLength) +
':' +
repo.branch.slice(0, MaxGithubBranchNameLength),
)
})
})
})
21 changes: 20 additions & 1 deletion utopia-remix/app/models/project.server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { prisma } from '../db.server'
import type { CollaboratorsByProject, ProjectListing } from '../types'
import type { CollaboratorsByProject, GithubRepository, ProjectListing } from '../types'
import {
AccessLevel,
AccessRequestStatus,
asAccessLevel,
userToCollaborator,
githubRepositoryStringOrNull,
type ProjectWithoutContentFromDB,
} from '../types'
import { ensure } from '../util/api.server'
Expand All @@ -19,6 +20,7 @@ const selectProjectWithoutContent: Record<keyof ProjectWithoutContentFromDB, tru
modified_at: true,
deleted: true,
ProjectAccess: true,
github_repository: true,
}

export async function listProjects(params: { ownerId: string }): Promise<ProjectListing[]> {
Expand Down Expand Up @@ -195,3 +197,20 @@ export async function listSharedWithMeProjectsAndCollaborators(params: {
collaborators: collaboratorsByProject,
}
}

export async function updateGithubRepository(params: {
projectId: string
userId: string
repository: GithubRepository | null
}) {
return prisma.project.update({
where: {
owner_id: params.userId,
proj_id: params.projectId,
},
data: {
github_repository: githubRepositoryStringOrNull(params.repository),
modified_at: new Date(),
},
})
}
4 changes: 2 additions & 2 deletions utopia-remix/app/models/projectCollaborators.server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('projectCollaborators model', () => {
it('returns the collaborator details by project id', async () => {
const ids = ['one', 'two', 'four', 'five']
const got = await getCollaborators({ ids: ids, userId: 'bob' })
expect(Object.keys(got)).toEqual(ids)
expect(Object.keys(got).length).toEqual(4)
expect(got['one'].map((c) => c.id)).toEqual(['bob'])
expect(got['two'].map((c) => c.id)).toEqual(['bob', 'wendy'])
expect(got['four'].map((c) => c.id)).toEqual([])
Expand All @@ -62,7 +62,7 @@ describe('projectCollaborators model', () => {
it('ignores mismatching projects', async () => {
const ids = ['one', 'two', 'three']
const got = await getCollaborators({ ids: ids, userId: 'bob' })
expect(Object.keys(got)).toEqual(['one', 'two'])
expect(Object.keys(got).length).toEqual(2)
expect(got['one'].map((c) => c.id)).toEqual(['bob'])
expect(got['two'].map((c) => c.id)).toEqual(['bob', 'wendy'])
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { prisma } from '../db.server'
import { handleUpdateGithubRepository } from '../routes/internal.projects.$id.github.repository.update'
import {
createTestProject,
createTestSession,
createTestUser,
newTestRequest,
truncateTables,
} from '../test-util'
import { ApiError } from '../util/errors'

describe('handleUpdateGithubRepository', () => {
afterEach(async () => {
await truncateTables([
prisma.userDetails,
prisma.persistentSession,
prisma.project,
prisma.projectID,
])
})

beforeEach(async () => {
await createTestUser(prisma, { id: 'bob' })
await createTestUser(prisma, { id: 'alice' })
await createTestSession(prisma, { key: 'the-key', userId: 'bob' })
await createTestProject(prisma, { id: 'one', ownerId: 'bob' })
await createTestProject(prisma, { id: 'two', ownerId: 'alice' })
})

it('requires a user', async () => {
const fn = async () =>
handleUpdateGithubRepository(newTestRequest({ method: 'POST', authCookie: 'wrong-key' }), {})
await expect(fn).rejects.toThrow(ApiError)
await expect(fn).rejects.toThrow('session not found')
})
it('requires a valid id', async () => {
const fn = async () =>
handleUpdateGithubRepository(newTestRequest({ method: 'POST', authCookie: 'the-key' }), {})
await expect(fn).rejects.toThrow(ApiError)
await expect(fn).rejects.toThrow('id is null')
})
it('requires a valid request body', async () => {
const fn = async () => {
const req = newTestRequest({
method: 'POST',
authCookie: 'the-key',
body: JSON.stringify({}),
})
return handleUpdateGithubRepository(req, { id: 'one' })
}

await expect(fn).rejects.toThrow('invalid request')
})
it('requires a valid project', async () => {
const fn = async () => {
const req = newTestRequest({
method: 'POST',
authCookie: 'the-key',
body: JSON.stringify({ githubRepository: null }),
})
return handleUpdateGithubRepository(req, { id: 'doesnt-exist' })
}

await expect(fn).rejects.toThrow('Record to update not found')
})
it('requires ownership of the project', async () => {
const fn = async () => {
const req = newTestRequest({
method: 'POST',
authCookie: 'the-key',
body: JSON.stringify({ githubRepository: null }),
})
return handleUpdateGithubRepository(req, { id: 'two' })
}

await expect(fn).rejects.toThrow('Record to update not found')
})
it('updates the github repository', async () => {
const fn = async () => {
const req = newTestRequest({
method: 'POST',
authCookie: 'the-key',
body: JSON.stringify({
githubRepository: { owner: 'foo', repository: 'bar', branch: 'baz' },
}),
})
return handleUpdateGithubRepository(req, { id: 'one' })
}

await fn()
const got = await prisma.project.findUnique({
where: { proj_id: 'one' },
select: { github_repository: true },
})
expect(got?.github_repository).toEqual('foo/bar:baz')
})
})
Loading

0 comments on commit e83d8e7

Please sign in to comment.