Add OIDC sign-in, group-derived roles and declarative instance configuration

Workspaces, group mappings and sign-in methods now come from a configuration
file synchronized at startup; workspaces dropped from it are disabled, never
deleted. Instance administrators (a new role) see and help every workspace and
may delete disabled ones; workspace administrators and employees are derived
from the user's groups at every SSO sign-in or administrator edit, and the
employee permission set belongs to the workspace. Self-registration is gone:
users come from SSO (only when their groups map) or from the administrator.
Break-glass bootstrap accounts get an HMAC-derived hourly password that all
replicas agree on. First SSO sign-in links a matching email contact to the
user. The demo runs on the same model with a generated per-start password.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-07 13:28:43 +01:00
1 parent e37d583179
commit 4764f3545f
68 files changed
+7220 -964

No files matched your search

+19
View File
@@ -194,3 +194,22 @@ input[type=date]:not(.native-date),.date-field{max-width:176px;min-width:0}
.map-source-card{border:1px solid var(--line);border-radius:7px;padding:10px;margin:8px 0;background:#f8faf9}.map-source-title{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12px}.map-source-title>small{margin-left:auto;color:var(--muted)}.map-source-addresses{display:flex;flex-direction:column;gap:4px;margin-top:7px}.map-source-addresses code{font:10px/1.5 Consolas,monospace;overflow-wrap:anywhere;color:#567565}.map-source-card>button{margin-top:6px;padding:3px 0}
.map-resource-fields{padding:0 12px 12px;border-top:1px solid var(--line)}.map-field-hint{display:block;font-size:10px;line-height:1.5;color:var(--muted);margin-bottom:6px}.map-resource-fields .map-reference>small{flex-basis:auto;margin:0;min-width:15px}.map-resource-fields .map-input-field:last-child{margin-bottom:0}
.login-form .password-login{display:block}.login-form .password-login>label:first-of-type{margin-top:22px}.login-form .sso-button{display:block;width:100%;margin-top:26px;padding:13px;text-align:center;text-decoration:none;border:1px solid var(--green);border-radius:7px;box-sizing:border-box}
.login-divider{display:flex;align-items:center;gap:12px;margin:22px 0 0;color:var(--muted);font-size:12px}.login-divider::before,.login-divider::after{content:"";flex:1;border-top:1px solid var(--line)}
.login-error{display:flex;flex-direction:column;gap:6px;margin-top:22px;padding:12px 14px;border:1px solid #e9c6b9;border-radius:7px;background:#fff3ee;color:#a04e34;font-size:13px;line-height:1.5}.login-error small{font-size:11px;user-select:all;color:#8a5f4f}
.demo-accounts{list-style:none;margin:8px 0;padding:0;display:flex;flex-direction:column;gap:2px}.demo-account{display:flex;align-items:baseline;gap:10px;padding:2px 0;font-size:11px;text-align:left;border:0;background:none}.demo-account code{min-width:56px;font-weight:700}.demo-password{display:block;margin-top:6px}.demo-password code{user-select:all}.demo-note small{display:block;margin-top:6px;color:#8a977f}
a.button{display:inline-block;text-decoration:none;border:1px solid #dbe3de;border-radius:7px;padding:9px 13px;color:inherit}a.button.primary{width:auto;margin-top:12px}
.sidebar nav .instance-link{margin-top:6px;border-top:1px solid var(--line);border-radius:0;padding-top:18px}
.warning-banner{margin:0 0 20px;padding:14px 16px;border:1px solid #e5c9a8;border-radius:8px;background:#fdf4e3;color:#7d5a1d;font-size:13px;line-height:1.5}
.config-summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 24px;margin:12px 0}.config-summary>div{min-width:0}.config-summary dt{font-size:11px;color:var(--muted);margin-bottom:3px}.config-summary dd{margin:0;overflow-wrap:anywhere}
.admin-workspace.disabled{background:#faf7f2}.admin-workspace small{display:block;margin-top:3px;overflow-wrap:anywhere}.disabled-note{color:#a17f32}
.admin-user small{display:block;margin-top:3px;overflow-wrap:anywhere}.admin-user .badge,.admin-workspace .badge{white-space:nowrap}.admin-user>button,.admin-workspace>button{white-space:nowrap}small.inline{display:inline}
.padded{padding:16px}.danger-button{color:#a04e34;border-color:#e9c6b9}.danger-button:hover{background:#fff3ee}.danger-note{background:#fff3ee;color:#a04e34}
.role-badge{text-transform:none}.role-badge.admin{background:#e4f1e8;color:#3d7e57}
.permission-list{list-style:none;margin:12px 0;padding:0;display:flex;flex-direction:column;gap:6px}.permission-list li{color:var(--muted)}.permission-list li.on{color:var(--ink)}
.group-picker{border:1px solid var(--line);border-radius:8px;padding:10px 14px;margin:12px 0 0}.group-picker legend{font-size:12px;color:var(--muted);padding:0 6px}.group-picker .check{margin:6px 0}
@media(max-width:700px){.config-summary{grid-template-columns:1fr}.admin-user,.admin-workspace{flex-wrap:wrap}.admin-user>.grow,.admin-workspace>.grow{flex-basis:calc(100% - 60px)}}
@media(max-width:700px){.sidebar nav .instance-link{margin:0;border:0;border-radius:6px;padding:6px}.sidebar nav .instance-link>span:not(.nav-icon){position:absolute;width:1px;height:1px;clip-path:inset(50%);overflow:hidden;white-space:nowrap}}
.admin-user .badge,.admin-workspace .badge{text-transform:none}.admin-user strong{display:inline}
@media(max-width:700px){.sidebar nav .instance-link{padding:6px 2px}.sidebar nav .instance-link .nav-icon{width:16px}}
+28 -16
View File
@@ -18,8 +18,8 @@ document.addEventListener('alpine:init', () => {
}));
Alpine.data('meguri', () => ({
...generationUi(),
user: null, csrf: '', ready: false, busy: false, error: '', toast: '', allowSignup: false, demo: false, demoLifecycle:null,
loginForm: { username: '', password: '', display_name: '' }, registering: false,
user: null, csrf: '', ready: false, busy: false, error: '', toast: '', demo: false, demoLifecycle:null, auth:{oidc:false,password:true}, loginError:null, admin:null,
loginForm: { username: '', password: '' },
directoryState: { workspaces: [] }, selected: '', detail: null, calendar: { days: [], assignments: [] },
view: 'calendar', scheduleId: '', startDay: '', modal: '', form: {}, menu: false,
navigationRestoring:0, navigationEpoch:0, setupOpen:false, scheduleSearch:'', schedulePicker:false, coverQuery:'', requestResult:null, requestError:'', requestLoading:false, requestSequence:0,
@@ -51,7 +51,7 @@ document.addEventListener('alpine:init', () => {
workingSummary() { const w=this.doc?.working_interval; return w ? w.weekdays.map(d=>[tr('mon_24b2a0'),tr('tue_529541'),tr('wed_23408b'),tr('thu_3593cc'),tr('fri_bbd6e3'),tr('sat_6b782d'),tr('sun_48c98c')][d-1]).join(', ')+' · '+w.start.slice(0,5)+'–'+w.end.slice(0,5)+' · '+w.timezone : ''; },
conditionText(c) { return (c.negate?tr('not_975146'):'')+this.label(c.field)+' '+this.label(c.operation)+' “'+c.value+'”'; },
scheduleTime(id) { return this.doc?.schedules.find(s=>s.id===id)?.handover?.slice(0,5) || '11:00'; },
get nav(){return [{ id: 'calendar', name: tr('calendar_adab50'), icon: '▦' }, { id: 'requests', name: tr('requests_f7194e'), icon: '⇄' }, { id: 'configuration', name: tr('schedule_setup'), icon: '⊞' }, { id: 'activity', name: tr('activity_81c0d9'), icon: '≡' }, { id: 'settings', name: tr('team_settings_06b774'), icon: '⚙' }];},
get nav(){return [{ id: 'calendar', name: tr('calendar_adab50'), icon: '▦' }, { id: 'requests', name: tr('requests_f7194e'), icon: '⇄' }, { id: 'configuration', name: tr('schedule_setup'), icon: '⊞' }, { id: 'activity', name: tr('activity_81c0d9'), icon: '≡' }, { id: 'settings', name: tr('team_settings_06b774'), icon: '⚙' }, { id: 'instance', name: tr('access_instance_nav'), icon: '⬢' }];},
get grantOptions(){return [{ id: 'view', name: tr('view_calendar_b702b1') }, { id: 'request_overrides', name: tr('request_replacements_386c5e') }, { id: 'edit_schedules', name: tr('edit_schedules_policies_a02744') }, { id: 'force_overrides', name: tr('apply_without_confirmation_f0350d') }, { id: 'manage_members', name: tr('manage_team_access_db8b53') }, { id: 'manage_integration', name: tr('manage_provider_804901') }, { id: 'view_audit', name: tr('view_audit_history_896487') }];},
get workspace() { return this.detail?.workspace; },
get doc() { return this.workspace?.document; },
@@ -70,6 +70,11 @@ document.addEventListener('alpine:init', () => {
},
get needsMe() { return this.pending.filter(r=>r.required_approvers.includes(this.user?.id) && !r.approvals.includes(this.user?.id)); },
get myRequests() { return this.pending.filter(r=>r.requested_by===this.user?.id); },
get employeeGrantOptions(){return this.grantOptions.filter(g=>!['manage_members','manage_integration'].includes(g.id));},
canEditUserGroups(u){return !u.sso && !u.bootstrap && u.id!==this.user?.id;},
async loadAdmin(){this.admin=await this.api('/api/admin/overview');},
useDemoAccount(account){this.loginForm.username=account.username;this.loginForm.password=this.demoLifecycle?.password||'';},
async copyDemoPassword(){try{await navigator.clipboard.writeText(this.demoLifecycle?.password||'');this.toast=tr('access_demo_password_copied');setTimeout(()=>this.toast='',4500);}catch(e){this.loginForm.password=this.demoLifecycle?.password||'';}},
get setupView() { return ['configuration','settings'].includes(this.view); },
get coverPeople() { const q=this.coverQuery.toLowerCase(); return this.people.filter(p=>p.id!==this.form.from_user && (this.members.some(m=>m.user.id===p.id) || this.form.force_mode) && [p.display_name,p.email,p.username].some(v=>v?.toLowerCase().includes(q))); },
get nextDuty() { const now=Date.now(); return this.calendar.assignments.filter(a=>a.schedule_id===this.scheduleId && a.user_id===this.user?.id && !a.unavailable && Date.parse(a.end)>now).sort((a,b)=>a.start.localeCompare(b.start))[0]; },
@@ -79,10 +84,11 @@ document.addEventListener('alpine:init', () => {
get monthTitle() { return new Date((this.startDay || this.today()) + 'T12:00:00Z').toLocaleDateString(this.language, { month: 'long', year: 'numeric', timeZone: 'UTC' }); },
get nowAssignment() { const now = new Date(); return this.calendar.assignments.find(a => !a.unavailable && a.schedule_id === this.scheduleId && new Date(a.start) <= now && now < new Date(a.end)); },
can(permission) { return (this.detail?.permissions || []).includes(permission); },
person(id) { return this.people.find(p=>p.id===id)?.display_name || this.members.find(m=>m.user.id===id)?.user.display_name || tr('unknown_employee_f68692'); },
personById(id) { return this.people.find(p=>p.id===id) || this.people.find(p=>p.aliases?.includes(id)); },
person(id) { return this.personById(id)?.display_name || this.members.find(m=>m.user.id===id)?.user.display_name || tr('unknown_employee_f68692'); },
shortName(id) { return this.person(id).split(' ')[0]; },
initials(id) { return this.person(id).split(' ').map(s => s[0]).slice(0, 2).join(''); },
color(id) { return ['teal', 'purple', 'amber', 'blue', 'rose'][Math.max(0, this.people.findIndex(p => p.id === id)) % 5]; },
color(id) { return ['teal', 'purple', 'amber', 'blue', 'rose'][Math.max(0, this.people.findIndex(p => p.id === id || p.aliases?.includes(id))) % 5]; },
scheduleName(id) { return this.doc?.schedules.find(s => s.id === id)?.name || tr('archived_schedule_40f4da'); },
today() { return this.dayKey(new Date(), this.doc?.timezone || 'UTC'); },
dayKey(date, zone = 'UTC') { const p = Object.fromEntries(new Intl.DateTimeFormat('en', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(date).map(p => [p.type, p.value])); return `${p.year}-${p.month}-${p.day}`; },
@@ -109,21 +115,23 @@ document.addEventListener('alpine:init', () => {
writeNavigation(replace=false){if(!this.ready || this.navigationRestoring)return;const href=this.menuHref(this.view);if(href!==location.pathname+location.search+location.hash)history[replace?'replaceState':'pushState']({},'',href);},
menuClick(event,view){if(event.button!==0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey)return;event.preventDefault();this.view=view;},
async changeWorkspace(){this.modal='';this.scheduleId='';this.startDay='';this.writeNavigation();await this.run(()=>this.loadWorkspace());},
async restoreNavigation(){const epoch=++this.navigationEpoch;this.navigationRestoring++;try{const route=this.readNavigation();this.modal='';this.error='';this.view=route.view;if(this.user){const selected=this.directoryState.workspaces.some(t=>t.id===route.workspace)?route.workspace:this.directoryState.workspaces[0]?.id||'';if(selected!==this.selected){this.selected=selected;this.scheduleId='';this.startDay='';await this.loadWorkspace();}}else this.selected=route.workspace;await this.$nextTick();}catch(e){if(epoch===this.navigationEpoch)this.error=e.message;}finally{this.navigationRestoring--;if(!this.navigationRestoring)this.writeNavigation(true);}},
async restoreNavigation(){const epoch=++this.navigationEpoch;this.navigationRestoring++;try{const route=this.readNavigation();this.modal='';this.error='';this.view=route.view==='instance' && this.user && !this.user.instance_admin?'calendar':route.view;if(this.user){const selected=this.directoryState.workspaces.some(t=>t.id===route.workspace)?route.workspace:this.directoryState.workspaces[0]?.id||'';if(selected!==this.selected){this.selected=selected;this.scheduleId='';this.startDay='';await this.loadWorkspace();}}else this.selected=route.workspace;await this.$nextTick();}catch(e){if(epoch===this.navigationEpoch)this.error=e.message;}finally{this.navigationRestoring--;if(!this.navigationRestoring)this.writeNavigation(true);}},
async init() {
this.$watch('user',user=>{Alpine.store('locale').language=user?.language || I18n.browser;});
this.$watch('form',()=>{if(this.modal==='absence')this.absenceChanged();if(this.modal==='request')this.requestChanged();if(['variable','generation-rule'].includes(this.modal))this.generationChanged();});
this.$watch('draftPeople',()=>{if(this.modal==='absence')this.absenceChanged();});
this.$watch('generationDraft',()=>{if(this.generationDraft && this.can('edit_schedules'))this.generationChanged();});
this.$watch('modal',(value,old)=>{if(['variable','generation-rule'].includes(old) && !value){this.bindingEditor=null;this.rulePeopleTarget=null;this.generationChanged();}});
this.$watch('view',v=>{if(v==='configuration')this.loadGenerationDraft();if(this.setupView)this.setupOpen=true;this.writeNavigation();});
this.$watch('view',v=>{if(v==='instance'){if(this.user?.instance_admin)this.run(()=>this.loadAdmin());}if(v==='configuration')this.loadGenerationDraft();if(this.setupView)this.setupOpen=true;this.writeNavigation();});
this.consumeLoginError();
const initial=this.readNavigation();this.selected=initial.workspace;this.view=initial.view;window.addEventListener('popstate',()=>this.restoreNavigation());
await this.run(async () => { const session = await this.api('/api/session'); this.user = session.user; this.csrf = session.csrf || ''; this.allowSignup = session.allow_signup; this.demo = session.demo;this.demoLifecycle=session.demo_lifecycle; if (this.user) await this.loadWorkspaces(); });
await this.run(async () => { const session = await this.api('/api/session'); this.user = session.user; this.csrf = session.csrf || ''; this.auth = session.auth || {oidc:false,password:true}; this.demo = session.demo;this.demoLifecycle=session.demo_lifecycle; if (this.user) { if(this.view==='instance' && !this.user.instance_admin)this.view='calendar'; await this.loadWorkspaces(); if(this.view==='instance')await this.loadAdmin(); } });
if(this.setupView)this.setupOpen=true;this.ready = true;this.writeNavigation(true);this.$nextTick(()=>{const update=()=>document.documentElement.style.setProperty('--demo-offset',(this.$refs.demoBanner?.getBoundingClientRect().height||0)+'px');new ResizeObserver(update).observe(this.$refs.demoBanner);update();});if(this.view==='configuration')this.loadGenerationDraft();
setInterval(() => { if (this.user && this.workspace && this.view!=='configuration' && !this.modal && !this.busy && !this.draggedRoute) this.run(() => this.loadWorkspace()); }, 20000);
},
async signIn() { await this.run(async () => { const s = await this.api(this.registering ? '/api/register' : '/api/login', 'POST', this.loginForm); this.user = s.user; this.csrf = s.csrf; this.loginForm.password = ''; await this.loadWorkspaces(); }); },
async signOut() { await this.run(async () => { await this.api('/api/logout', 'POST', {}); this.generationSequence++;clearTimeout(this.generationTimer);this.generationDraft=null;this.generationResult=null;this.generationWorkspace='';this.view='calendar';this.modal='';this.form={};this.user = null; this.detail = null; this.menu = false; this.selected = '';this.directoryState={workspaces:[]};this.startDay=''; }); },
consumeLoginError(){const url=new URL(location.href),code=url.searchParams.get('login_error');if(code===null)return;const ref=url.searchParams.get('ref')||'';this.loginError={code:['no_access','failed','unavailable'].includes(code)?code:'failed',ref:ref.slice(0,64)};url.searchParams.delete('login_error');url.searchParams.delete('ref');history.replaceState({},'',url.pathname+url.search+url.hash);},
async signIn() { await this.run(async () => { this.loginError=null; const s = await this.api('/api/login', 'POST', this.loginForm); this.user = s.user; this.csrf = s.csrf; this.loginForm.password = ''; await this.loadWorkspaces(); if(this.view==='instance'){if(this.user.instance_admin)await this.loadAdmin();else this.view='calendar';} }); },
async signOut() { await this.run(async () => { await this.api('/api/logout', 'POST', {}); this.generationSequence++;clearTimeout(this.generationTimer);this.generationDraft=null;this.generationResult=null;this.generationWorkspace='';this.view='calendar';this.modal='';this.form={};this.user = null; this.admin = null; this.detail = null; this.menu = false; this.selected = '';this.directoryState={workspaces:[]};this.startDay=''; }); },
async loadWorkspaces() { this.directoryState = await this.api('/api/workspaces'); if (!this.directoryState.workspaces.some(t => t.id === this.selected)) this.selected = (this.demo && ['alice','bob','carol'].includes(this.user?.username) ? this.directoryState.workspaces.find(t=>t.name===(this.user.username==='carol'?'Customer Support':'Acme Engineering'))?.id : '') || this.directoryState.workspaces[0]?.id || ''; if (this.selected) await this.loadWorkspace(); else this.detail = null; this.writeNavigation(true); },
async loadWorkspace() { const selected = this.directoryState.workspaces.find(t => t.id === this.selected); if (!selected) return; const detail = await this.api(`/api/workspaces/${selected.id}`);if(this.selected!==selected.id)return;this.detail=detail; if (!this.doc.schedules.some(s => s.id === this.scheduleId)) this.scheduleId = this.doc.schedules[0]?.id || ''; if (!this.startDay) this.startDay = this.monday(); await this.loadCalendar(); if(this.view==='configuration')this.loadGenerationDraft(); },
async loadCalendar() { const workspace=this.workspace.id;const calendar=await this.api(this.endpoint(`/calendar?from=${this.startDay}&days=28&schedule=${this.scheduleId}`));if(this.selected===workspace)this.calendar=calendar; },
@@ -138,8 +146,8 @@ document.addEventListener('alpine:init', () => {
if (type === 'priority') this.form = { id: crypto.randomUUID(), name: '', type: 'priority', handover:'11:00:00', participants: this.members.slice(0,2).map(m=>m.user.id), fallback: this.doc.schedules.find(s => s.type === 'rotation')?.id, ...data };
if (type === 'absence') {this.absenceResult=null;this.absenceError='';this.absenceRevision=this.workspace.revision;this.showAllAbsence=false;this.form = { id: crypto.randomUUID(), user_id: this.user.id, start: this.today(), end: this.today(), ranges:[],force:false,reason:'',...data };this.absenceChanged();}
if (type === 'holiday') this.form = { date: this.today(), name: '', country: this.user.country || 'CY', ...data };
if (type === 'member') this.form = { username: '', permissions: ['view', 'request_overrides'], ...data };
if (type === 'workspace') this.form = { name: '' };
if (type === 'member-permissions') this.form = { permissions: [...(this.doc.member_permissions || ['view','request_overrides','edit_schedules'])] };
if (type === 'admin-user') this.form = { username: '', display_name: '', password: '', email: '', groups: [] };
if (type === 'account') this.form = { language:this.user.language || '', display_name: this.user.display_name, country: this.user.country || 'CY', timezone:this.user.timezone || '', current_password: '', password: '' };
if (type === 'policy') this.form = { id: crypto.randomUUID(), name: '', steps: [{ delay_minutes: 0, condition: 'if-not-acked', target: `schedule:${this.doc.schedules[0]?.id}` }], repeat: { count: 0, wait_minutes: 0, reset_recipient_states: false, close_alert_after_all: false }, ...data };
if (type === 'policy' && data.steps) this.form.steps = data.steps.map(s => ({ ...s, target: s.recipient.id ? `${s.recipient.type}:${s.recipient.id}` : s.recipient.type }));
@@ -258,13 +266,16 @@ document.addEventListener('alpine:init', () => {
if(['variable','generation-rule'].includes(this.modal)){this.saveGenerationModal();return;}
await this.run(async () => {
const f = copy(this.form); const kind = this.modal; const revision = this.workspace?.revision;
if (kind === 'workspace') { const created=await this.api('/api/workspaces','POST',f);this.selected=created.id; }
else if (kind === 'account') { const r = await this.api('/api/account', 'PUT', f); this.user = r.user; }
if (kind === 'account') { const r = await this.api('/api/account', 'PUT', f); this.user = r.user; }
else if(kind==='working') await this.api(this.endpoint('/working-interval'),'PUT',{revision,interval:{...f,weekdays:f.weekdays.map(Number),start:timeValue(f.start),end:timeValue(f.end)}});
else if(kind==='holiday-import') { if(!this.canImport)throw new Error(tr('preview_the_csv_and_fix_any_errors_before_importing_2ccc4c'));await this.api(this.endpoint('/holiday-import'),'POST',{revision:this.csvResult.revision,csv:this.csvText}); }
else if(kind==='remove-schedule') { if(!this.canRemoveSchedule)throw new Error(tr('resolve_the_listed_dependencies_before_removing_this_fa1445'));await this.api(this.endpoint('/schedules/'+f.id+'/remove'),'POST',{revision:this.removal.revision,redirect_to:f.redirect_to || null}); }
else if(kind==='absence') {if(!this.canSaveAbsence)throw new Error(tr('wait_for_a_valid_absence_preview_before_saving_c743bf'));await this.api(this.endpoint('/absences'),'POST',this.absencePayload());}
else if (kind === 'member') await this.api(this.endpoint('/members'), 'PUT', { revision, ...f });
else if (kind === 'member-permissions') await this.api(this.endpoint('/member-permissions'), 'PUT', { revision, permissions: ['view', ...f.permissions.filter(p=>p!=='view')] });
else if (kind === 'admin-user') { const body={username:f.username,display_name:f.display_name,password:f.password,groups:f.groups};if(f.email.trim())body.email=f.email.trim();await this.api('/api/admin/users','POST',body);await this.loadAdmin(); }
else if (kind === 'admin-groups') { await this.api(`/api/admin/users/${f.id}/groups`,'PUT',{groups:f.groups});await this.loadAdmin(); }
else if (kind === 'admin-password') { await this.api(`/api/admin/users/${f.id}/password`,'POST',{password:f.password});await this.loadAdmin(); }
else if (kind === 'admin-delete') { await this.api(`/api/admin/workspaces/${f.id}`,'DELETE',{confirm:f.confirm});await this.loadAdmin(); }
else if (kind === 'integration') await this.api(this.endpoint('/integration'), 'PUT', { revision, integration: f.provider === 'mock' ? { provider: 'mock' } : {...f,users:Object.fromEntries(Object.entries(f.users).filter(([id,value])=>value?.trim()).map(([id,value])=>[id,value.trim()]))} });
else if (kind === 'force') await this.api(this.endpoint(`/requests/${f.id}`), 'POST', { revision, action: 'force', reason: f.reason });
else {
@@ -289,8 +300,9 @@ document.addEventListener('alpine:init', () => {
await this.api(this.endpoint('/config'), 'PUT', { revision, document: doc });
}
await this.loadWorkspaces(); this.modal = ''; this.draftPeople=[];
}, force ? tr('replacement_applied_publication_is_queued_89e283') : tr('changes_saved_5ffbaf'));
}, force ? tr('replacement_applied_publication_is_queued_89e283') : this.savedMessage(this.modal));
},
savedMessage(kind){const key={'admin-user':'access_user_created','admin-groups':'access_groups_saved','admin-password':'access_password_reset_done','admin-delete':'access_workspace_deleted'}[kind];return tr(key||'changes_saved_5ffbaf');},
async decide(request, action) { await this.run(async () => { await this.api(this.endpoint(`/requests/${request.id}`), 'POST', { revision: this.workspace.revision, action }); await this.loadWorkspace(); }, tr('request_updated_c52ad2')); },
async removeItem(collection, id) {
if(collection==='schedules') { await this.run(async()=>{const result=await this.api(this.endpoint('/schedules/'+id+'/removal'));this.open('remove-schedule',{id,redirect_to:''});this.removal=result;});return; }
+171 -43
View File
@@ -41,22 +41,31 @@
</div>
<span class="quiet"><span x-text="t('plan_here_stay_in_sync_everywhere_70e528')"></span></span>
</div>
<form class="login-form" @submit.prevent="signIn">
<div class="login-form">
<span class="eyebrow"><span x-text="t('your_team_s_home_for_on_call_66a895')"></span></span>
<h2 x-text="registering ? t('create_your_account_4046b5') : t('welcome_back_b80783')"></h2>
<h2 x-text="t('welcome_back_b80783')"></h2>
<p class="muted"><span x-text="t('make_space_for_a_smoother_handover_44adaf')"></span></p>
<div class="login-error" role="alert" x-cloak x-show="loginError"><span x-text="loginError ? t('access_login_error_'+loginError.code) : ''"></span><small x-show="loginError?.ref" x-text="loginError?.ref ? t('access_support_reference',{ref:loginError.ref}) : ''"></small></div>
<a class="button primary sso-button" href="/api/oidc/login" x-cloak x-show="auth.oidc"><span x-text="t('access_sso_button')"></span></a>
<div class="login-divider" x-cloak x-show="auth.oidc && auth.password"><span x-text="t('access_or_divider')"></span></div>
<form class="password-login" @submit.prevent="signIn" x-cloak x-show="auth.password">
<label><span x-text="t('username_84c290')"></span><input autocomplete="username" x-model="loginForm.username" required maxlength="64">
</label>
<label x-show="registering"><span x-text="t('display_name_c7874a')"></span><input x-model="loginForm.display_name" :required="registering">
<label><span x-text="t('password_8be3c9')"></span><input type="password" autocomplete="current-password" x-model="loginForm.password" required>
</label>
<label><span x-text="t('password_8be3c9')"></span><input type="password" :autocomplete="registering ? 'new-password' : 'current-password'" x-model="loginForm.password" required :minlength="registering ? 12 : 1">
</label>
<button class="primary" :disabled="busy" x-text="registering ? t('create_account_aaf374') : t('sign_in_ada2e9')">
<button class="primary" :disabled="busy" x-text="t('sign_in_ada2e9')">
</button>
<button type="button" class="text" x-show="allowSignup" @click="registering=!registering" x-text="registering ? t('already_registered_sign_in_0df05c') : t('new_here_create_an_account_2b2c3f')">
</button>
<div class="demo-note" x-show="demo && !registering"><strong><span x-text="t('try_the_demo_38cfaf')"></span></strong><dl><div><dt><span x-text="t('alice_522b27')"></span></dt><dd><span x-text="t('lead_both_workspaces_variables_rules_ed9a19')"></span></dd></div><div><dt><span x-text="t('bob_48181a')"></span></dt><dd><span x-text="t('engineering_weekly_on_call_swaps_de49e8')"></span></dd></div><div><dt><span x-text="t('carol_28b92b')"></span></dt><dd><span x-text="t('support_daily_shifts_approvals_c9a0d8')"></span></dd></div><div><dt x-text="t('demo_extra_users')"></dt><dd x-text="t('demo_extra_users_hint')"></dd></div></dl><span><span x-text="t('password_for_all_2f1a67')"></span> <code><span x-text="t('meguri_demo_only_0c7b5d')"></span></code></span></div>
</form>
<p class="info-note" x-cloak x-show="!auth.password && !auth.oidc"><span x-text="t('access_no_sign_in_method')"></span></p>
<div class="demo-note" x-cloak x-show="demo && demoLifecycle?.accounts?.length"><strong><span x-text="t('access_demo_accounts')"></span></strong>
<ul class="demo-accounts">
<template x-for="account in demoLifecycle?.accounts || []" :key="account.username">
<li><button type="button" class="text demo-account" :data-demo-account="account.username" :aria-label="t('access_demo_use_account',{username:account.username})" @click="useDemoAccount(account)"><code x-text="account.username"></code><span x-text="t('access_role_'+account.role)"></span></button></li>
</template>
</ul>
<span class="demo-password"><span x-text="t('access_demo_password')"></span> <code data-demo-password x-text="demoLifecycle?.password"></code> <button type="button" class="text" @click="copyDemoPassword()"><span x-text="t('access_demo_copy_password')"></span></button></span>
<small x-text="t('access_demo_restart_hint')"></small></div>
</div>
</main>
<div class="shell" :inert="!!modal" x-cloak x-show="ready && user">
<aside class="sidebar">
@@ -83,6 +92,7 @@
</template>
<button class="setup-toggle" :aria-label="t('setup_cdd7bb')" :class="{active:setupView}" @click="setupOpen=!setupOpen" :aria-expanded="setupOpen"><span class="nav-icon"><span x-text="t('symbol_be22a3')"></span></span><span><span x-text="t('setup_cdd7bb')"></span></span><span class="grow"></span><span x-text="setupOpen?'−':'+'"></span></button>
<div class="setup-nav" x-show="setupOpen"><template x-for="item in nav.filter(n=>['configuration','settings'].includes(n.id))" :key="item.id"><a :href="menuHref(item.id)" :aria-current="view===item.id?'page':null" :class="{active:view===item.id}" @click="menuClick($event,item.id)" x-text="item.name"></a></template></div>
<a class="instance-link" x-cloak x-show="user?.instance_admin" :href="menuHref('instance')" :aria-current="view==='instance'?'page':null" :class="{active:view==='instance'}" @click="menuClick($event,'instance')"><span aria-hidden="true" class="nav-icon" x-text="nav.find(n=>n.id==='instance').icon"></span><span x-text="nav.find(n=>n.id==='instance').name"></span></a>
</nav>
<div class="sidebar-bottom">
@@ -115,21 +125,79 @@
</span>
<div class="top-actions">
<span class="display-zone" x-text="t('your_time_31bdbf')+displayZone"></span>
<button class="text" @click="open('workspace')"><span x-text="t('new_workspace_b21b86')"></span></button>
</div>
</header>
<div class="content">
<template x-if="!workspace">
<section class="empty welcome">
<span class="empty-symbol"><span x-text="t('symbol_4d82f1')"></span></span>
<h1><span x-text="t('your_team_s_next_chapter_starts_here_4e986d')"></span></h1>
<p><span x-text="t('create_a_workspace_add_a_team_and_give_your_on_call__af9412')"></span></p>
<button class="primary" @click="open('workspace')" x-text="t('create_a_workspace_e94c64')">
</button>
<template x-if="view==='instance' && user?.instance_admin">
<section class="instance-page" id="instance">
<div class="page-heading"><div><div class="eyebrow" x-text="t('access_instance_eyebrow')"></div><h1 x-text="t('access_instance_title')"></h1></div></div>
<div class="warning-banner" role="alert" x-show="admin?.config.bootstrap_usernames.length"><span x-text="t('access_bootstrap_warning')"></span></div>
<template x-if="admin">
<div>
<article class="panel rule-card spaced" id="instance-config">
<span class="eyebrow"><span x-text="t('access_config_title')"></span></span>
<dl class="config-summary">
<div><dt x-text="t('access_config_password_login')"></dt><dd x-text="admin.config.password_login?t('access_enabled'):t('access_disabled')"></dd></div>
<div><dt x-text="t('access_config_oidc')"></dt><dd x-text="admin.config.oidc_issuer || t('access_not_configured')"></dd></div>
<div><dt x-text="t('access_config_admin_groups')"></dt><dd x-text="admin.config.instance_admin_groups.join(', ') || t('access_none')"></dd></div>
<div><dt x-text="t('access_config_bootstrap')"></dt><dd x-text="admin.config.bootstrap_usernames.join(', ') || t('access_none')"></dd></div>
</dl>
<p class="muted"><span x-text="t('access_config_note')"></span></p>
</article>
<div class="section-line"><h2><span x-text="t('access_workspaces_title')"></span></h2></div>
<div class="panel compact" id="instance-workspaces">
<p class="muted padded" x-show="!admin.workspaces.length"><span x-text="t('access_workspaces_empty')"></span></p>
<template x-for="w in admin.workspaces" :key="w.id">
<div class="list-row admin-workspace" :class="{disabled:w.disabled_at}" :data-workspace-key="w.key">
<div class="grow">
<strong x-text="w.name"></strong>
<small x-text="t('access_workspace_key',{key:w.key || '—'})+' · '+t('access_workspace_members',{count:w.member_count})+(w.sync_state?' · '+t('access_workspace_publication',{state:w.sync_state}):'')"></small>
<small x-show="w.in_config"><span x-text="t('access_groups_admin')+': '+(w.admin_groups.join(', ') || t('access_none'))"></span></small>
<small x-show="w.in_config"><span x-text="t('access_groups_member')+': '+(w.member_groups.join(', ') || t('access_none'))"></span></small>
<small class="disabled-note" x-show="w.disabled_at" x-text="t('access_workspace_disabled')+' · '+t('access_workspace_disabled_since',{date:formatTime(w.disabled_at)})"></small>
</div>
<span class="badge" x-show="w.in_config" x-text="t('access_workspace_in_config')"></span>
<button class="danger-button" x-show="w.disabled_at" @click="open('admin-delete',{id:w.id,name:w.name,expected:w.confirm,confirm:''})"><span x-text="t('access_delete_workspace')"></span></button>
</div>
</template>
</div>
<div class="section-line"><h2><span x-text="t('access_users_title')"></span></h2>
<button class="primary" @click="open('admin-user')"><span x-text="t('access_add_user')"></span></button>
</div>
<div class="panel compact" id="instance-users">
<p class="muted padded" x-show="!admin.users.length"><span x-text="t('access_users_empty')"></span></p>
<template x-for="u in admin.users" :key="u.id">
<div class="list-row admin-user" :data-username="u.username">
<span class="avatar" x-text="u.display_name.slice(0,1)"></span>
<div class="grow">
<strong x-text="u.display_name"></strong> <small class="inline" x-show="u.id===user.id"><span x-text="t('you_8af56d')"></span></small>
<small x-text="'@'+u.username+(u.email?' · '+u.email:'')"></small>
<small x-text="t('access_user_groups')+': '+(u.groups.join(', ') || t('access_user_no_groups'))"></small>
<small class="muted" x-show="u.sso"><span x-text="t('access_groups_sso_readonly')"></span></small>
<small class="muted" x-show="u.bootstrap"><span x-text="t('access_groups_bootstrap_readonly')"></span></small>
</div>
<span class="badge" x-show="u.instance_admin" x-text="t('access_role_instance_admin')"></span>
<span class="badge" x-show="u.sso" x-text="t('access_badge_sso')"></span>
<span class="badge" x-show="u.bootstrap" x-text="t('access_badge_bootstrap')"></span>
<span class="badge" x-show="!u.sso && !u.bootstrap" x-text="t('access_badge_password')"></span>
<button x-show="canEditUserGroups(u)" @click="open('admin-groups',{id:u.id,username:u.username,groups:[...u.groups]})"><span x-text="t('access_edit_groups')"></span></button>
<button x-show="!u.sso && !u.bootstrap" @click="open('admin-password',{id:u.id,username:u.username,password:''})"><span x-text="t('access_reset_password')"></span></button>
</div>
</template>
</div>
</div>
</template>
</section>
</template>
<template x-if="workspace">
<template x-if="view!=='instance' && !workspace">
<section class="empty welcome">
<span class="empty-symbol"><span x-text="t('symbol_4d82f1')"></span></span>
<h1><span x-text="t('access_no_workspace_title')"></span></h1>
<p x-text="user?.instance_admin?t('access_no_workspace_admin_text'):t('access_no_workspace_text')"></p>
<a class="button primary" x-show="user?.instance_admin" :href="menuHref('instance')" @click="menuClick($event,'instance')"><span x-text="t('access_open_instance_page')"></span></a>
</section>
</template>
<template x-if="view!=='instance' && workspace">
<div>
<div class="page-heading">
<div>
@@ -340,23 +408,43 @@
</div>
<div class="section-line">
<h2><span x-text="t('team_access_8fb357')"></span></h2>
<button class="primary" x-show="can('manage_members')" @click="open('member')"><span x-text="t('add_teammate_d6bc5a')"></span></button>
</div>
<div class="panel compact">
<p class="info-note" x-show="detail?.instance_admin && !members.some(m=>m.user.id===user?.id)"><span x-text="t('access_instance_admin_note')"></span></p>
<div class="panel compact" id="workspace-members">
<template x-for="m in members" :key="m.user?.id">
<div class="list-row">
<div class="list-row" :data-username="m.user.username">
<span class="avatar" :class="color(m.user?.id)" x-text="initials(m.user?.id)">
</span>
<div class="grow">
<strong x-text="m.user.display_name">
</strong>
<small x-text="'@'+m.user.username+' · '+m.permissions.length+t('permissions_607d39')">
<small x-text="'@'+m.user.username">
</small>
</div>
<button x-show="can('manage_members')" @click="open('member',{username:m.user.username,permissions:m.permissions})"><span x-text="t('edit_access_e381a8')"></span></button>
<span class="badge role-badge" :class="{admin:m.is_owner}" x-text="m.is_owner?t('access_role_workspace_admin'):t('access_role_employee')"></span>
</div>
</template>
</div>
<p class="muted"><span x-text="t('access_members_hint')"></span></p>
<article class="panel rule-card spaced" id="employee-permissions">
<span class="eyebrow"><span x-text="t('access_employee_permissions_title')"></span></span>
<p class="muted"><span x-text="t('access_employee_permissions_hint')"></span></p>
<ul class="permission-list">
<template x-for="grant in employeeGrantOptions" :key="grant.id">
<li :class="{on:(doc.member_permissions||[]).includes(grant.id)}" :data-permission="grant.id"><span aria-hidden="true" x-text="(doc.member_permissions||[]).includes(grant.id)?'✓':'·'"></span> <span x-text="grant.name"></span></li>
</template>
</ul>
<p class="info-note" x-text="(doc.member_permissions||[]).includes('force_overrides')?t('access_force_enabled'):t('access_force_disabled')"></p>
<div class="actions" x-show="can('manage_members')"><button @click="open('member-permissions')"><span x-text="t('access_edit_employee_permissions')"></span></button></div>
</article>
<article class="panel rule-card spaced" id="access-groups" x-show="detail?.access">
<span class="eyebrow"><span x-text="t('access_groups_title')"></span></span>
<dl class="config-summary">
<div><dt x-text="t('access_groups_admin')"></dt><dd x-text="(detail?.access?.admin_groups||[]).join(', ') || t('access_none')"></dd></div>
<div><dt x-text="t('access_groups_member')"></dt><dd x-text="(detail?.access?.member_groups||[]).join(', ') || t('access_none')"></dd></div>
</dl>
<p class="muted"><span x-text="t('access_groups_hint')"></span></p>
</article>
<div class="section-line">
<h2><span x-text="t('scheduling_directory_e8fdd4')"></span></h2>
<span class="muted"><span x-text="t('country_controls_public_holidays_defaults_to_cyprus_e828a2')"></span></span>
@@ -407,8 +495,8 @@
<form @submit.prevent="save()">
<div class="modal-heading">
<div>
<span class="eyebrow" x-show="modal!=='workspace'" x-text="['request','day','force'].includes(modal)?scheduleName(form.schedule_id || scheduleId):doc?.name || 'MEGURI'"></span>
<h2 id="dialog-title" x-text="modal==='request'?(form.force_mode?t('apply_without_approval_d72926'):form.from_user===user?.id?t('who_can_cover_for_you_6e894c'):t('cover_person',{name:shortName(form.from_user)})):modal==='day'?t('choose_a_shift_330348'):({variable:t('workspace_variable_7abc57'),'generation-rule':t('generation_rule_d34ccf'),rotation:t('rotation_906fe5'),priority:t('ordered_project_coverage_cf12c3'),coverage:t('project_coverage_536388'),request:t('request_a_change_3d465f'),force:t('apply_without_confirmation_f0350d'),absence:t('planned_absence_bfcea7'),holiday:t('public_holiday_49ec03'),'holiday-import':t('import_public_holidays_8098e3'),'remove-schedule':t('remove_schedule_ef0a8a'),person:t('scheduling_person_1273c6'),working:t('workspace_working_interval_c7d250'),member:t('team_access_8fb357'),workspace:t('new_workspace_b21b86'),account:t('your_account_4ab291'),policy:t('escalation_policy_bbb0f7'),route:t('routing_rule_b00b22'),integration:t('connected_provider_9702c0'),preferences:t('team_preferences_7fcab5')})[modal]">
<span class="eyebrow" x-text="modal.startsWith('admin-')?t('access_instance_eyebrow'):['request','day','force'].includes(modal)?scheduleName(form.schedule_id || scheduleId):doc?.name || 'MEGURI'"></span>
<h2 id="dialog-title" x-text="modal==='request'?(form.force_mode?t('apply_without_approval_d72926'):form.from_user===user?.id?t('who_can_cover_for_you_6e894c'):t('cover_person',{name:shortName(form.from_user)})):modal==='day'?t('choose_a_shift_330348'):({variable:t('workspace_variable_7abc57'),'generation-rule':t('generation_rule_d34ccf'),rotation:t('rotation_906fe5'),priority:t('ordered_project_coverage_cf12c3'),coverage:t('project_coverage_536388'),request:t('request_a_change_3d465f'),force:t('apply_without_confirmation_f0350d'),absence:t('planned_absence_bfcea7'),holiday:t('public_holiday_49ec03'),'holiday-import':t('import_public_holidays_8098e3'),'remove-schedule':t('remove_schedule_ef0a8a'),person:t('scheduling_person_1273c6'),working:t('workspace_working_interval_c7d250'),'member-permissions':t('access_employee_permissions_title'),'admin-user':t('access_new_user_title'),'admin-groups':t('access_edit_groups_title'),'admin-password':t('access_reset_password_title'),'admin-delete':t('access_delete_workspace_title'),account:t('your_account_4ab291'),policy:t('escalation_policy_bbb0f7'),route:t('routing_rule_b00b22'),integration:t('connected_provider_9702c0'),preferences:t('team_preferences_7fcab5')})[modal]">
</h2>
</div>
<button type="button" class="icon-button" @click="modal=''" :aria-label="t('close_dialog_7b2902')"><span x-text="t('symbol_67fba2')"></span></button>
@@ -471,13 +559,6 @@
</div>
</div>
</template><div x-show="!picker">
<template x-if="modal==='workspace'">
<div>
<label><span x-text="t('name_709a23')"></span><input x-model="form.name" required maxlength="100">
</label>
</div>
</template>
<template x-if="modal==='account'">
<div>
<label><span x-text="t('language')"></span><select x-model="form.language" :aria-label="t('language')"><option value="" x-text="t('language_auto')"></option><template x-for="code in I18n.supported"><option :value="code" :selected="form.language===code" x-text="t('language_'+code)"></option></template></select></label>
@@ -496,10 +577,13 @@
<p class="info-note" x-text="t('dates_and_timestamps_are_displayed_in_77ec74')+(form.timezone || browserZone)+t('this_never_changes_the_workspace_timezone_or_shifts_1de864')">
</p>
<p class="muted"><span x-text="t('country_changes_update_your_holiday_calendar_in_team_1e4eb8')"></span></p>
<template x-if="user?.has_password"><div>
<label><span x-text="t('current_password_19dff4')"></span><input type="password" autocomplete="current-password" x-model="form.current_password">
</label>
<label><span x-text="t('new_password_d850ee')"></span><input type="password" autocomplete="new-password" minlength="12" x-model="form.password">
</label>
</div></template>
<p class="muted" x-show="!user?.has_password"><span x-text="t('access_sso_password_note')"></span></p>
</div>
</template>
<template x-if="modal==='person'">
@@ -562,7 +646,7 @@
<div class="grow">
<strong x-text="person(id)">
</strong>
<small x-text="countryName(people.find(p=>p.id===id)?.country)">
<small x-text="countryName(personById(id)?.country)">
</small>
</div>
<button type="button" @click="editPerson(id)" :aria-label="t('edit_participant_details_73e491')"><span x-text="t('details_dc3dec')"></span></button>
@@ -735,18 +819,62 @@
</div>
</div>
</template>
<template x-if="modal==='member'">
<template x-if="modal==='member-permissions'">
<div>
<label><span x-text="t('registered_username_e29aa9')"></span><input x-model="form.username" required>
</label>
<p class="muted"><span x-text="t('your_teammate_needs_an_account_before_you_can_add_th_80fa32')"></span></p>
<template x-for="grant in grantOptions">
<p class="muted"><span x-text="t('access_employee_permissions_hint')"></span></p>
<template x-for="grant in employeeGrantOptions" :key="grant.id">
<label class="check">
<input type="checkbox" :value="grant.id" x-model="form.permissions" :disabled="grant.id==='view'">
<span x-text="grant.name">
</span>
<input type="checkbox" :value="grant.id" x-model="form.permissions" :disabled="grant.id==='view'" :data-permission="grant.id">
<span x-text="grant.name"></span>
</label>
</template>
<p class="info-note" x-text="t('access_force_hint')"></p>
</div>
</template>
<template x-if="modal==='admin-user'">
<div>
<label><span x-text="t('username_84c290')"></span><input x-model="form.username" autocomplete="off" required minlength="3" maxlength="64">
</label>
<label><span x-text="t('display_name_c7874a')"></span><input x-model="form.display_name" required>
</label>
<label><span x-text="t('access_email_optional')"></span><input type="email" x-model="form.email" autocomplete="off">
</label>
<label><span x-text="t('password_8be3c9')"></span><input type="password" x-model="form.password" autocomplete="new-password" required minlength="12" maxlength="256">
</label>
<p class="muted"><span x-text="t('access_password_hint')"></span></p>
<fieldset class="group-picker"><legend x-text="t('access_user_groups')"></legend>
<p class="muted" x-show="!admin?.known_groups.length"><span x-text="t('access_no_known_groups')"></span></p>
<template x-for="group in admin?.known_groups || []" :key="group">
<label class="check"><input type="checkbox" :value="group" x-model="form.groups"><span x-text="group"></span></label>
</template>
</fieldset>
</div>
</template>
<template x-if="modal==='admin-groups'">
<div>
<p class="muted"><span x-text="t('access_groups_picker_hint')"></span></p>
<fieldset class="group-picker"><legend x-text="'@'+form.username"></legend>
<p class="muted" x-show="!admin?.known_groups.length"><span x-text="t('access_no_known_groups')"></span></p>
<template x-for="group in admin?.known_groups || []" :key="group">
<label class="check"><input type="checkbox" :value="group" x-model="form.groups"><span x-text="group"></span></label>
</template>
</fieldset>
</div>
</template>
<template x-if="modal==='admin-password'">
<div>
<p class="muted" x-text="'@'+form.username"></p>
<label><span x-text="t('new_password_d850ee')"></span><input type="password" x-model="form.password" autocomplete="new-password" required minlength="12" maxlength="256">
</label>
<p class="muted"><span x-text="t('access_password_hint')"></span></p>
</div>
</template>
<template x-if="modal==='admin-delete'">
<div>
<p class="info-note danger-note"><span x-text="t('access_delete_warning')"></span></p>
<p><strong x-text="form.name"></strong></p>
<label><span x-text="t('access_delete_confirm_label',{confirm:form.expected})"></span><input x-model="form.confirm" autocomplete="off" required>
</label>
</div>
</template>
<template x-if="modal==='preferences'">
@@ -918,7 +1046,7 @@
<button type="button" @click="modal=''"><span x-text="t('cancel_77dfd2')"></span></button>
<button type="button" class="primary" x-show="rulePeopleTarget" @click="closeRuleEditor()" x-text="t('done_e9b450')"></button>
<button x-show="!rulePeopleTarget" class="primary" :class="{'remove-confirm':modal==='remove-schedule'}" :disabled="busy || (modal==='holiday-import' && !canImport) || (modal==='absence' && !canSaveAbsence) || (modal==='remove-schedule' && !canRemoveSchedule)" x-text="busy?t('saving_56a228'):modal==='remove-schedule'?(form.redirect_to?t('remove_redirect_references_f1a241'):removal?.plan.escalation_steps.length?t('remove_schedule_affected_steps_7348af'):t('remove_schedule_ef0a8a')):modal==='holiday-import'?t('import_new_holidays_b36db6'):modal==='request'?t('send_for_confirmation_f3df0f'):modal==='force'?t('apply_replacement_9b0845'):['variable','generation-rule'].includes(modal)?t('use_in_draft_a8f78c'):t('save_changes_179359')">
<button x-show="!rulePeopleTarget" class="primary" :class="{'remove-confirm':modal==='remove-schedule'}" :disabled="busy || (modal==='holiday-import' && !canImport) || (modal==='absence' && !canSaveAbsence) || (modal==='remove-schedule' && !canRemoveSchedule) || (modal==='admin-delete' && form.confirm!==form.expected)" x-text="busy?t('saving_56a228'):modal==='admin-user'?t('access_create_user'):modal==='admin-password'?t('access_reset_password'):modal==='admin-delete'?t('access_delete_permanently'):modal==='remove-schedule'?(form.redirect_to?t('remove_redirect_references_f1a241'):removal?.plan.escalation_steps.length?t('remove_schedule_affected_steps_7348af'):t('remove_schedule_ef0a8a')):modal==='holiday-import'?t('import_new_holidays_b36db6'):modal==='request'?t('send_for_confirmation_f3df0f'):modal==='force'?t('apply_replacement_9b0845'):['variable','generation-rule'].includes(modal)?t('use_in_draft_a8f78c'):t('save_changes_179359')">
</button>
</div>
</div>
+468 -118
View File
@@ -25,9 +25,9 @@
"fr": " · demandes : "
},
"visitor_accounts_f4ac15": {
"en": " · visitor accounts: ",
"ru": " · аккаунты посетителей: ",
"fr": " · comptes visiteurs : "
"en": " · added accounts: ",
"ru": " · добавленные аккаунты: ",
"fr": " · comptes ajoutés : "
},
"restart_resets_demo_data_examples_refresh_automatica_0082d1": {
"en": "Restart resets demo data. Examples refresh automatically.",
@@ -99,11 +99,6 @@
"ru": "ДЕЖУРСТВА ВАШЕЙ КОМАНДЫ",
"fr": "LES ASTREINTES DE VOTRE ÉQUIPE"
},
"create_your_account_4046b5": {
"en": "Create your account",
"ru": "Создайте аккаунт",
"fr": "Créez votre compte"
},
"welcome_back_b80783": {
"en": "Welcome back",
"ru": "С возвращением",
@@ -129,71 +124,16 @@
"ru": "Пароль",
"fr": "Mot de passe"
},
"create_account_aaf374": {
"en": "Create account",
"ru": "Создать аккаунт",
"fr": "Créer un compte"
},
"sign_in_ada2e9": {
"en": "Sign in",
"ru": "Войти",
"fr": "Se connecter"
},
"already_registered_sign_in_0df05c": {
"en": "Already registered? Sign in",
"ru": "Уже зарегистрированы? Войти",
"fr": "Déjà inscrit ? Se connecter"
},
"new_here_create_an_account_2b2c3f": {
"en": "New here? Create an account",
"ru": "Впервые здесь? Создать аккаунт",
"fr": "Nouveau ? Créer un compte"
},
"try_the_demo_38cfaf": {
"en": "Try the demo",
"ru": "Попробуйте демо",
"fr": "Essayez la démonstration"
},
"alice_522b27": {
"en": "alice",
"ru": "alice",
"fr": "alice"
},
"lead_both_workspaces_variables_rules_ed9a19": {
"en": "Lead · all four workspaces, variables & rules",
"ru": "Тимлид · все четыре пространства, переменные и правила",
"fr": "Responsable · les quatre espaces, variables et règles"
},
"bob_48181a": {
"en": "bob",
"ru": "bob",
"fr": "bob"
},
"engineering_weekly_on_call_swaps_de49e8": {
"en": "Engineering & regions · rotations and swaps",
"ru": "Разработка и регионы · ротации и подмены",
"fr": "Ingénierie et régions · rotations et échanges"
},
"carol_28b92b": {
"en": "carol",
"ru": "carol",
"fr": "carol"
},
"support_daily_shifts_approvals_c9a0d8": {
"en": "Support & commerce · shifts and service tiers",
"ru": "Поддержка и коммерция · смены и уровни обслуживания",
"fr": "Assistance et commerce · rotations et niveaux de service"
},
"password_for_all_2f1a67": {
"en": "Password for all:",
"ru": "Общий пароль:",
"fr": "Mot de passe commun :"
},
"meguri_demo_only_0c7b5d": {
"en": "meguri-demo-only",
"ru": "meguri-demo-only",
"fr": "meguri-demo-only"
},
"workspace_703988": {
"en": "WORKSPACE",
"ru": "ПРОСТРАНСТВО",
@@ -254,26 +194,6 @@
"ru": "Ваш часовой пояс: ",
"fr": "Votre fuseau : "
},
"new_workspace_b21b86": {
"en": "New workspace",
"ru": "Новое пространство",
"fr": "Nouvel espace"
},
"your_team_s_next_chapter_starts_here_4e986d": {
"en": "Your team’s next chapter starts here.",
"ru": "Начните планировать дежурства команды.",
"fr": "Organisez les prochaines astreintes de votre équipe."
},
"create_a_workspace_add_a_team_and_give_your_on_call__af9412": {
"en": "Create a workspace, invite colleagues and set up your schedules.",
"ru": "Создайте пространство, пригласите коллег и настройте расписания.",
"fr": "Créez un espace, invitez vos collègues et configurez les plannings."
},
"create_a_workspace_e94c64": {
"en": "Create a workspace",
"ru": "Создать пространство",
"fr": "Créer un espace"
},
"team_setup_504738": {
"en": "WORKSPACE SETUP",
"ru": "НАСТРОЙКИ ПРОСТРАНСТВА",
@@ -934,21 +854,6 @@
"ru": "Доступ к пространству",
"fr": "Accès à l’espace"
},
"add_teammate_d6bc5a": {
"en": "+ Add teammate",
"ru": "+ Добавить коллегу",
"fr": "+ Ajouter un collègue"
},
"permissions_607d39": {
"en": " permissions",
"ru": " разрешений",
"fr": " autorisations"
},
"edit_access_e381a8": {
"en": "Edit access",
"ru": "Изменить доступ",
"fr": "Modifier l’accès"
},
"scheduling_directory_e8fdd4": {
"en": "Scheduling directory",
"ru": "Участники расписаний",
@@ -1889,16 +1794,6 @@
"ru": "Будет добавлено",
"fr": "Sera ajouté"
},
"registered_username_e29aa9": {
"en": "Registered username",
"ru": "Имя зарегистрированного пользователя",
"fr": "Identifiant enregistré"
},
"your_teammate_needs_an_account_before_you_can_add_th_80fa32": {
"en": "Your teammate needs an account before you can add them.",
"ru": "Чтобы добавить коллегу, ему нужен аккаунт.",
"fr": "Votre collègue doit avoir un compte pour être ajouté."
},
"team_name_9b11ed": {
"en": "Workspace name",
"ru": "Название пространства",
@@ -5624,16 +5519,6 @@
"ru": "Без изменений: {count}",
"fr": "Inchangés : {count}"
},
"demo_extra_users": {
"en": "dave / erin",
"ru": "dave / erin",
"fr": "dave / erin"
},
"demo_extra_users_hint": {
"en": "Engineering · service ownership and cover",
"ru": "Engineering · ответственные и подмены",
"fr": "Engineering · responsables et remplacements"
},
"rule_result_title": {
"en": "Generated resources",
"ru": "Ресурсы правила",
@@ -5953,5 +5838,470 @@
"en": "Select a pass above to inspect its values.",
"ru": "Выберите проход выше, чтобы увидеть его значения.",
"fr": "Sélectionnez un passage ci-dessus pour voir ses valeurs."
},
"access_sso_button": {
"en": "Sign in with SSO",
"ru": "Войти через SSO",
"fr": "Se connecter avec SSO"
},
"access_or_divider": {
"en": "or",
"ru": "или",
"fr": "ou"
},
"access_login_error_no_access": {
"en": "The instance could not map your groups to any workspace. Contact the service administrator.",
"ru": "Инстанс не смог сопоставить ваши группы ни с одним пространством. Обратитесь к администратору сервиса.",
"fr": "L’instance n’a pu associer vos groupes à aucun espace. Contactez l’administrateur du service."
},
"access_login_error_failed": {
"en": "Single sign-on did not complete. Try again or contact the service administrator.",
"ru": "Вход через SSO не завершился. Повторите попытку или обратитесь к администратору сервиса.",
"fr": "L’authentification unique n’a pas abouti. Réessayez ou contactez l’administrateur du service."
},
"access_login_error_unavailable": {
"en": "The sign-in provider is unavailable. Try again later or contact the service administrator.",
"ru": "Провайдер входа недоступен. Повторите позже или обратитесь к администратору сервиса.",
"fr": "Le fournisseur d’authentification est indisponible. Réessayez plus tard ou contactez l’administrateur du service."
},
"access_support_reference": {
"en": "Reference for support: {ref}",
"ru": "Код для обращения в поддержку: {ref}",
"fr": "Référence pour le support : {ref}"
},
"access_no_sign_in_method": {
"en": "No sign-in method is configured. Contact the service administrator.",
"ru": "Способ входа не настроен. Обратитесь к администратору сервиса.",
"fr": "Aucune méthode de connexion n’est configurée. Contactez l’administrateur du service."
},
"access_demo_accounts": {
"en": "Demo accounts",
"ru": "Демо-аккаунты",
"fr": "Comptes de démonstration"
},
"access_demo_password": {
"en": "Password for every account this run:",
"ru": "Пароль всех аккаунтов этого запуска:",
"fr": "Mot de passe de tous les comptes de cette exécution :"
},
"access_demo_use_account": {
"en": "Sign in as {username}",
"ru": "Войти как {username}",
"fr": "Se connecter en tant que {username}"
},
"access_demo_copy_password": {
"en": "Copy password",
"ru": "Скопировать пароль",
"fr": "Copier le mot de passe"
},
"access_demo_password_copied": {
"en": "Password copied",
"ru": "Пароль скопирован",
"fr": "Mot de passe copié"
},
"access_demo_restart_hint": {
"en": "The password and all data change on every restart.",
"ru": "Пароль и все данные меняются при каждом перезапуске.",
"fr": "Le mot de passe et toutes les données changent à chaque redémarrage."
},
"access_role_instance_admin": {
"en": "Instance administrator",
"ru": "Администратор инстанса",
"fr": "Administrateur de l’instance"
},
"access_role_workspace_admin": {
"en": "Workspace administrator",
"ru": "Администратор пространства",
"fr": "Administrateur de l’espace"
},
"access_role_employee": {
"en": "Employee",
"ru": "Сотрудник",
"fr": "Employé"
},
"access_no_workspace_title": {
"en": "No workspace access yet",
"ru": "Пока нет доступа к пространствам",
"fr": "Aucun accès à un espace pour l’instant"
},
"access_no_workspace_text": {
"en": "Your account is not linked to any workspace. Contact the service administrator to get access.",
"ru": "Ваша учётная запись не связана ни с одним пространством. Обратитесь к администратору сервиса, чтобы получить доступ.",
"fr": "Votre compte n’est lié à aucun espace. Contactez l’administrateur du service pour obtenir l’accès."
},
"access_no_workspace_admin_text": {
"en": "No workspace is enabled. Workspaces come from the instance configuration; review them on the instance page.",
"ru": "Нет включённых пространств. Пространства задаются в конфигурации инстанса; посмотрите их на странице инстанса.",
"fr": "Aucun espace n’est activé. Les espaces viennent de la configuration de l’instance ; consultez-les sur la page de l’instance."
},
"access_open_instance_page": {
"en": "Open instance administration",
"ru": "Открыть администрирование инстанса",
"fr": "Ouvrir l’administration de l’instance"
},
"access_instance_nav": {
"en": "Instance",
"ru": "Инстанс",
"fr": "Instance"
},
"access_instance_title": {
"en": "Instance administration",
"ru": "Администрирование инстанса",
"fr": "Administration de l’instance"
},
"access_instance_eyebrow": {
"en": "Instance",
"ru": "Инстанс",
"fr": "Instance"
},
"access_bootstrap_warning": {
"en": "Bootstrap access is enabled. These accounts have full administrator access and their password is written to the server log. Remove the bootstrap setting from the configuration when it is no longer needed.",
"ru": "Включён bootstrap-доступ. Эти аккаунты имеют полные права администратора, а их пароль записан в лог сервера. Уберите bootstrap из конфигурации, когда он больше не нужен.",
"fr": "L’accès bootstrap est activé. Ces comptes ont tous les droits d’administration et leur mot de passe est écrit dans le journal du serveur. Retirez le réglage bootstrap de la configuration quand il n’est plus nécessaire."
},
"access_config_title": {
"en": "Configuration (read-only)",
"ru": "Конфигурация (только чтение)",
"fr": "Configuration (lecture seule)"
},
"access_config_note": {
"en": "Workspaces and group mapping are defined only in the instance configuration file. They cannot be edited here.",
"ru": "Пространства и сопоставление групп задаются только в файле конфигурации инстанса. Здесь их изменить нельзя.",
"fr": "Les espaces et l’association des groupes sont définis uniquement dans le fichier de configuration de l’instance. Ils ne peuvent pas être modifiés ici."
},
"access_config_password_login": {
"en": "Password sign-in",
"ru": "Вход по паролю",
"fr": "Connexion par mot de passe"
},
"access_config_oidc": {
"en": "SSO issuer",
"ru": "Провайдер SSO",
"fr": "Fournisseur SSO"
},
"access_config_admin_groups": {
"en": "Instance administrator groups",
"ru": "Группы администраторов инстанса",
"fr": "Groupes d’administrateurs de l’instance"
},
"access_config_bootstrap": {
"en": "Bootstrap accounts",
"ru": "Bootstrap-аккаунты",
"fr": "Comptes bootstrap"
},
"access_enabled": {
"en": "Enabled",
"ru": "Включено",
"fr": "Activé"
},
"access_disabled": {
"en": "Disabled",
"ru": "Выключено",
"fr": "Désactivé"
},
"access_not_configured": {
"en": "Not configured",
"ru": "Не настроено",
"fr": "Non configuré"
},
"access_none": {
"en": "None",
"ru": "Нет",
"fr": "Aucun"
},
"access_workspaces_title": {
"en": "Workspaces",
"ru": "Пространства",
"fr": "Espaces"
},
"access_workspace_key": {
"en": "Key: {key}",
"ru": "Ключ: {key}",
"fr": "Clé : {key}"
},
"access_workspace_members": {
"en": "Members: {count}",
"ru": "Участников: {count}",
"fr": "Membres : {count}"
},
"access_workspace_publication": {
"en": "Publication: {state}",
"ru": "Публикация: {state}",
"fr": "Publication : {state}"
},
"access_workspace_disabled": {
"en": "Disabled — not in configuration",
"ru": "Отключено — нет в конфигурации",
"fr": "Désactivé — absent de la configuration"
},
"access_workspace_disabled_since": {
"en": "Disabled since {date}",
"ru": "Отключено с {date}",
"fr": "Désactivé depuis le {date}"
},
"access_workspace_in_config": {
"en": "In configuration",
"ru": "В конфигурации",
"fr": "Dans la configuration"
},
"access_workspaces_empty": {
"en": "No workspaces exist yet.",
"ru": "Пространств пока нет.",
"fr": "Aucun espace pour l’instant."
},
"access_delete_workspace": {
"en": "Delete from database",
"ru": "Удалить из базы",
"fr": "Supprimer de la base"
},
"access_delete_workspace_title": {
"en": "Delete workspace",
"ru": "Удаление пространства",
"fr": "Supprimer l’espace"
},
"access_delete_warning": {
"en": "This permanently deletes the workspace with all its schedules, history and audit records. Resources already published to an external provider are not removed.",
"ru": "Пространство будет удалено навсегда вместе со всеми расписаниями, историей и записями аудита. Ресурсы, уже опубликованные во внешнем провайдере, не удаляются.",
"fr": "Cette action supprime définitivement l’espace avec tous ses plannings, son historique et son audit. Les ressources déjà publiées chez un fournisseur externe ne sont pas supprimées."
},
"access_delete_confirm_label": {
"en": "Type {confirm} to confirm",
"ru": "Введите {confirm} для подтверждения",
"fr": "Saisissez {confirm} pour confirmer"
},
"access_delete_permanently": {
"en": "Delete permanently",
"ru": "Удалить навсегда",
"fr": "Supprimer définitivement"
},
"access_workspace_deleted": {
"en": "Workspace deleted",
"ru": "Пространство удалено",
"fr": "Espace supprimé"
},
"access_users_title": {
"en": "Users",
"ru": "Пользователи",
"fr": "Utilisateurs"
},
"access_users_empty": {
"en": "No users yet.",
"ru": "Пользователей пока нет.",
"fr": "Aucun utilisateur pour l’instant."
},
"access_add_user": {
"en": "Add user",
"ru": "Добавить пользователя",
"fr": "Ajouter un utilisateur"
},
"access_new_user_title": {
"en": "New user",
"ru": "Новый пользователь",
"fr": "Nouvel utilisateur"
},
"access_create_user": {
"en": "Create user",
"ru": "Создать пользователя",
"fr": "Créer l’utilisateur"
},
"access_user_created": {
"en": "User created",
"ru": "Пользователь создан",
"fr": "Utilisateur créé"
},
"access_email_optional": {
"en": "Email (optional)",
"ru": "Email (необязательно)",
"fr": "E-mail (facultatif)"
},
"access_password_hint": {
"en": "At least 12 characters. Share it with the user through a secure channel.",
"ru": "Не менее 12 символов. Передайте его пользователю по защищённому каналу.",
"fr": "Au moins 12 caractères. Transmettez-le à la personne par un canal sûr."
},
"access_badge_sso": {
"en": "SSO",
"ru": "SSO",
"fr": "SSO"
},
"access_badge_password": {
"en": "Password",
"ru": "Пароль",
"fr": "Mot de passe"
},
"access_badge_bootstrap": {
"en": "Bootstrap",
"ru": "Bootstrap",
"fr": "Bootstrap"
},
"access_user_groups": {
"en": "Groups",
"ru": "Группы",
"fr": "Groupes"
},
"access_user_no_groups": {
"en": "No groups",
"ru": "Нет групп",
"fr": "Aucun groupe"
},
"access_groups_sso_readonly": {
"en": "Managed by the identity provider; shown here for reference.",
"ru": "Управляется провайдером входа; показано для справки.",
"fr": "Géré par le fournisseur d’identité ; affiché à titre indicatif."
},
"access_groups_bootstrap_readonly": {
"en": "Managed by the instance configuration.",
"ru": "Управляется конфигурацией инстанса.",
"fr": "Géré par la configuration de l’instance."
},
"access_groups_self_readonly": {
"en": "You cannot change your own groups.",
"ru": "Нельзя менять собственные группы.",
"fr": "Vous ne pouvez pas modifier vos propres groupes."
},
"access_edit_groups": {
"en": "Edit groups",
"ru": "Изменить группы",
"fr": "Modifier les groupes"
},
"access_edit_groups_title": {
"en": "User groups",
"ru": "Группы пользователя",
"fr": "Groupes de l’utilisateur"
},
"access_groups_picker_hint": {
"en": "Choose the groups this user belongs to. Groups come from the instance configuration.",
"ru": "Выберите группы пользователя. Список групп берётся из конфигурации инстанса.",
"fr": "Choisissez les groupes de cette personne. Les groupes viennent de la configuration de l’instance."
},
"access_no_known_groups": {
"en": "The configuration defines no groups.",
"ru": "В конфигурации не задано ни одной группы.",
"fr": "La configuration ne définit aucun groupe."
},
"access_groups_saved": {
"en": "Groups saved",
"ru": "Группы сохранены",
"fr": "Groupes enregistrés"
},
"access_reset_password": {
"en": "Reset password",
"ru": "Сбросить пароль",
"fr": "Réinitialiser le mot de passe"
},
"access_reset_password_title": {
"en": "Reset password",
"ru": "Сброс пароля",
"fr": "Réinitialisation du mot de passe"
},
"access_password_reset_done": {
"en": "Password changed",
"ru": "Пароль изменён",
"fr": "Mot de passe modifié"
},
"access_groups_title": {
"en": "Groups that grant access",
"ru": "Группы, дающие доступ",
"fr": "Groupes donnant accès"
},
"access_groups_admin": {
"en": "Workspace administrators",
"ru": "Администраторы пространства",
"fr": "Administrateurs de l’espace"
},
"access_groups_member": {
"en": "Employees",
"ru": "Сотрудники",
"fr": "Employés"
},
"access_groups_hint": {
"en": "Access follows the user’s groups at sign-in. These groups come from the instance configuration and cannot be changed here.",
"ru": "Доступ определяется группами пользователя при входе. Эти группы заданы в конфигурации инстанса и здесь не меняются.",
"fr": "L’accès suit les groupes de la personne à la connexion. Ces groupes viennent de la configuration de l’instance et ne peuvent pas être modifiés ici."
},
"access_members_hint": {
"en": "Members are added automatically when they sign in with a matching group.",
"ru": "Участники добавляются автоматически при входе с подходящей группой.",
"fr": "Les membres sont ajoutés automatiquement à la connexion avec un groupe correspondant."
},
"access_employee_permissions_title": {
"en": "Employee permissions",
"ru": "Права сотрудников",
"fr": "Autorisations des employés"
},
"access_employee_permissions_hint": {
"en": "Applies to every employee of this workspace. Workspace administrators always have every permission.",
"ru": "Действует для всех сотрудников этого пространства. Администраторы пространства всегда имеют все права.",
"fr": "S’applique à tous les employés de cet espace. Les administrateurs de l’espace ont toujours toutes les autorisations."
},
"access_edit_employee_permissions": {
"en": "Edit employee permissions",
"ru": "Изменить права сотрудников",
"fr": "Modifier les autorisations des employés"
},
"access_force_hint": {
"en": "With this permission an employee can apply a replacement immediately, without waiting for confirmation from the other person.",
"ru": "С этим правом сотрудник может применить подмену сразу, не дожидаясь согласия второго человека.",
"fr": "Avec cette autorisation, un employé peut appliquer un remplacement immédiatement, sans attendre la validation de l’autre personne."
},
"access_force_enabled": {
"en": "Replacements can be applied without confirmation",
"ru": "Подмены можно применять без согласования",
"fr": "Les remplacements peuvent être appliqués sans validation"
},
"access_force_disabled": {
"en": "Replacements need confirmation from the other person",
"ru": "Подмены требуют согласия второго человека",
"fr": "Les remplacements nécessitent la validation de l’autre personne"
},
"access_sso_password_note": {
"en": "Your password is managed by your identity provider.",
"ru": "Пароль управляется вашим провайдером входа.",
"fr": "Votre mot de passe est géré par votre fournisseur d’identité."
},
"access_instance_admin_note": {
"en": "You see this workspace as an instance administrator.",
"ru": "Вы видите это пространство как администратор инстанса.",
"fr": "Vous voyez cet espace en tant qu’administrateur de l’instance."
},
"err_employees_cannot_manage_members_or_integrations_3c21aa": {
"en": "Employees cannot be allowed to manage members or integrations",
"ru": "Сотрудникам нельзя разрешить управлять участниками или интеграциями",
"fr": "Les employés ne peuvent pas gérer les membres ni les intégrations"
},
"err_choose_groups_from_the_configured_list_5d0e71": {
"en": "Choose groups from the configured list",
"ru": "Выберите группы из настроенного списка",
"fr": "Choisissez des groupes dans la liste configurée"
},
"err_you_cannot_change_your_own_access_9b44c0": {
"en": "You cannot change your own access",
"ru": "Нельзя изменять собственный доступ",
"fr": "Vous ne pouvez pas modifier votre propre accès"
},
"err_this_account_is_managed_by_sso_or_configuration_a3c8e2": {
"en": "This account is managed by SSO or by the instance configuration",
"ru": "Эта учётная запись управляется через SSO или конфигурацию инстанса",
"fr": "Ce compte est géré par le SSO ou par la configuration de l’instance"
},
"err_only_disabled_workspaces_can_be_deleted_8f27d1": {
"en": "Only disabled workspaces that are absent from the configuration can be deleted",
"ru": "Удалять можно только отключённые рабочие пространства, которых нет в конфигурации",
"fr": "Seuls les espaces de travail désactivés et absents de la configuration peuvent être supprimés"
},
"err_type_the_workspace_key_to_confirm_deletion_4be9a0": {
"en": "Type the workspace key to confirm deletion",
"ru": "Введите ключ рабочего пространства для подтверждения удаления",
"fr": "Saisissez la clé de l’espace de travail pour confirmer la suppression"
},
"err_the_workspace_is_publishing_try_again_shortly_2a7d94": {
"en": "The workspace is publishing right now; try again shortly",
"ru": "Рабочее пространство сейчас публикуется; повторите чуть позже",
"fr": "L’espace de travail est en cours de publication ; réessayez dans un instant"
},
"err_this_account_signs_in_with_sso_and_has_no_password_6d1f3b": {
"en": "This account signs in with SSO and has no password",
"ru": "Эта учётная запись входит через SSO и не имеет пароля",
"fr": "Ce compte se connecte par SSO et n’a pas de mot de passe"
}
}