Skip to content

Commit a3220d9

Browse files
authored
Merge pull request #127 from jackbuehner/announcements-admin
Add an option for administrators to pin one or more notices/announcements at the top of the web app
2 parents b3edcc7 + 12932cc commit a3220d9

File tree

8 files changed

+284
-51
lines changed

8 files changed

+284
-51
lines changed

aspx/wwwroot/lib/controls/AppRoot.ascx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@
268268
iconBackgroundsEnabled: '<%= System.Configuration.ConfigurationManager.AppSettings["App.IconBackgroundsEnabled"] %>',
269269
simpleModeEnabled: '<%= System.Configuration.ConfigurationManager.AppSettings["App.SimpleModeEnabled"] %>',
270270
passwordChangeEnabled: '<%= System.Configuration.ConfigurationManager.AppSettings["PasswordChange.Enabled"] %>',
271+
signedInUserGlobalAlerts: `<%= System.Configuration.ConfigurationManager.AppSettings["App.Alerts.SignedInUser"] %>`,
271272
}
272273
window.__machineName = '<%= resolver.Resolve(Environment.MachineName) %>';
273274
window.__envMachineName = '<%= Environment.MachineName %>';

frontend/lib/App.vue

Lines changed: 105 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -234,35 +234,101 @@
234234
onMounted(() => {
235235
populateUpdateDetails();
236236
});
237+
238+
const signedInUserGlobalAlerts = (() => {
239+
const alertsJson = window.__policies.signedInUserGlobalAlerts;
240+
if (!alertsJson) {
241+
return [];
242+
}
243+
244+
try {
245+
const alerts = JSON.parse(alertsJson);
246+
if (!Array.isArray(alerts)) {
247+
return [];
248+
}
249+
return alerts.filter(
250+
(
251+
alert
252+
): alert is {
253+
title?: string;
254+
message?: string;
255+
linkText?: string;
256+
linkHref?: string;
257+
type?: 'information' | 'attention';
258+
} =>
259+
(alert && typeof alert === 'object' && typeof alert.title === 'string') ||
260+
(alert.title === undefined && typeof alert.message === 'string') ||
261+
(alert.message === undefined &&
262+
(typeof alert.linkText === 'string' || alert.linkText === undefined) &&
263+
(typeof alert.linkHref === 'string' || alert.linkHref === undefined) &&
264+
(alert.type === 'information' || alert.type === 'attention' || alert.type === undefined))
265+
);
266+
} catch {
267+
return [];
268+
}
269+
})();
270+
271+
const securityErrorHelpHref =
272+
'https://github.com/kimmknight/raweb/wiki/Trusting-the-RAWeb-server-(Fix-security-error-5003)';
273+
274+
function openInfoBarPopup(href: string, target: string) {
275+
const popup = window.open(href, target, 'width=1000,height=600,menubar=0,status=0');
276+
if (popup) {
277+
popup.focus();
278+
} else {
279+
alert('Please allow popups for this application');
280+
}
281+
}
237282
</script>
238283

