Fix widgets
This commit is contained in:
parent
1bd300e497
commit
20e0d655fb
10
Dockerfile
10
Dockerfile
@ -8,9 +8,11 @@ WORKDIR /app
|
||||
COPY --link package.json pnpm-lock.yaml* ./
|
||||
|
||||
SHELL ["/bin/ash", "-xeo", "pipefail", "-c"]
|
||||
RUN nslookup www.google.com
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories
|
||||
RUN apk add --no-cache libc6-compat \
|
||||
&& apk add --no-cache --virtual .gyp python3 make g++ \
|
||||
&& npm install -g pnpm
|
||||
&& apk add --no-cache --virtual .gyp python3 make g++ \
|
||||
&& npm install -g pnpm
|
||||
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm fetch | grep -v "cross-device link not permitted\|Falling back to copying packages from store"
|
||||
|
||||
@ -29,8 +31,8 @@ COPY . .
|
||||
|
||||
SHELL ["/bin/ash", "-xeo", "pipefail", "-c"]
|
||||
RUN npm run telemetry \
|
||||
&& mkdir config \
|
||||
&& NEXT_PUBLIC_BUILDTIME=$BUILDTIME NEXT_PUBLIC_VERSION=$VERSION NEXT_PUBLIC_REVISION=$REVISION npm run build
|
||||
&& mkdir config \
|
||||
&& NEXT_PUBLIC_BUILDTIME=$BUILDTIME NEXT_PUBLIC_VERSION=$VERSION NEXT_PUBLIC_REVISION=$REVISION npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM docker.io/node:18-alpine AS runner
|
||||
|
||||
@ -26,7 +26,7 @@ export default function List({ group, services, layout, isGroup = false }) {
|
||||
service.type === "grouped-service" ? (
|
||||
<List
|
||||
key={service.name}
|
||||
group={service.name}
|
||||
group={group}
|
||||
services={service.services}
|
||||
layout={{ columns: parseInt(service.name, 10) || service.services.length, style: "row" }}
|
||||
isGroup
|
||||
|
||||
@ -177,7 +177,14 @@ function Home({ initialSettings }) {
|
||||
const { data: bookmarks } = useSWR("/api/bookmarks");
|
||||
const { data: widgets } = useSWR("/api/widgets");
|
||||
|
||||
const servicesAndBookmarks = [...services.map(sg => sg.services).flat(), ...bookmarks.map(bg => bg.bookmarks).flat()]
|
||||
const servicesAndBookmarks = [
|
||||
...services
|
||||
.map((sg) => sg.services)
|
||||
.flat(1)
|
||||
.map((service) => (service.type === "grouped-service" ? service.services : service))
|
||||
.flat(1),
|
||||
...bookmarks.map((bg) => bg.bookmarks).flat(),
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (settings.language) {
|
||||
@ -196,15 +203,15 @@ function Home({ initialSettings }) {
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchString, setSearchString] = useState("");
|
||||
let searchProvider = null;
|
||||
const searchWidget = Object.values(widgets).find(w => w.type === "search");
|
||||
const searchWidget = Object.values(widgets).find((w) => w.type === "search");
|
||||
if (searchWidget) {
|
||||
if (Array.isArray(searchWidget.options?.provider)) {
|
||||
// if search provider is a list, try to retrieve from localstorage, fall back to the first
|
||||
searchProvider = getStoredProvider() ?? searchProviders[searchWidget.options.provider[0]];
|
||||
} else if (searchWidget.options?.provider === 'custom') {
|
||||
} else if (searchWidget.options?.provider === "custom") {
|
||||
searchProvider = {
|
||||
url: searchWidget.options.url
|
||||
}
|
||||
url: searchWidget.options.url,
|
||||
};
|
||||
} else {
|
||||
searchProvider = searchProviders[searchWidget.options?.provider];
|
||||
}
|
||||
@ -223,12 +230,12 @@ function Home({ initialSettings }) {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return function cleanup() {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
})
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -255,12 +262,7 @@ function Home({ initialSettings }) {
|
||||
<meta name="theme-color" content={themes[initialSettings.color || "slate"][initialSettings.theme || "dark"]} />
|
||||
</Head>
|
||||
<div className="relative container m-auto flex flex-col justify-start z-10 h-full">
|
||||
<div
|
||||
className={classNames(
|
||||
"flex flex-row flex-wrap justify-between",
|
||||
headerStyles[headerStyle]
|
||||
)}
|
||||
>
|
||||
<div className={classNames("flex flex-row flex-wrap justify-between", headerStyles[headerStyle])}>
|
||||
<QuickLaunch
|
||||
servicesAndBookmarks={servicesAndBookmarks}
|
||||
searchString={searchString}
|
||||
@ -274,17 +276,19 @@ function Home({ initialSettings }) {
|
||||
{widgets
|
||||
.filter((widget) => !rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: false}} />
|
||||
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: false }} />
|
||||
))}
|
||||
|
||||
<div className={classNames(
|
||||
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end",
|
||||
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2"
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end",
|
||||
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2"
|
||||
)}
|
||||
>
|
||||
{widgets
|
||||
.filter((widget) => rightAlignedWidgets.includes(widget.type))
|
||||
.map((widget, i) => (
|
||||
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: true}} />
|
||||
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: true }} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
@ -294,24 +298,27 @@ function Home({ initialSettings }) {
|
||||
{services?.length > 0 && (
|
||||
<div className="flex flex-wrap p-4 sm:p-8 sm:pt-4 items-start pb-2">
|
||||
{services.map((group) => (
|
||||
<ServicesGroup
|
||||
<ServicesGroup
|
||||
key={group.name}
|
||||
group={group.name}
|
||||
services={group}
|
||||
layout={initialSettings.layout?.[group.name]}
|
||||
fiveColumns={settings.fiveColumns}
|
||||
disableCollapse={settings.disableCollapse} />
|
||||
fiveColumns={settings.fiveColumns}
|
||||
disableCollapse={settings.disableCollapse}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bookmarks?.length > 0 && (
|
||||
<div className={`grow flex flex-wrap pt-0 p-4 sm:p-8 gap-2 grid-cols-1 lg:grid-cols-2 lg:grid-cols-${Math.min(6, bookmarks.length)}`}>
|
||||
<div
|
||||
className={`grow flex flex-wrap pt-0 p-4 sm:p-8 gap-2 grid-cols-1 lg:grid-cols-2 lg:grid-cols-${Math.min(
|
||||
6,
|
||||
bookmarks.length
|
||||
)}`}
|
||||
>
|
||||
{bookmarks.map((group) => (
|
||||
<BookmarksGroup
|
||||
key={group.name}
|
||||
group={group}
|
||||
disableCollapse={settings.disableCollapse} />
|
||||
<BookmarksGroup key={group.name} group={group} disableCollapse={settings.disableCollapse} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -323,9 +330,7 @@ function Home({ initialSettings }) {
|
||||
{!initialSettings?.theme && <ThemeToggle />}
|
||||
</div>
|
||||
|
||||
<div className="flex mt-4 w-full justify-end">
|
||||
{!initialSettings?.hideVersion && <Version />}
|
||||
</div>
|
||||
<div className="flex mt-4 w-full justify-end">{!initialSettings?.hideVersion && <Version />}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -340,7 +345,7 @@ export default function Wrapper({ initialSettings, fallback }) {
|
||||
if (initialSettings && initialSettings.background) {
|
||||
let opacity = initialSettings.backgroundOpacity ?? 1;
|
||||
let backgroundImage = initialSettings.background;
|
||||
if (typeof initialSettings.background === 'object') {
|
||||
if (typeof initialSettings.background === "object") {
|
||||
backgroundImage = initialSettings.background.image;
|
||||
backgroundBlur = initialSettings.background.blur !== undefined;
|
||||
backgroundSaturate = initialSettings.background.saturate !== undefined;
|
||||
@ -373,13 +378,15 @@ export default function Wrapper({ initialSettings, fallback }) {
|
||||
style={wrappedStyle}
|
||||
>
|
||||
<div
|
||||
id="inner_wrapper"
|
||||
className={classNames(
|
||||
'fixed overflow-auto w-full h-full',
|
||||
backgroundBlur && `backdrop-blur${initialSettings.background.blur.length ? '-' : ""}${initialSettings.background.blur}`,
|
||||
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
|
||||
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`,
|
||||
)}>
|
||||
id="inner_wrapper"
|
||||
className={classNames(
|
||||
"fixed overflow-auto w-full h-full",
|
||||
backgroundBlur &&
|
||||
`backdrop-blur${initialSettings.background.blur.length ? "-" : ""}${initialSettings.background.blur}`,
|
||||
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
|
||||
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`
|
||||
)}
|
||||
>
|
||||
<Index initialSettings={initialSettings} fallback={fallback} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -25,35 +25,34 @@ export async function servicesFromConfig() {
|
||||
return [];
|
||||
}
|
||||
// map easy to write YAML objects into easy to consume JS arrays
|
||||
|
||||
const servicesArray = services.map((servicesGroup) => ({
|
||||
name: Object.keys(servicesGroup)[0],
|
||||
services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) => {
|
||||
if (Array.isArray(entries[Object.keys(entries)[0]]))
|
||||
return {
|
||||
name: Object.keys(entries)[0],
|
||||
services: entries[Object.keys(entries)[0]].map((entry) => ({
|
||||
name: Object.keys(entry)[0],
|
||||
...entry[Object.keys(entry)[0]],
|
||||
services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) =>
|
||||
Array.isArray(entries[Object.keys(entries)[0]])
|
||||
? {
|
||||
name: Object.keys(entries)[0],
|
||||
services: entries[Object.keys(entries)[0]].map((entry) => ({
|
||||
name: Object.keys(entry)[0],
|
||||
...entry[Object.keys(entry)[0]],
|
||||
type: "service",
|
||||
})),
|
||||
type: "grouped-service",
|
||||
}
|
||||
: {
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]],
|
||||
type: "service",
|
||||
})),
|
||||
type: "grouped-service",
|
||||
};
|
||||
|
||||
return {
|
||||
name: Object.keys(entries)[0],
|
||||
...entries[Object.keys(entries)[0]],
|
||||
type: "service",
|
||||
};
|
||||
}),
|
||||
}
|
||||
),
|
||||
}));
|
||||
|
||||
// add default weight to services based on their position in the configuration
|
||||
servicesArray.forEach((group, groupIndex) => {
|
||||
group.services.forEach((service, serviceIndex) => {
|
||||
if (!service.weight && service.type !== "grouped-service") {
|
||||
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
|
||||
}
|
||||
if (!service.weight) servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
|
||||
|
||||
if (service.type === "grouped-service") {
|
||||
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
|
||||
service.services.forEach((groupedService, groupedServiceIndex) => {
|
||||
if (!groupedService.weight) {
|
||||
servicesArray[groupIndex].services[serviceIndex].services[groupedServiceIndex].weight =
|
||||
@ -297,97 +296,102 @@ export async function servicesFromKubernetes() {
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanService(service, serviceGroup) {
|
||||
const cleanedService =
|
||||
service.type === "grouped-service"
|
||||
? { ...service, services: service.services.map((s) => cleanService(s, serviceGroup)) }
|
||||
: { ...service };
|
||||
if (cleanedService.showStats !== undefined) cleanedService.showStats = JSON.parse(cleanedService.showStats);
|
||||
if (typeof service.weight === "string") {
|
||||
const weight = parseInt(service.weight, 10);
|
||||
if (Number.isNaN(weight)) {
|
||||
cleanedService.weight = 0;
|
||||
} else {
|
||||
cleanedService.weight = weight;
|
||||
}
|
||||
}
|
||||
if (typeof cleanedService.weight !== "number") {
|
||||
cleanedService.weight = 0;
|
||||
}
|
||||
|
||||
if (cleanedService.widget) {
|
||||
// whitelisted set of keys to pass to the frontend
|
||||
const {
|
||||
type, // all widgets
|
||||
fields,
|
||||
hideErrors,
|
||||
server, // docker widget
|
||||
container,
|
||||
currency, // coinmarketcap widget
|
||||
symbols,
|
||||
defaultinterval,
|
||||
site, // unifi widget
|
||||
namespace, // kubernetes widget
|
||||
app,
|
||||
podSelector,
|
||||
wan, // opnsense widget, pfsense widget
|
||||
enableBlocks, // emby/jellyfin
|
||||
enableNowPlaying,
|
||||
volume, // diskstation widget,
|
||||
enableQueue, // sonarr/radarr
|
||||
} = cleanedService.widget;
|
||||
|
||||
let fieldsList = fields;
|
||||
if (typeof fields === "string") {
|
||||
try {
|
||||
JSON.parse(fields);
|
||||
} catch (e) {
|
||||
logger.error("Invalid fields list detected in config for service '%s'", service.name);
|
||||
fieldsList = null;
|
||||
}
|
||||
}
|
||||
|
||||
cleanedService.widget = {
|
||||
type,
|
||||
fields: fieldsList || null,
|
||||
hide_errors: hideErrors || false,
|
||||
service_name: service.name,
|
||||
service_group: serviceGroup.name,
|
||||
};
|
||||
|
||||
if (currency) cleanedService.widget.currency = currency;
|
||||
if (symbols) cleanedService.widget.symbols = symbols;
|
||||
if (defaultinterval) cleanedService.widget.defaultinterval = defaultinterval;
|
||||
|
||||
if (type === "docker") {
|
||||
if (server) cleanedService.widget.server = server;
|
||||
if (container) cleanedService.widget.container = container;
|
||||
}
|
||||
if (type === "unifi") {
|
||||
if (site) cleanedService.widget.site = site;
|
||||
}
|
||||
if (type === "kubernetes") {
|
||||
if (namespace) cleanedService.widget.namespace = namespace;
|
||||
if (app) cleanedService.widget.app = app;
|
||||
if (podSelector) cleanedService.widget.podSelector = podSelector;
|
||||
}
|
||||
if (["opnsense", "pfsense"].includes(type)) {
|
||||
if (wan) cleanedService.widget.wan = wan;
|
||||
}
|
||||
if (["emby", "jellyfin"].includes(type)) {
|
||||
if (enableBlocks !== undefined) cleanedService.widget.enableBlocks = JSON.parse(enableBlocks);
|
||||
if (enableNowPlaying !== undefined) cleanedService.widget.enableNowPlaying = JSON.parse(enableNowPlaying);
|
||||
}
|
||||
if (["sonarr", "radarr"].includes(type)) {
|
||||
if (enableQueue !== undefined) cleanedService.widget.enableQueue = JSON.parse(enableQueue);
|
||||
}
|
||||
if (["diskstation", "qnap"].includes(type)) {
|
||||
if (volume) cleanedService.widget.volume = volume;
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedService;
|
||||
}
|
||||
|
||||
export function cleanServiceGroups(groups) {
|
||||
return groups.map((serviceGroup) => ({
|
||||
name: serviceGroup.name,
|
||||
services: serviceGroup.services.map((service) => {
|
||||
const cleanedService = { ...service };
|
||||
if (cleanedService.showStats !== undefined) cleanedService.showStats = JSON.parse(cleanedService.showStats);
|
||||
if (typeof service.weight === "string") {
|
||||
const weight = parseInt(service.weight, 10);
|
||||
if (Number.isNaN(weight)) {
|
||||
cleanedService.weight = 0;
|
||||
} else {
|
||||
cleanedService.weight = weight;
|
||||
}
|
||||
}
|
||||
if (typeof cleanedService.weight !== "number") {
|
||||
cleanedService.weight = 0;
|
||||
}
|
||||
|
||||
if (cleanedService.widget) {
|
||||
// whitelisted set of keys to pass to the frontend
|
||||
const {
|
||||
type, // all widgets
|
||||
fields,
|
||||
hideErrors,
|
||||
server, // docker widget
|
||||
container,
|
||||
currency, // coinmarketcap widget
|
||||
symbols,
|
||||
defaultinterval,
|
||||
site, // unifi widget
|
||||
namespace, // kubernetes widget
|
||||
app,
|
||||
podSelector,
|
||||
wan, // opnsense widget, pfsense widget
|
||||
enableBlocks, // emby/jellyfin
|
||||
enableNowPlaying,
|
||||
volume, // diskstation widget,
|
||||
enableQueue, // sonarr/radarr
|
||||
} = cleanedService.widget;
|
||||
|
||||
let fieldsList = fields;
|
||||
if (typeof fields === "string") {
|
||||
try {
|
||||
JSON.parse(fields);
|
||||
} catch (e) {
|
||||
logger.error("Invalid fields list detected in config for service '%s'", service.name);
|
||||
fieldsList = null;
|
||||
}
|
||||
}
|
||||
|
||||
cleanedService.widget = {
|
||||
type,
|
||||
fields: fieldsList || null,
|
||||
hide_errors: hideErrors || false,
|
||||
service_name: service.name,
|
||||
service_group: serviceGroup.name,
|
||||
};
|
||||
|
||||
if (currency) cleanedService.widget.currency = currency;
|
||||
if (symbols) cleanedService.widget.symbols = symbols;
|
||||
if (defaultinterval) cleanedService.widget.defaultinterval = defaultinterval;
|
||||
|
||||
if (type === "docker") {
|
||||
if (server) cleanedService.widget.server = server;
|
||||
if (container) cleanedService.widget.container = container;
|
||||
}
|
||||
if (type === "unifi") {
|
||||
if (site) cleanedService.widget.site = site;
|
||||
}
|
||||
if (type === "kubernetes") {
|
||||
if (namespace) cleanedService.widget.namespace = namespace;
|
||||
if (app) cleanedService.widget.app = app;
|
||||
if (podSelector) cleanedService.widget.podSelector = podSelector;
|
||||
}
|
||||
if (["opnsense", "pfsense"].includes(type)) {
|
||||
if (wan) cleanedService.widget.wan = wan;
|
||||
}
|
||||
if (["emby", "jellyfin"].includes(type)) {
|
||||
if (enableBlocks !== undefined) cleanedService.widget.enableBlocks = JSON.parse(enableBlocks);
|
||||
if (enableNowPlaying !== undefined) cleanedService.widget.enableNowPlaying = JSON.parse(enableNowPlaying);
|
||||
}
|
||||
if (["sonarr", "radarr"].includes(type)) {
|
||||
if (enableQueue !== undefined) cleanedService.widget.enableQueue = JSON.parse(enableQueue);
|
||||
}
|
||||
if (["diskstation", "qnap"].includes(type)) {
|
||||
if (volume) cleanedService.widget.volume = volume;
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedService;
|
||||
}),
|
||||
services: serviceGroup.services.map((service) => cleanService(service, serviceGroup)),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -396,7 +400,10 @@ export async function getServiceItem(group, service) {
|
||||
|
||||
const serviceGroup = configuredServices.find((g) => g.name === group);
|
||||
if (serviceGroup) {
|
||||
const serviceEntry = serviceGroup.services.find((s) => s.name === service);
|
||||
const serviceEntry = serviceGroup.services
|
||||
.map((s) => (s.type === "grouped-service" ? s.services : s))
|
||||
.flat(1)
|
||||
.find((s) => s.name === service);
|
||||
if (serviceEntry) return serviceEntry;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user