Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
331dfe8
Clean commit history from feature/keycloak-middleware
dominiquekleeven May 14, 2025
6c122de
Update pyproject.toml
dominiquekleeven May 14, 2025
128fc3a
Construct valid issuer list based on OR realms rather than pattern ma…
dominiquekleeven May 14, 2025
a4b7b07
Additional comment
dominiquekleeven May 14, 2025
6dfc19d
Increase JWKS cache as its not tied to the expiry interval
dominiquekleeven May 15, 2025
76fc179
Unreachable code
dominiquekleeven May 15, 2025
b4c5c1d
Minor tweaks, use both issuer and kid as cache key for JWKS.
dominiquekleeven May 15, 2025
aea9d25
Added TODO comment with issue link for RBAC improvements
dominiquekleeven May 15, 2025
75c8b76
Additional logging
dominiquekleeven May 15, 2025
2150eec
Fix failing test due to model changes
dominiquekleeven May 15, 2025
ada37e1
Login and logout are promises, should be awaited
dominiquekleeven May 19, 2025
098f611
Update app-layout.ts
dominiquekleeven May 19, 2025
a923798
Proxy service was unnecessary, we can just override the OpenRemoteCl…
dominiquekleeven May 19, 2025
66fb3d1
Use vaadin router commands for redirect rather than using the router …
dominiquekleeven May 20, 2025
4ccfd65
Don't expect an optional property in the token payload
dominiquekleeven May 21, 2025
8f075b0
Update pages-config-editor.ts
dominiquekleeven May 21, 2025
a2e0086
Increase timeout for HTTPX to accomodate slower requests
dominiquekleeven May 21, 2025
365f31b
Fix timeout on proxy client
dominiquekleeven May 21, 2025
a3c95dc
Update openremote_proxy_client.py
dominiquekleeven May 21, 2025
fced7b4
Minor refactor to OR client to properly support realm users
dominiquekleeven May 22, 2025
bb6a3fd
Fix exception on retrieving realm config when realm isn't in the mana…
dominiquekleeven Jun 10, 2025
11be849
Update frontend/src/services/api-service.ts
dominiquekleeven Jul 4, 2025
e6d4e38
Address misleading function name, uses a more explicit name now
dominiquekleeven Jul 4, 2025
944cb0d
Merge branch 'main' into feature/keycloak-middleware-clean
dominiquekleeven Jul 4, 2025
a8fbd08
Use ISO8601 period for training dataset (regressors + target), added …
dominiquekleeven Jul 14, 2025
3819b3d
Fix prettier formatting issue
dominiquekleeven Jul 14, 2025
0094bac
Directly call manager API in front-end rather than proxying requests …
dominiquekleeven Jul 16, 2025
dc52829
Fix front-end linter error
dominiquekleeven Jul 16, 2025
0dd0e30
Accomodate new front-end env variable (ML_OR_URL)
dominiquekleeven Jul 18, 2025
b3497f1
Update docker-compose.yml
dominiquekleeven Jul 18, 2025
fc2332e
Minor adjustment to env handling front-end
dominiquekleeven Jul 21, 2025
30c9ecb
Remove isEmbedded function since it is unused
dominiquekleeven Jul 28, 2025
c6bd020
Keycloak middleware refactor and decorator additions
dominiquekleeven Jul 28, 2025
44bc47f
Mock the issuer provider for the keycloak middleware
dominiquekleeven Jul 28, 2025
efcbec6
Clearer decorator name for realm access
dominiquekleeven Jul 29, 2025
f65ec6e
Resolve potential data cut off in training data consolidation
dominiquekleeven Jul 30, 2025
b254152
Tests for training data request batching and time operations/data pre…
dominiquekleeven Jul 30, 2025
cc07c2e
Fix openremote client mocked method signatures and missing types for …
dominiquekleeven Jul 30, 2025
5340f11
Increase grace period for job scheduling, job overlap could cause a j…
dominiquekleeven Jul 30, 2025
e0c1f74
Update model_scheduler.py
dominiquekleeven Jul 30, 2025
1e56000
Replace auth boilerplate by re-using manager.init which handles auto …
dominiquekleeven Aug 1, 2025
945b258
Remove keycloak-js, since it is no longer needed
dominiquekleeven Aug 1, 2025
0f9f434
Enhanced OpenRemoteClient to allow specifying the default realm rathe…
dominiquekleeven Aug 7, 2025
e366a92
Update index.ts
dominiquekleeven Aug 8, 2025
2f55344
Default config change
dominiquekleeven Aug 8, 2025
c21a9b2
Revert "Default config change"
dominiquekleeven Aug 8, 2025
40ed5be
Fallback to relative url for manager config
dominiquekleeven Aug 8, 2025
a316e9d
Performance improvement, allow functions to run in parallel rather th…
dominiquekleeven Aug 11, 2025
4a18dfe
Remove the logging prefix override, and clean up regex for bundle pat…
dominiquekleeven Aug 15, 2025
21a5484
Format
dominiquekleeven Aug 15, 2025
6b4e2fc
Unnecessary since these tests were covered by the forecast tests them…
dominiquekleeven Aug 20, 2025
22aea98
Only include the roles that are actually usable for services
dominiquekleeven Sep 3, 2025
880395c
Fix failing test and format
dominiquekleeven Sep 3, 2025
ff8a0fb
Update route_exception_handlers.py
dominiquekleeven Sep 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ COPY frontend/ ./
# Define build arguments
ARG ML_SERVICE_URL
ARG ML_WEB_ROOT_PATH
ARG ML_OR_KEYCLOAK_URL
ARG ML_OR_URL