239284
<template>
240285
<Titlebar forceVisible :loading="titlebarLoading || loading" :update="updateDetails" />
241286
<div id="appContent">
242287
<NavigationRail v-if="!simpleModeEnabled" />
243288
<main :class="{ simple: simpleModeEnabled }">
244-
<div>
245-
<template v-if="data">
246-
<InfoBar
247-
severity="caution"
248-
v-if="sslError"
249-
:title="$t('securityError503.title')"
250-
style="
251-
margin: calc(-1 * var(--padding)) calc(-1 * var(--padding)) var(--padding)
252-
calc(-1 * var(--padding));
253-
border-radius: 0;
254-
"
289+
<InfoBar
290+
severity="caution"
291+
v-if="sslError"
292+
:title="$t('securityError503.title')"
293+
style="border-radius: 0"
294+
>
295+
{{ $t('securityError503.message') }}
296+
<br />
297+
<Button
298+
variant="hyperlink"
299+
:href="securityErrorHelpHref"
300+
style="margin-left: -11px; margin-bottom: -6px"
301+
target="_blank"
302+
@click.prevent="openInfoBarPopup(securityErrorHelpHref, 'help')"
303+
>
304+
{{ $t('securityError503.action') }}
305+
</Button>
306+
</InfoBar>
307+
308+
<InfoBar
309+
v-for="(alert, index) in signedInUserGlobalAlerts"
310+
:key="index"
311+
:severity="alert.type || 'attention'"
312+
:title="alert.title"
313+
class="global-alert"
314+
>
315+
{{ alert.message }}
316+
<template v-if="alert.linkText && alert.linkHref">
317+
<br />
318+
<Button
319+
variant="hyperlink"
320+
:href="alert.linkHref"
321+
style="margin-left: -11px; margin-bottom: -6px"
322+
target="_blank"
323+
@click.prevent="openInfoBarPopup(alert.linkHref, alert.title || `alert-link-${index}`)"
255324
>
256-
{{ $t('securityError503.message') }}
257-
<br />
258-
<Button
259-
variant="hyperlink"
260-
href="https://github.com/kimmknight/raweb/wiki/Trusting-the-RAWeb-server-(Fix-security-error-5003)"
261-
style="margin-left: -11px; margin-bottom: -6px"
262-
>
263-
{{ $t('securityError503.action') }}
264-
</Button>
265-
</InfoBar>
325+
{{ alert.linkText }}
326+
</Button>
327+
</template>
328+
</InfoBar>
329+
330+
<div id="page">
331+
<template v-if="data">
266332
<Favorites :data v-if="hash === '#favorites'" />
267333
<Devices :data v-else-if="hash === '#devices'" />
268334
<Apps :data v-else-if="hash === '#apps'" />
@@ -292,24 +358,38 @@
292358

293359
<style scoped>
294360
main {
295-
flex: 1;
296-
height: auto;
361+
flex-grow: 1;
362+
flex-shrink: 1;
363+
flex-basis: 0%;
364+
365+
height: var(--content-height);
297366
overflow: auto;
298367
background-color: var(--wui-solid-background-tertiary);
299368
box-sizing: border-box;
300369
border-radius: var(--wui-overlay-corner-radius) 0 0 0;
370+
371+
display: flex;
372+
flex-direction: column;
301373
}
302374
main.simple {
303375
border-radius: 0;
304376
}
305377
306-
main > div {
378+
main > div#page {
307379
--padding: 36px;
308380
padding: var(--padding);
309381
width: 100%;
310-
height: var(--content-height);
311382
box-sizing: border-box;
312383
view-transition-name: main;
384+
flex-grow: 1;
385+
flex-shrink: 1;
386+
}
387+
388+
:deep(.global-alert) {
389+
border-radius: 0 !important;
390+
}
391+
:deep(.global-alert .info-bar-content p) {
392+
flex-basis: 100%;
313393
}
314394
</style>
315395

frontend/lib/components/ContentDialog/ContentDialog.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
<script setup lang="ts">
22
import TextBlock from '$components/TextBlock/TextBlock.vue';
33
import { PreventableEvent } from '$utils';
4-
import { ref, useTemplateRef, watch } from 'vue';
4+
import { ref, useAttrs, useTemplateRef, watch } from 'vue';
55
66
const {
77
closeOnEscape = true,
88
closeOnBackdropClick = true,
99
title,
1010
size = 'standard',
11-
...restProps
1211
} = defineProps<{
1312
closeOnEscape?: boolean;
1413
closeOnBackdropClick?: boolean;
1514
title?: string;
1615
size?: 'min' | 'standard' | 'max';
1716
}>();
17+
const restProps = useAttrs();
1818
1919
const emit = defineEmits<{
2020
(e: 'beforeClose'): void;
@@ -107,6 +107,8 @@
107107
},
108108
{ immediate: true }
109109
);
110+
111+
console.log(restProps);
110112
</script>
111113

