Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions apps/app-frontend/src/pages/hosting/manage/Access.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup lang="ts">
import { ServersManageAccessPage } from '@modrinth/ui'
</script>

<template>
<ServersManageAccessPage />
</template>
3 changes: 2 additions & 1 deletion apps/app-frontend/src/pages/hosting/manage/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Access from './Access.vue'
import Backups from './Backups.vue'
import Content from './Content.vue'
import Files from './Files.vue'
import Index from './Index.vue'
import Overview from './Overview.vue'

export { Backups, Content, Files, Index, Overview }
export { Access, Backups, Content, Files, Index, Overview }
8 changes: 8 additions & 0 deletions apps/app-frontend/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ export default new createRouter({
breadcrumb: [{ name: '?Server' }],
},
},
{
path: 'access',
name: 'ServerManageAccess',
component: Hosting.Access,
meta: {
breadcrumb: [{ name: '?Server' }],
},
},
],
},
{
Expand Down
13 changes: 13 additions & 0 deletions apps/frontend/src/pages/hosting/manage/[id]/access.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
import { injectModrinthServerContext, ServersManageAccessPage } from '@modrinth/ui'

const { server } = injectModrinthServerContext()

useHead({
title: computed(() => `Access - ${server.value?.name ?? 'Server'} - Modrinth`),
})
</script>

<template>
<ServersManageAccessPage />
</template>
65 changes: 65 additions & 0 deletions packages/api-client/src/modules/archon/server-users/v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'

export class ArchonServerUsersV1Module extends AbstractModule {
public getModuleID(): string {
return 'archon_server_users_v1'
}

/**
* Get list of users with access to a server
* GET /v1/servers/:server_id/users
*/
public async list(serverId: string): Promise<Archon.ServerUsers.v1.ServerUser[]> {
return this.client.request<Archon.ServerUsers.v1.ServerUser[]>(`/servers/${serverId}/users`, {
api: 'archon',
version: 1,
method: 'GET',
})
}

/**
* Add a user to a server
* POST /v1/servers/:server_id/users
*/
public async add(
serverId: string,
user: Archon.ServerUsers.v1.AddServerUserRequest,
): Promise<void> {
await this.client.request(`/servers/${serverId}/users`, {
api: 'archon',
version: 1,
method: 'POST',
body: user,
})
}

/**
* Remove a user from a server
* DELETE /v1/servers/:server_id/users/:user_id
*/
public async delete(serverId: string, userId: string): Promise<void> {
await this.client.request(`/servers/${serverId}/users/${userId}`, {
api: 'archon',
version: 1,
method: 'DELETE',
})
}

/**
* Update a user's server role
* PATCH /v1/servers/:server_id/users/:user_id
*/
public async update(
serverId: string,
userId: string,
role: Archon.ServerUsers.v1.AssignableServerUserRole,
): Promise<void> {
await this.client.request(`/servers/${serverId}/users/${userId}`, {
api: 'archon',
version: 1,
method: 'PATCH',
body: role,
})
}
}
22 changes: 22 additions & 0 deletions packages/api-client/src/modules/archon/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,28 @@ export namespace Archon {
}
}

export namespace ServerUsers {
export namespace v1 {
export type ServerUserRole = 'Owner' | 'Editor' | 'Viewer' | 'Unknown'

export type AssignableServerUserRole = Exclude<ServerUserRole, 'Owner' | 'Unknown'>

export type ServerUser = {
server_id: string | null
user_id: string
added_on: string | null
role: ServerUserRole
}

export type AddServerUserRequest = {
server_id?: string | null
user_id: string
added_on?: string | null
role: AssignableServerUserRole
}
}
}

