Rework user selection (#1728)

* Move to new route

* Convert to tailwind

* Basic style

* Add userStore

* nit

* nit

* nit

* Remove app.#setUser

* Route to user-select view

* Mock login

* Use primevue UI components

* handle create new user

* Remove legacy user selection

* Add logout button on side toolbar

* Add username to logout button tooltip

* Add playwright tests

* hide logout button in single user server

* nit
This commit is contained in:
Chenlei Hu
2024-11-28 20:36:41 -08:00
committed by GitHub
parent 541335bb31
commit 9e565154a9
18 changed files with 341 additions and 673 deletions

View File

@@ -42,6 +42,9 @@ export class ComfyApi extends EventTarget {
* The current client id from websocket status updates.
*/
clientId?: string
/**
* The current user id.
*/
user: string
socket: WebSocket | null = null
@@ -49,7 +52,6 @@ export class ComfyApi extends EventTarget {
constructor() {
super()
// api.user is set by ComfyApp.setup()
this.user = ''
this.api_host = location.host
this.api_base = location.pathname.split('/').slice(0, -1).join('/')

View File

@@ -1711,67 +1711,6 @@ export class ComfyApp {
)
}
async #setUser() {
const userConfig = await api.getUserConfig()
// Return in single user mode.
if (userConfig.users === undefined) {
return
}
let user = localStorage['Comfy.userId']
const users = userConfig.users ?? {}
if (!user || !users[user]) {
// Lift spinner / BlockUI for user selection.
if (this.vueAppReady) useWorkspaceStore().spinner = false
// This will rarely be hit so move the loading to on demand
const { UserSelectionScreen } = await import('./ui/userSelection')
this.ui.menuContainer.style.display = 'none'
const { userId, username } = await new UserSelectionScreen().show(
users,
user
)
this.ui.menuContainer.style.display = ''
user = userId
localStorage['Comfy.userName'] = username
localStorage['Comfy.userId'] = user
}
api.user = user
this.ui.settings.addSetting({
id: 'Comfy.SwitchUser',
name: 'Switch User',
type: (name) => {
let currentUser = localStorage['Comfy.userName']
if (currentUser) {
currentUser = ` (${currentUser})`
}
return $el('tr', [
$el('td', [
$el('label', {
textContent: name
})
]),
$el('td', [
$el('button', {
textContent: name + (currentUser ?? ''),
onclick: () => {
delete localStorage['Comfy.userId']
delete localStorage['Comfy.userName']
window.location.reload()
}
})
])
])
},
// TODO: Is that the correct default value?
defaultValue: undefined
})
}
/**
* Set up the app on the page
*/
@@ -1779,7 +1718,6 @@ export class ComfyApp {
this.canvasEl = canvasEl
// Show menu container for GraphView.
this.ui.menuContainer.style.display = 'block'
await this.#setUser()
this.resizeCanvas()

View File

@@ -1,135 +0,0 @@
.comfy-user-selection {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
font-family: sans-serif;
background: linear-gradient(var(--tr-even-bg-color), var(--tr-odd-bg-color));
}
.comfy-user-selection-inner {
background: var(--comfy-menu-bg);
margin-top: -30vh;
padding: 20px 40px;
border-radius: 10px;
min-width: 365px;
position: relative;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.comfy-user-selection-inner form {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.comfy-user-selection-inner h1 {
margin: 10px 0 30px 0;
font-weight: normal;
}
.comfy-user-selection-inner label {
display: flex;
flex-direction: column;
width: 100%;
}
.comfy-user-selection input,
.comfy-user-selection select {
background-color: var(--comfy-input-bg);
color: var(--input-text);
border: 0;
border-radius: 5px;
padding: 5px;
margin-top: 10px;
}
.comfy-user-selection input::placeholder {
color: var(--descrip-text);
opacity: 1;
}
.comfy-user-existing {
width: 100%;
}
.no-users .comfy-user-existing {
display: none;
}
.comfy-user-selection-inner .or-separator {
margin: 10px 0;
padding: 10px;
display: block;
text-align: center;
width: 100%;
color: var(--descrip-text);
}
.comfy-user-selection-inner .or-separator {
overflow: hidden;
text-align: center;
margin-left: -10px;
}
.comfy-user-selection-inner .or-separator::before,
.comfy-user-selection-inner .or-separator::after {
content: "";
background-color: var(--border-color);
position: relative;
height: 1px;
vertical-align: middle;
display: inline-block;
width: calc(50% - 20px);
top: -1px;
}
.comfy-user-selection-inner .or-separator::before {
right: 10px;
margin-left: -50%;
}
.comfy-user-selection-inner .or-separator::after {
left: 10px;
margin-right: -50%;
}
.comfy-user-selection-inner section {
width: 100%;
padding: 10px;
margin: -10px;
transition: background-color 0.2s;
}
.comfy-user-selection-inner section.selected {
background: var(--border-color);
border-radius: 5px;
}
.comfy-user-selection-inner footer {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.comfy-user-selection-inner .comfy-user-error {
color: var(--error-text);
margin-bottom: 10px;
}
.comfy-user-button-next {
font-size: 16px;
padding: 6px 10px;
width: 100px;
display: flex;
gap: 5px;
align-items: center;
justify-content: center;
}

View File

@@ -1,146 +0,0 @@
// @ts-strict-ignore
import { api } from '../api'
import { $el } from '../ui'
import { createSpinner } from './spinner'
import './userSelection.css'
interface SelectedUser {
username: string
userId: string
created: boolean
}
export class UserSelectionScreen {
async show(users, user): Promise<SelectedUser> {
const userSelection = document.getElementById('comfy-user-selection')
userSelection.style.display = ''
return new Promise((resolve) => {
const input = userSelection.getElementsByTagName('input')[0]
const select = userSelection.getElementsByTagName('select')[0]
const inputSection = input.closest('section')
const selectSection = select.closest('section')
const form = userSelection.getElementsByTagName('form')[0]
const error = userSelection.getElementsByClassName('comfy-user-error')[0]
const button = userSelection.getElementsByClassName(
'comfy-user-button-next'
)[0]
let inputActive = null
input.addEventListener('focus', () => {
inputSection.classList.add('selected')
selectSection.classList.remove('selected')
inputActive = true
})
select.addEventListener('focus', () => {
inputSection.classList.remove('selected')
selectSection.classList.add('selected')
inputActive = false
select.style.color = ''
})
select.addEventListener('blur', () => {
if (!select.value) {
select.style.color = 'var(--descrip-text)'
}
})
form.addEventListener('submit', async (e) => {
e.preventDefault()
if (inputActive == null) {
error.textContent =
'Please enter a username or select an existing user.'
} else if (inputActive) {
const username = input.value.trim()
if (!username) {
error.textContent = 'Please enter a username.'
return
}
// Create new user
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
input.disabled =
select.disabled =
// @ts-expect-error
input.readonly =
// @ts-expect-error
select.readonly =
true
const spinner = createSpinner()
button.prepend(spinner)
try {
const resp = await api.createUser(username)
if (resp.status >= 300) {
let message =
'Error creating user: ' + resp.status + ' ' + resp.statusText
try {
const res = await resp.json()
if (res.error) {
message = res.error
}
} catch (error) {}
throw new Error(message)
}
resolve({ username, userId: await resp.json(), created: true })
} catch (err) {
spinner.remove()
error.textContent =
err.message ??
err.statusText ??
err ??
'An unknown error occurred.'
// Property 'readonly' does not exist on type 'HTMLSelectElement'.ts(2339)
// Property 'readonly' does not exist on type 'HTMLInputElement'. Did you mean 'readOnly'?ts(2551)
input.disabled =
select.disabled =
// @ts-expect-error
input.readonly =
// @ts-expect-error
select.readonly =
false
return
}
} else if (!select.value) {
error.textContent = 'Please select an existing user.'
return
} else {
resolve({
username: users[select.value],
userId: select.value,
created: false
})
}
})
if (user) {
const name = localStorage['Comfy.userName']
if (name) {
input.value = name
}
}
if (input.value) {
// Focus the input, do this separately as sometimes browsers like to fill in the value
input.focus()
}
const userIds = Object.keys(users ?? {})
if (userIds.length) {
for (const u of userIds) {
$el('option', { textContent: users[u], value: u, parent: select })
}
select.style.color = 'var(--descrip-text)'
if (select.value) {
// Focus the select, do this separately as sometimes browsers like to fill in the value
select.focus()
}
} else {
userSelection.classList.add('no-users')
input.focus()
}
}).then((r: SelectedUser) => {
userSelection.remove()
return r
})
}
}