112114
<template>

frontend/lib/components/InfoBar/InfoBar.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,12 @@
7373
padding-inline-start: 15px;
7474
position: relative;
7575
user-select: none;
76+
flex-grow: 0;
77+
flex-shrink: 0;
7678
}
7779
7880
.info-bar.severity-information {
79-
background-color: var(--wui-card-secondary-background);
81+
background-color: var(--wui-card-background-secondary);
8082
}
8183
.info-bar.severity-attention {
8284
background-color: var(--wui-system-attention-background);

frontend/lib/components/PolicyDialog/PolicyDialog.vue

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
interface ExtraFieldSpecCore {
99
key: string;
1010
label?: string;
11-
type: 'key-value' | 'string';
11+
type: 'key-value' | 'string' | 'json';
1212
multiple?: boolean;
13-
keyValueLabels?: [string, string];
13+
keyValueLabels?: string[];
14+
/**
15+
* An object indicating the field ids and display labels for the json type.
16+
* If the value is a tuple, the first element is the label and the second is
17+
* a selection of options that will be presented as a dropdown.
18+
*/
19+
jsonFields?: Record<string, string | [string, string]>;
1420
}
1521
1622
interface ExtraFieldSpecSingle extends ExtraFieldSpecCore {
@@ -20,7 +26,7 @@
2026
2127
interface ExtraFieldSpecMultiple extends ExtraFieldSpecCore {
2228
multiple: true;
23-
interpret: (value: string) => [string, string][];
29+
interpret: (value: string) => [string, string][] | Record<string, string>[];
2430
}
2531
2632
type ExtraFieldSpec = ExtraFieldSpecSingle | ExtraFieldSpecMultiple;
@@ -43,7 +49,7 @@
4349
}
4450
});
4551
46-
const extraFieldsState = ref<Record<string, string | [string, string][]>>({});
52+
const extraFieldsState = ref<Record<string, string | [string, string][] | Record<string, string>[]>>({});
4753
watchEffect(() => {
4854
extraFields?.forEach((field) => {
4955
extraFieldsState.value[field.key] = field.interpret?.(stringValue || '') ?? (stringValue || '');
@@ -55,7 +61,7 @@
5561
e: 'save',
5662
closeDialog: () => void,
5763
state: boolean | null,
58-
extra?: Record<string, string | [string, string][]>
64+
extra?: Record<string, string | [string, string][] | Record<string, string>[]>
5965
): void;
6066
}>();
6167
@@ -91,6 +97,15 @@
9197
});
9298
}
9399
}
100+
101+
// helper that narrows to the object[] case
102+
function getJsonFieldArray(key: string): Record<string, string>[] {
103+
const vals = extraFieldsState.value[key];
104+
if (Array.isArray(vals) && vals.length > 0 && typeof vals[0] === 'object') {
105+
return vals as Record<string, string>[];
106+
}
107+
return [];
108+
}
94109
</script>
95110

