Fix widgets

This commit is contained in:
lukylix 2023-07-07 01:09:07 +02:00
parent 1bd300e497
commit 20e0d655fb
5 changed files with 3527 additions and 155 deletions

View File

@ -8,9 +8,11 @@ WORKDIR /app
COPY --link package.json pnpm-lock.yaml* ./ COPY --link package.json pnpm-lock.yaml* ./
SHELL ["/bin/ash", "-xeo", "pipefail", "-c"] 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 \ RUN apk add --no-cache libc6-compat \
&& apk add --no-cache --virtual .gyp python3 make g++ \ && apk add --no-cache --virtual .gyp python3 make g++ \
&& npm install -g pnpm && 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" 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"] SHELL ["/bin/ash", "-xeo", "pipefail", "-c"]
RUN npm run telemetry \ RUN npm run telemetry \
&& mkdir config \ && mkdir config \
&& NEXT_PUBLIC_BUILDTIME=$BUILDTIME NEXT_PUBLIC_VERSION=$VERSION NEXT_PUBLIC_REVISION=$REVISION npm run build && NEXT_PUBLIC_BUILDTIME=$BUILDTIME NEXT_PUBLIC_VERSION=$VERSION NEXT_PUBLIC_REVISION=$REVISION npm run build
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM docker.io/node:18-alpine AS runner FROM docker.io/node:18-alpine AS runner

View File