RUN ML_SERVICE_URL=${ML_SERVICE_URL:-/services/ml-forecast} \
ML_WEB_ROOT_PATH=${ML_WEB_ROOT_PATH:-/services/ml-forecast/ui} \
ML_OR_KEYCLOAK_URL=${ML_OR_KEYCLOAK_URL:-/auth} \
ML_OR_URL=${ML_OR_URL:-} \
npm run build:prod

# --- Python Build Phase -------------------------------------------------------
Expand Down
20 changes: 11 additions & 9 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ services:
context: ..
dockerfile: docker/Dockerfile
args:
ML_SERVICE_URL: ${ML_SERVICE_URL:-/services/ml-forecast} # Url to reach the back-end service, should be the same as ML_API_ROOT_PATH
ML_WEB_ROOT_PATH: ${ML_WEB_ROOT_PATH:-/services/ml-forecast/ui} # Public path for the front-end (e.g. when behind a reverse proxy)
ML_OR_URL: ${ML_OR_URL} # OpenRemote URL
ML_OR_KEYCLOAK_URL: ${ML_OR_KEYCLOAK_URL} # OpenRemote Keycloak URL
ML_SERVICE_URL: ${ML_SERVICE_URL} # Url to reach the back-end service, should be the same as ML_API_ROOT_PATH
ML_WEB_ROOT_PATH: ${ML_WEB_ROOT_PATH} # Public path for the front-end (e.g. when behind a reverse proxy)
container_name: service-ml-forecast
ports:
- "8000:8000"
environment:
- ML_LOG_LEVEL=${ML_LOG_LEVEL:-INFO} # Log level to use
- ML_ENVIRONMENT=${ML_ENVIRONMENT:-production} # Environment to run the service in
- ML_API_ROOT_PATH=${ML_API_ROOT_PATH:-/services/ml-forecast} # Public path for the back-end (e.g. when behind a reverse proxy)
- ML_OR_URL=${ML_OR_URL:-http://host.docker.internal:8080} # OpenRemote URL
- ML_OR_KEYCLOAK_URL=${ML_OR_KEYCLOAK_URL:-http://host.docker.internal:8081} # OpenRemote Keycloak URL
- ML_OR_SERVICE_USER=${ML_OR_SERVICE_USER:-serviceuser} # OpenRemote service user
- ML_OR_SERVICE_USER_SECRET=${ML_OR_SERVICE_USER_SECRET:-secret} # OpenRemote service user secret
- ML_LOG_LEVEL=${ML_LOG_LEVEL} # Log level to use
- ML_ENVIRONMENT=${ML_ENVIRONMENT} # Environment to run the service in
- ML_API_ROOT_PATH=${ML_API_ROOT_PATH} # Public path for the back-end (e.g. when behind a reverse proxy)
- ML_OR_URL=${ML_OR_URL} # OpenRemote URL
- ML_OR_KEYCLOAK_URL=${ML_OR_KEYCLOAK_URL} # OpenRemote Keycloak URL
- ML_OR_SERVICE_USER=${ML_OR_SERVICE_USER} # OpenRemote service user
- ML_OR_SERVICE_USER_SECRET=${ML_OR_SERVICE_USER_SECRET} # OpenRemote service user secret
volumes:
# Model storage
- ../deployment/data/models:/app/deployment/data/models
Expand Down
4 changes: 4 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Prevent index.html from being cached -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="icon" href="<%= templateRootPath %>/assets/images/logo.svg" />
<style>
/* Global styles */
Expand Down
20 changes: 10 additions & 10 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"type": "module",
"scripts": {
"serve": "cross-env rspack serve",
"build:dev": "cross-env rspack build --mode development",
"build:prod": "cross-env SERVICE_URL=${SERVICE_URL:-/services/ml-forecast} ML_WEB_ROOT_PATH=${ML_WEB_ROOT_PATH:-/services/ml-forecast/ui} rspack build --mode production",
"build:prod": "cross-env ML_OR_KEYCLOAK_URL=${ML_OR_KEYCLOAK_URL:-/auth} ML_OR_URL=${ML_OR_URL} ML_SERVICE_URL=${ML_SERVICE_URL:-/services/ml-forecast} ML_WEB_ROOT_PATH=${ML_WEB_ROOT_PATH:-/services/ml-forecast/ui} rspack build --mode production",
"build:analyze": "rspack build --mode production --analyze",
"lint": "eslint && prettier . --check",
"format": "prettier . --write"
Expand Down
9 changes: 6 additions & 3 deletions frontend/rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const isProduction = process.env.NODE_ENV === 'production';
const serviceUrl = process.env.ML_SERVICE_URL || 'http://localhost:8000'; // Default to default dev backend
const rootPath = process.env.ML_WEB_ROOT_PATH;
const serviceUrl = process.env.ML_SERVICE_URL || 'http://localhost:8000'; // Default to default service backend
const keycloakUrl = process.env.ML_OR_KEYCLOAK_URL || 'http://localhost:8081/auth'; // Default to openremote keycloak address
const openremoteUrl = process.env.ML_OR_URL !== undefined ? process.env.ML_OR_URL : 'http://localhost:8080'; // Default to openremote url

export default {
mode: isProduction ? 'production' : 'development',
Expand All @@ -20,7 +22,6 @@ export default {
filename: `bundle.[contenthash].js`,
clean: true,
path: path.resolve(__dirname, 'dist'),
// prefix for the bundle, use root path or fallback to '/'
publicPath: rootPath ? rootPath : '/'
},
resolve: {
Expand Down Expand Up @@ -65,7 +66,9 @@ export default {
patterns: [{ from: 'assets', to: 'assets' }]
}),
new rspack.DefinePlugin({
'process.env.ML_SERVICE_URL': JSON.stringify(serviceUrl)
'process.env.ML_SERVICE_URL': JSON.stringify(serviceUrl),
'process.env.ML_OR_KEYCLOAK_URL': JSON.stringify(keycloakUrl),
'process.env.ML_OR_URL': JSON.stringify(openremoteUrl)
})
],
devServer: {
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@
export const APP_OUTLET = document.querySelector('#outlet') as HTMLElement;
export const IS_DEVELOPMENT = process.env.NODE_ENV === 'development';
export const ML_SERVICE_URL = (process.env.ML_SERVICE_URL || '').replace(/\/$/, '');
export const ML_OR_URL = process.env.ML_OR_URL || '';
export const ML_OR_KEYCLOAK_URL = process.env.ML_OR_KEYCLOAK_URL || '';

// Returns true if the app is embedded in an iframe
export const IS_EMBEDDED = window.top !== window.self;
39 changes: 21 additions & 18 deletions frontend/src/common/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,13 @@
import { IconSet, IconSets, OrIconSet, createSvgIconSet } from '@openremote/or-icon';
import { html } from 'lit';
import * as Core from '@openremote/core';
import { APIService } from '../services/api-service';
import { getRootPath } from './util';
import { manager } from '@openremote/core';

/**
* Base theme settings
*/
const BASE_THEME = {
color1: Core.DefaultColor1,
color2: Core.DefaultColor2,
color3: Core.DefaultColor3,
color4: Core.DefaultColor4,
color5: Core.DefaultColor5,
color6: Core.DefaultColor6
};

/**
* Setup the MDI-Icons for or-icon element
* Initialize icon sets, overriding the default createMdiIconSet with a custom implementation to allow custom urls
* Creates a custom MDI icon set that uses static font files from the assets directory
* and registers both MDI and OR icon sets for use with or-icon elements
*/
export function setupORIcons() {
function createMdiIconSet(): IconSet {
Expand Down Expand Up @@ -67,6 +57,18 @@ export function setupORIcons() {
IconSets.addIconSet('or', createSvgIconSet(OrIconSet.size, OrIconSet.icons));
}

/**
* Base theme settings
*/
const BASE_THEME = {
color1: Core.DefaultColor1,
color2: Core.DefaultColor2,
color3: Core.DefaultColor3,
color4: Core.DefaultColor4,
color5: Core.DefaultColor5,
color6: Core.DefaultColor6
};

/**
* Theme settings
*/
Expand All @@ -92,9 +94,11 @@ export async function setRealmTheme(realm: string) {
}

try {
const config = await APIService.getOpenRemoteRealmConfig(realm);
if (config && config.styles) {
const cssString = config.styles;
const config = (await manager.rest.api.ConfigurationResource.getManagerConfig()).data;
const styles = config.realms?.[realm]?.styles;

if (styles) {
const cssString = styles;
const colorRegex = /--or-app-color(\d+):\s*(#[0-9a-fA-F]{6})/g;
let match: RegExpExecArray | null;

Expand Down Expand Up @@ -128,7 +132,6 @@ export async function setRealmTheme(realm: string) {
} catch {
console.warn('Was unable to retrieve realm specific theme settings, falling back to default');
}

setTheme(theme);
}

Expand Down
19 changes: 16 additions & 3 deletions frontend/src/common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,24 @@
export function getRootPath() {
const scriptElement = document.querySelector('script[src*="bundle"]');

if (scriptElement && scriptElement.getAttribute('src')) {
if (scriptElement?.getAttribute('src')) {
const scriptPath = new URL(scriptElement.getAttribute('src')!, window.location.href).pathname;
// Positive lookahead to match everything up to bundle.js
const match = scriptPath.match(/(.*?)(?=bundle)/);
return match ? (match[1].endsWith('/') ? match[1].slice(0, -1) : match[1]) : '';
const regex = /(.*?)(?=bundle)/;
const match = regex.exec(scriptPath);
let rootPath = '';
if (match) {
rootPath = match[1].endsWith('/') ? match[1].slice(0, -1) : match[1];
}
return rootPath;
}
return '';
}

/**
* Get the realm search param from the url (?realm=)
* @returns The realm search param
*/
export const getRealmSearchParam = () => {
return new URLSearchParams(window.location.search).get('realm');
};
5 changes: 3 additions & 2 deletions frontend/src/components/configs-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@
import { OrMwcTable, TableColumn, TableConfig, TableRow } from '@openremote/or-mwc-components/or-mwc-table';
import { css, html, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { BasicAsset, ModelConfig } from '../services/models';
import { ModelConfig } from '../services/models';
import { getRootPath } from '../common/util';
import { Router } from '@vaadin/router';
import { InputType } from '@openremote/or-mwc-components/or-mwc-input';
import * as Model from '@openremote/model';

@customElement('configs-table')
export class ConfigsTable extends OrMwcTable {
Expand Down Expand Up @@ -88,7 +89,7 @@ export class ConfigsTable extends OrMwcTable {
public modelConfigs: ModelConfig[] = [];

@property({ type: Array })
public configAssets: BasicAsset[] = [];
public configAssets: Model.Asset[] = [];

@property({ type: String })
public realm: string = '';
Expand Down
82 changes: 61 additions & 21 deletions frontend/src/components/custom-duration-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,37 @@ export enum DurationInputType {
}

// ISO 8601
enum TimeDurationUnit {
MINUTE = 'M',
HOUR = 'H'
export enum TimeDurationUnit {
MINUTE = 'PT%M',
HOUR = 'PT%H',
DAY = 'P%D',
WEEK = 'P%W',
MONTH = 'P%M',
YEAR = 'P%Y'
}

// Display name for the ISO 8601 units
const TimeDurationUnitDisplay: Record<TimeDurationUnit, string> = {
[TimeDurationUnit.MINUTE]: 'Minutes',
[TimeDurationUnit.HOUR]: 'Hours',
[TimeDurationUnit.DAY]: 'Days',
[TimeDurationUnit.WEEK]: 'Weeks',
[TimeDurationUnit.MONTH]: 'Months',
[TimeDurationUnit.YEAR]: 'Years'
};

// Pandas Frequency
enum PandasTimeUnit {
MINUTE = 'min',
HOUR = 'h'
export enum PandasTimeUnit {
MINUTE = '%min',
HOUR = '%h'
}

// Display name for the Pandas units
const PandasTimeUnitDisplay: Record<PandasTimeUnit, string> = {
[PandasTimeUnit.MINUTE]: 'Minutes',
[PandasTimeUnit.HOUR]: 'Hours'
};

// This is a input component that renders both a number input and a dropdown for the unit of the duration
@customElement('custom-duration-input')
export class CustomDurationInput extends LitElement {
Expand Down Expand Up @@ -79,12 +99,17 @@ export class CustomDurationInput extends LitElement {
break;
}

if (!this.unit || !this.number) {
return;
}

// Replace the % in the unit with the number and set the value
switch (this.type) {
case DurationInputType.ISO_8601:
this.value = `PT${this.number}${this.unit}`;
this.value = `${this.unit.replace('%', this.number.toString())}`;
break;
case DurationInputType.PANDAS_FREQ:
this.value = `${this.number}${this.unit}`;
this.value = `${this.unit.replace('%', this.number.toString())}`;
}

// Fire event to notify parent component that the value has changed
Expand All @@ -105,20 +130,29 @@ export class CustomDurationInput extends LitElement {
@property({ type: String })
public value: string = '';

@property({ type: Array })
public iso_units: TimeDurationUnit[] = [TimeDurationUnit.MINUTE, TimeDurationUnit.HOUR];

@property({ type: Array })
public pandas_units: PandasTimeUnit[] = [PandasTimeUnit.MINUTE, PandasTimeUnit.HOUR];

protected number: number | null = null;

protected unit: TimeDurationUnit | PandasTimeUnit | null = null;

// Extract the number from the ISO 8601 Duration string
getNumberFromDuration(duration: string): number | null {
const match = /PT(\d+)([HM])/.exec(duration);
const match = /P(?:T)?(\d+)([HMDWMOY]+)/.exec(duration);
return match ? parseInt(match[1], 10) : null;
}

// Extract the unit from the ISO 8601 Duration string
getUnitFromDuration(duration: string): TimeDurationUnit | null {
const match = /PT(\d+)([HM])/.exec(duration);
return match ? (match[2] as TimeDurationUnit) : null;
const match = /P(?:T)?(\d+)([HMDWMOY]+)/.exec(duration);

// replace the number in the full match with % to get the unit
const unit = match?.[0]?.replace(match?.[1], '%');
return unit as TimeDurationUnit;
}

// Extract the number from the Pandas Frequency string
Expand All @@ -130,7 +164,21 @@ export class CustomDurationInput extends LitElement {
// Extract the unit from the Pandas Frequency string
getUnitFromPandasFrequency(freq: string): PandasTimeUnit | null {
const match = /(\d+)(min|h)/.exec(freq);
return match ? (match[2] as PandasTimeUnit) : null;

// replace the number in the full match with % to get the unit
const unit = match?.[0]?.replace(match?.[1], '%');
return unit as PandasTimeUnit;
}

// Get available unit options based on the units property or defaults
getUnitOptions(): [string, string][] {
if (this.type === DurationInputType.ISO_8601) {
return this.iso_units.map((unit) => [unit, TimeDurationUnitDisplay[unit]]);
} else if (this.type === DurationInputType.PANDAS_FREQ) {
return this.pandas_units.map((unit) => [unit, PandasTimeUnitDisplay[unit]]);
}

return [];
}

render() {
Expand All @@ -153,15 +201,7 @@ export class CustomDurationInput extends LitElement {
label="Unit"
.value=${this.unit}
@or-mwc-input-changed=${this.onInput}
.options="${this.type === DurationInputType.ISO_8601
? [
[TimeDurationUnit.MINUTE, 'Minutes'],
[TimeDurationUnit.HOUR, 'Hours']
]
: [
[PandasTimeUnit.MINUTE, 'Minutes'],
[PandasTimeUnit.HOUR, 'Hours']
]}"
.options="${this.getUnitOptions()}"
></or-mwc-input>
`;
}
Expand Down
Loading