96111
<template>
@@ -184,7 +199,11 @@
184199
@click="
185200
() => {
186201
const values = extraFieldsState[field.key];
187-
if (field.multiple && Array.isArray(values)) {
202+
if (
203+
field.multiple &&
204+
Array.isArray(values) &&
205+
values.every((val) => Array.isArray(val))
206+
) {
188207
values.push(['', '']);
189208
}
190209
}
@@ -205,6 +224,85 @@
205224
</div>
206225
</template>
207226
</template>
227+
228+
<template v-if="field.type === 'json'">
229+
<template v-if="field.multiple && extraFieldsState[field.key]">
230+
<fieldset v-for="(value, index) in getJsonFieldArray(field.key)" :key="index">
231+
<IconButton
232+
v-if="state === 'enabled'"
233+
@click="
234+
() => {
235+
const values = extraFieldsState[field.key];
236+
if (field.multiple && Array.isArray(values)) {
237+
values.splice(index, 1);
238+
}
239+
}
240+
"
241+
class="remove-button"
242+
>
243+
<svg
244+
width="24"
245+
height="24"
246+
fill="none"
247+
viewBox="0 0 24 24"
248+
xmlns="http://www.w3.org/2000/svg"
249+
>
250+
<path
251+
d="M12 1.75a3.25 3.25 0 0 1 3.245 3.066L15.25 5h5.25a.75.75 0 0 1 .102 1.493L20.5 6.5h-.796l-1.28 13.02a2.75 2.75 0 0 1-2.561 2.474l-.176.006H8.313a2.75 2.75 0 0 1-2.714-2.307l-.023-.174L4.295 6.5H3.5a.75.75 0 0 1-.743-.648L2.75 5.75a.75.75 0 0 1 .648-.743L3.5 5h5.25A3.25 3.25 0 0 1 12 1.75Zm6.197 4.75H5.802l1.267 12.872a1.25 1.25 0 0 0 1.117 1.122l.127.006h7.374c.6 0 1.109-.425 1.225-1.002l.02-.126L18.196 6.5ZM13.75 9.25a.75.75 0 0 1 .743.648L14.5 10v7a.75.75 0 0 1-1.493.102L13 17v-7a.75.75 0 0 1 .75-.75Zm-3.5 0a.75.75 0 0 1 .743.648L11 10v7a.75.75 0 0 1-1.493.102L9.5 17v-7a.75.75 0 0 1 .75-.75Zm1.75-6a1.75 1.75 0 0 0-1.744 1.606L10.25 5h3.5A1.75 1.75 0 0 0 12 3.25Z"
252+
fill="currentColor"
253+
/>
254+
</svg>
255+
</IconButton>
256+
<label v-for="[key, label] in Object.entries(field.jsonFields || {})" :key="key">
257+
<TextBlock variant="body" :disabled="state !== 'enabled'">
258+
{{ typeof label === 'string' ? label : label[0] }}
259+
</TextBlock>
260+
<TextBox
261+
v-if="typeof label === 'string'"
262+
v-model:value="value[key]"
263+
:placeholder="field.keyValueLabels?.[0]"
264+
:disabled="state !== 'enabled'"
265+
/>
266+
<select v-else v-model="value[key]" :disabled="state !== 'enabled'">
267+
<option value="" disabled hidden>Select...</option>
268+
<option v-for="option in label[1].split('|')" :key="option" :value="option.trim()">
269+
{{ option.trim() }}
270+
</option>
271+
</select>
272+
</label>
273+
</fieldset>
274+
<div class="extra-fields-actions-row">
275+
<Button
276+
:disabled="state !== 'enabled'"
277+
@click="
278+
() => {
279+
const values = extraFieldsState[field.key];
280+
if (
281+
field.multiple &&
282+
Array.isArray(values) &&
283+
values.every((val): val is Record<string, string> => typeof val === 'object' && !Array.isArray(val))
284+
) {
285+
values.push({});
286+
}
287+
}
288+
"
289+
>
290+
Add new
291+
</Button>
292+
<Button
293+
:disabled="state !== 'enabled' || extraFieldsState[field.key].length === 0"
294+
@click="
295+
() => {
296+
extraFieldsState[field.key] = [];
297+
}
298+
"
299+
>
300+
Clear all
301+
</Button>
302+
</div>
303+
</template>
304+
</template>
305+
208306
<template v-if="field.type === 'string'">
209307
<template v-if="!field.multiple">
210308
<TextBox v-model:value="extraFieldsState[field.key] as string" :disabled="state !== 'enabled'" />
@@ -250,7 +348,11 @@
250348
@click="
251349
() => {
252350
const values = extraFieldsState[field.key];
253-
if (field.multiple && Array.isArray(values)) {
351+
if (
352+
field.multiple &&
353+
Array.isArray(values) &&
354+
values.every((val) => Array.isArray(val))
355+
) {
254356
values.push(['', '']);
255357
}
256358
}

0 commit comments

Comments
 (0)