@ -26,7 +26,7 @@ export default function List({ group, services, layout, isGroup = false }) {
service.type === "grouped-service" ? ( service.type === "grouped-service" ? (
<List <List
key={service.name} key={service.name}
group={service.name} group={group}
services={service.services} services={service.services}
layout={{ columns: parseInt(service.name, 10) || service.services.length, style: "row" }} layout={{ columns: parseInt(service.name, 10) || service.services.length, style: "row" }}
isGroup isGroup

View File

@ -177,7 +177,14 @@ function Home({ initialSettings }) {
const { data: bookmarks } = useSWR("/api/bookmarks"); const { data: bookmarks } = useSWR("/api/bookmarks");
const { data: widgets } = useSWR("/api/widgets"); 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(() => { useEffect(() => {
if (settings.language) { if (settings.language) {
@ -196,15 +203,15 @@ function Home({ initialSettings }) {
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [searchString, setSearchString] = useState(""); const [searchString, setSearchString] = useState("");
let searchProvider = null; 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 (searchWidget) {
if (Array.isArray(searchWidget.options?.provider)) { if (Array.isArray(searchWidget.options?.provider)) {
// if search provider is a list, try to retrieve from localstorage, fall back to the first // if search provider is a list, try to retrieve from localstorage, fall back to the first
searchProvider = getStoredProvider() ?? searchProviders[searchWidget.options.provider[0]]; searchProvider = getStoredProvider() ?? searchProviders[searchWidget.options.provider[0]];
} else if (searchWidget.options?.provider === 'custom') { } else if (searchWidget.options?.provider === "custom") {
searchProvider = { searchProvider = {
url: searchWidget.options.url url: searchWidget.options.url,
} };
} else { } else {
searchProvider = searchProviders[searchWidget.options?.provider]; searchProvider = searchProviders[searchWidget.options?.provider];
} }
@ -223,12 +230,12 @@ function Home({ initialSettings }) {
} }
} }
document.addEventListener('keydown', handleKeyDown); document.addEventListener("keydown", handleKeyDown);
return function cleanup() { return function cleanup() {
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener("keydown", handleKeyDown);
} };
}) });
return ( return (
<> <>
@ -255,12 +262,7 @@ function Home({ initialSettings }) {
<meta name="theme-color" content={themes[initialSettings.color || "slate"][initialSettings.theme || "dark"]} /> <meta name="theme-color" content={themes[initialSettings.color || "slate"][initialSettings.theme || "dark"]} />
</Head> </Head>
<div className="relative container m-auto flex flex-col justify-start z-10 h-full"> <div className="relative container m-auto flex flex-col justify-start z-10 h-full">
<div <div className={classNames("flex flex-row flex-wrap justify-between", headerStyles[headerStyle])}>
className={classNames(
"flex flex-row flex-wrap justify-between",
headerStyles[headerStyle]
)}
>
<QuickLaunch <QuickLaunch
servicesAndBookmarks={servicesAndBookmarks} servicesAndBookmarks={servicesAndBookmarks}
searchString={searchString} searchString={searchString}
@ -274,17 +276,19 @@ function Home({ initialSettings }) {
{widgets {widgets
.filter((widget) => !rightAlignedWidgets.includes(widget.type)) .filter((widget) => !rightAlignedWidgets.includes(widget.type))
.map((widget, i) => ( .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( <div
"m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end", className={classNames(
headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2" "m-auto flex flex-wrap grow sm:basis-auto justify-between md:justify-end",
)}> headerStyle === "boxedWidgets" ? "sm:ml-4" : "sm:ml-2"
)}
>
{widgets {widgets
.filter((widget) => rightAlignedWidgets.includes(widget.type)) .filter((widget) => rightAlignedWidgets.includes(widget.type))
.map((widget, i) => ( .map((widget, i) => (
<Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: true}} /> <Widget key={i} widget={widget} style={{ header: headerStyle, isRightAligned: true }} />
))} ))}
</div> </div>
</> </>
@ -300,18 +304,21 @@ function Home({ initialSettings }) {
services={group} services={group}
layout={initialSettings.layout?.[group.name]} layout={initialSettings.layout?.[group.name]}
fiveColumns={settings.fiveColumns} fiveColumns={settings.fiveColumns}
disableCollapse={settings.disableCollapse} /> disableCollapse={settings.disableCollapse}
/>
))} ))}
</div> </div>
)} )}
{bookmarks?.length > 0 && ( {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) => ( {bookmarks.map((group) => (
<BookmarksGroup <BookmarksGroup key={group.name} group={group} disableCollapse={settings.disableCollapse} />
key={group.name}
group={group}
disableCollapse={settings.disableCollapse} />
))} ))}
</div> </div>
)} )}
@ -323,9 +330,7 @@ function Home({ initialSettings }) {
{!initialSettings?.theme && <ThemeToggle />} {!initialSettings?.theme && <ThemeToggle />}
</div> </div>
<div className="flex mt-4 w-full justify-end"> <div className="flex mt-4 w-full justify-end">{!initialSettings?.hideVersion && <Version />}</div>
{!initialSettings?.hideVersion && <Version />}
</div>
</div> </div>
</div> </div>
</> </>
@ -340,7 +345,7 @@ export default function Wrapper({ initialSettings, fallback }) {
if (initialSettings && initialSettings.background) { if (initialSettings && initialSettings.background) {
let opacity = initialSettings.backgroundOpacity ?? 1; let opacity = initialSettings.backgroundOpacity ?? 1;
let backgroundImage = initialSettings.background; let backgroundImage = initialSettings.background;
if (typeof initialSettings.background === 'object') { if (typeof initialSettings.background === "object") {
backgroundImage = initialSettings.background.image; backgroundImage = initialSettings.background.image;
backgroundBlur = initialSettings.background.blur !== undefined; backgroundBlur = initialSettings.background.blur !== undefined;
backgroundSaturate = initialSettings.background.saturate !== undefined; backgroundSaturate = initialSettings.background.saturate !== undefined;
@ -373,13 +378,15 @@ export default function Wrapper({ initialSettings, fallback }) {
style={wrappedStyle} style={wrappedStyle}
> >
<div <div
id="inner_wrapper" id="inner_wrapper"
className={classNames( className={classNames(
'fixed overflow-auto w-full h-full', "fixed overflow-auto w-full h-full",
backgroundBlur && `backdrop-blur${initialSettings.background.blur.length ? '-' : ""}${initialSettings.background.blur}`, backgroundBlur &&
backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`, `backdrop-blur${initialSettings.background.blur.length ? "-" : ""}${initialSettings.background.blur}`,
backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`, backgroundSaturate && `backdrop-saturate-${initialSettings.background.saturate}`,
)}> backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`
)}
>
<Index initialSettings={initialSettings} fallback={fallback} /> <Index initialSettings={initialSettings} fallback={fallback} />
</div> </div>
</div> </div>

View File

@ -25,35 +25,34 @@ export async function servicesFromConfig() {
return []; return [];
} }
// map easy to write YAML objects into easy to consume JS arrays // map easy to write YAML objects into easy to consume JS arrays
const servicesArray = services.map((servicesGroup) => ({ const servicesArray = services.map((servicesGroup) => ({
name: Object.keys(servicesGroup)[0], name: Object.keys(servicesGroup)[0],
services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) => { services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) =>
if (Array.isArray(entries[Object.keys(entries)[0]])) Array.isArray(entries[Object.keys(entries)[0]])
return { ? {
name: Object.keys(entries)[0], name: Object.keys(entries)[0],
services: entries[Object.keys(entries)[0]].map((entry) => ({ services: entries[Object.keys(entries)[0]].map((entry) => ({
name: Object.keys(entry)[0], name: Object.keys(entry)[0],
...entry[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: "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 // add default weight to services based on their position in the configuration
servicesArray.forEach((group, groupIndex) => { servicesArray.forEach((group, groupIndex) => {
group.services.forEach((service, serviceIndex) => { group.services.forEach((service, serviceIndex) => {
if (!service.weight && service.type !== "grouped-service") { if (!service.weight) servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
}
if (service.type === "grouped-service") { if (service.type === "grouped-service") {
servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100;
service.services.forEach((groupedService, groupedServiceIndex) => { service.services.forEach((groupedService, groupedServiceIndex) => {
if (!groupedService.weight) { if (!groupedService.weight) {
servicesArray[groupIndex].services[serviceIndex].services[groupedServiceIndex].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) { export function cleanServiceGroups(groups) {
return groups.map((serviceGroup) => ({ return groups.map((serviceGroup) => ({
name: serviceGroup.name, name: serviceGroup.name,
services: serviceGroup.services.map((service) => { services: serviceGroup.services.map((service) => cleanService(service, serviceGroup)),
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;
}),
})); }));
} }
@ -396,7 +400,10 @@ export async function getServiceItem(group, service) {
const serviceGroup = configuredServices.find((g) => g.name === group); const serviceGroup = configuredServices.find((g) => g.name === group);
if (serviceGroup) { 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; if (serviceEntry) return serviceEntry;
} }

3356
yarn.lock Normal file

File diff suppressed because it is too large Load Diff