export namespace Servers {
export namespace v0 {
export type ServerGetResponse = {
Expand Down
2 changes: 2 additions & 0 deletions packages/api-client/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ArchonBackupsQueueV1Module } from './archon/backups-queue/v1'
import { ArchonContentV1Module } from './archon/content/v1'
import { ArchonOptionsV1Module } from './archon/options/v1'
import { ArchonPropertiesV1Module } from './archon/properties/v1'
import { ArchonServerUsersV1Module } from './archon/server-users/v1'
import { ArchonServersV0Module } from './archon/servers/v0'
import { ArchonServersV1Module } from './archon/servers/v1'
import { ISO3166Module } from './iso3166'
Expand Down Expand Up @@ -61,6 +62,7 @@ export const MODULE_REGISTRY = {
archon_content_v1: ArchonContentV1Module,
archon_options_v1: ArchonOptionsV1Module,
archon_properties_v1: ArchonPropertiesV1Module,
archon_server_users_v1: ArchonServerUsersV1Module,
archon_servers_v0: ArchonServersV0Module,
archon_servers_v1: ArchonServersV1Module,
iso3166_data: ISO3166Module,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions packages/assets/generated-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

import type { FunctionalComponent, SVGAttributes } from 'vue'

export type IconComponent = FunctionalComponent<SVGAttributes>

import _AffiliateIcon from './icons/affiliate.svg?component'
import _AlignLeftIcon from './icons/align-left.svg?component'
import _ArchiveIcon from './icons/archive.svg?component'
Expand Down Expand Up @@ -395,6 +393,8 @@ import _XCircleIcon from './icons/x-circle.svg?component'
import _ZoomInIcon from './icons/zoom-in.svg?component'
import _ZoomOutIcon from './icons/zoom-out.svg?component'

export type IconComponent = FunctionalComponent<SVGAttributes>

export const AffiliateIcon = _AffiliateIcon
export const AlignLeftIcon = _AlignLeftIcon
export const ArchiveIcon = _ArchiveIcon
Expand Down
2 changes: 2 additions & 0 deletions packages/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import _DiscordIcon from './external/discord.svg?component'
import _FacebookIcon from './external/facebook.svg?component'
import _FlathubIcon from './external/flathub.svg?component'
import _GithubIcon from './external/github.svg?component'
import _IntercomBubbleIcon from './external/illustrations/intercom_bubble_icon.png?url'
import _MinecraftServerIcon from './external/illustrations/minecraft_server_icon.png?url'
import _InstagramIcon from './external/instagram.svg?component'
import _KoFiIcon from './external/kofi.svg?component'
Expand Down Expand Up @@ -132,6 +133,7 @@ export const VenmoIcon = _VenmoIcon
export const PolygonIcon = _PolygonIcon
export const USDCColorIcon = _USDCColorIcon
export const VisaIcon = _VisaIcon
export const IntercomBubbleIcon = _IntercomBubbleIcon
export const MinecraftServerIcon = _MinecraftServerIcon

export * from './generated-icons'
Expand Down
28 changes: 26 additions & 2 deletions packages/ui/src/components/base/Combobox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
class="fixed z-[9999] flex flex-col overflow-hidden rounded-[14px] bg-surface-4 border border-solid border-surface-5"
:class="[
openDirection === 'up' ? 'shadow-[0_-25px_50px_-12px_rgb(0,0,0,0.25)]' : 'shadow-2xl',
props.dropdownClass,
]"
:style="dropdownStyle"
:role="listbox ? 'listbox' : 'menu'"
Expand Down Expand Up @@ -157,7 +158,7 @@
</template>
</div>

<div v-else-if="searchQuery" class="p-4 mb-2 text-center text-sm text-secondary">
<div v-else-if="searchQuery" class="p-4 text-center text-sm text-secondary">
{{ noOptionsMessage }}
</div>

Expand Down Expand Up @@ -229,6 +230,9 @@ const props = withDefaults(
forceDirection?: 'up' | 'down'
noOptionsMessage?: string
disableSearchFilter?: boolean
dropdownClass?: string
dropdownMinWidth?: string
minSearchLengthToOpen?: number
/** Keep the selected option's label in the input after selection, and show all options on focus */
syncWithSelection?: boolean
/** Show a search icon in the searchable input */
Expand All @@ -244,6 +248,7 @@ const props = withDefaults(
showIconInSelected: false,
maxHeight: DEFAULT_MAX_HEIGHT,
noOptionsMessage: 'No results found',
minSearchLengthToOpen: 0,
syncWithSelection: true,
showSearchIcon: false,
},
Expand Down Expand Up @@ -283,6 +288,7 @@ const dropdownStyle = ref({
top: '0px',
left: '0px',
width: '0px',
minWidth: '0px',
})

const openDirection = ref<'down' | 'up'>('down')
Expand Down Expand Up @@ -316,6 +322,12 @@ const triggerText = computed(() => {
return props.placeholder
})

const hasMinimumSearchLength = computed(
() =>
!props.searchable ||
searchQuery.value.trim().length >= props.minSearchLengthToOpen,
)

const optionsWithKeys = computed(() => {
return props.options.map((opt, index) => ({
...opt,
Expand Down Expand Up @@ -426,13 +438,15 @@ async function updateDropdownPosition() {
top: `${top}px`,
left: `${left}px`,
width: `${triggerRect.width}px`,
minWidth: props.dropdownMinWidth ?? `${triggerRect.width}px`,
}

openDirection.value = direction
}

async function openDropdown() {
if (props.disabled || isOpen.value) return
if (!hasMinimumSearchLength.value) return

isOpen.value = true
emit('open')
Expand Down Expand Up @@ -622,6 +636,10 @@ function handleSearchKeydown(event: KeyboardEvent) {
function handleSearchInput() {
userHasTyped.value = true
emit('searchInput', searchQuery.value)
if (!hasMinimumSearchLength.value) {
closeDropdown()
return
}
if (!isOpen.value) {
openDropdown()
}
Expand Down Expand Up @@ -689,10 +707,16 @@ watch(filteredOptions, () => {
}
})

watch(hasMinimumSearchLength, (canOpen) => {
if (!canOpen) {
closeDropdown()
}
})

watch(
[() => props.modelValue, () => props.options],
([val]) => {
if (props.searchable && props.syncWithSelection && !isOpen.value) {
if (props.searchable && props.syncWithSelection && !isOpen.value && !userHasTyped.value) {
const opt = props.options.find((o) => isDropdownOption(o) && o.value === val)
searchQuery.value = opt && isDropdownOption(opt) ? opt.label : ''
}
Expand Down
26 changes: 26 additions & 0 deletions packages/ui/src/components/servers/ServerListing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@
<h2 class="m-0 text-xl font-bold text-contrast" :class="{ 'opacity-50': isDisabled }">
{{ name }}
</h2>
<div
v-if="owner"
v-tooltip="formatMessage(messages.ownerTooltip, { username: owner.username })"
class="flex min-w-0 items-center gap-1 rounded-full bg-surface-4 px-2 pr-2.5 py-1 text-sm font-medium text-primary !border !border-surface-5 border-solid"
:class="{ 'opacity-50': isDisabled }"
>
<Avatar
:src="owner.avatarUrl"
:alt="formatMessage(messages.ownerAvatarAlt, { username: owner.username })"
:tint-by="owner.username"
size="1.25rem"
circle
no-shadow
/>
<span class="max-w-32 truncate">{{ owner.username }}</span>
</div>
<div
v-if="isConfiguring && noticeType !== 'cancelled' && noticeType !== 'setToCancel'"
class="flex min-w-0 items-center gap-2 truncate text-sm font-medium text-brand rounded-full bg-brand-highlight border border-solid border-brand px-2.5 h-[28px]"
Expand Down Expand Up @@ -264,6 +280,7 @@ import { injectModrinthClient } from '../../providers/api-client'
import Avatar from '../base/Avatar.vue'
import IntlFormatted from '../base/IntlFormatted.vue'
import ServersSpecs from '../billing/ServersSpecs.vue'
import type { ServerListingOwner } from './access/types'
import ServerIcon from './icons/ServerIcon.vue'
import ServerInfoLabels from './labels/ServerInfoLabels.vue'

Expand All @@ -283,6 +300,14 @@ const messages = defineMessages({
id: 'servers.listing.using-project-label',
defaultMessage: 'Using {projectTitle}',
},
ownerTooltip: {
id: 'servers.listing.owner-tooltip',
defaultMessage: 'Owned by {username}',
},
ownerAvatarAlt: {
id: 'servers.listing.owner-avatar-alt',
defaultMessage: "{username}'s avatar",
},
provisioningNotice: {
id: 'servers.listing.notice.provisioning',
defaultMessage: 'Please wait while we set up your server. This can take up to 10 minutes.',
Expand Down Expand Up @@ -404,6 +429,7 @@ type ServerListingProps = {
cancellationDate?: string | Date | null
onResubscribe?: (() => void) | null
onDownloadBackup?: (() => void) | null
owner?: ServerListingOwner
}

const props = defineProps<ServerListingProps>()
Expand Down
Loading
Loading