Compare commits

...

4 Commits

Author SHA1 Message Date
Nawaz Dhandala
a4e64c88d9 refactor: simplify DashboardLogsComponent arguments and default values 2025-11-28 14:41:08 +00:00
Nawaz Dhandala
196c4ae42d feat: migrate from VERSION_PREFIX to VERSION for version management in workflows 2025-11-28 14:23:37 +00:00
Nawaz Dhandala
12eea138d3 Merge branch 'master' into dashboard-logs 2025-11-28 14:22:52 +00:00
Simon Larsen
65dd8f857c feat: add Logs component to dashboard with configuration options 2025-11-28 14:22:23 +00:00
12 changed files with 253 additions and 16 deletions

View File

@@ -42,15 +42,15 @@ jobs:
run: |
set -euo pipefail
VERSION_PREFIX_RAW="$(tr -d ' \n' < VERSION_PREFIX)"
if [[ -z "$VERSION_PREFIX_RAW" ]]; then
echo "VERSION_PREFIX is empty" >&2
VERSION_RAW="$(tr -d ' \n' < VERSION)"
if [[ -z "$VERSION_RAW" ]]; then
echo "VERSION is empty" >&2
exit 1
fi
IFS='.' read -r major minor patch <<< "$VERSION_PREFIX_RAW"
IFS='.' read -r major minor patch <<< "$VERSION_RAW"
if [[ -z "$minor" ]]; then
echo "VERSION_PREFIX must contain major and minor components" >&2
echo "VERSION must contain major and minor components" >&2
exit 1
fi
patch="${patch:-0}"
@@ -58,7 +58,7 @@ jobs:
for part_name in major minor patch; do
part="${!part_name}"
if ! [[ "$part" =~ ^[0-9]+$ ]]; then
echo "Invalid ${part_name} component '$part' in VERSION_PREFIX" >&2
echo "Invalid ${part_name} component '$part' in VERSION" >&2
exit 1
fi
done
@@ -82,18 +82,18 @@ jobs:
fi
new_version="${major}.${minor}.${target_patch}"
if [[ "$new_version" != "$VERSION_PREFIX_RAW" ]]; then
echo "$new_version" > VERSION_PREFIX
if [[ "$new_version" != "$VERSION_RAW" ]]; then
echo "$new_version" > VERSION
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add VERSION_PREFIX
git add VERSION
if ! git diff --cached --quiet; then
branch_name="chore/bump-version-prefix-${new_version}-$(date +%s)"
git checkout -b "$branch_name"
git commit -m "chore: bump version prefix to ${new_version} [skip ci]"
git push origin "$branch_name"
pr_title="chore: bump version prefix to ${new_version}"
pr_body=$'Automated change to VERSION_PREFIX to align release workflow with master.\n\nCreated by GitHub Actions release workflow.'
pr_body=$'Automated change to VERSION to align release workflow with master.\n\nCreated by GitHub Actions release workflow.'
gh pr create --repo "$REPOSITORY" --base master --head "$branch_name" --title "$pr_title" --body "$pr_body"
pr_url="$(gh pr view "$branch_name" --repo "$REPOSITORY" --json url --jq '.url' 2>/dev/null || true)"
updated="true"

View File

@@ -41,15 +41,15 @@ jobs:
run: |
set -euo pipefail
VERSION_PREFIX_RAW="$(tr -d ' \n' < VERSION_PREFIX)"
if [[ -z "$VERSION_PREFIX_RAW" ]]; then
echo "VERSION_PREFIX is empty" >&2
VERSION_RAW="$(tr -d ' \n' < VERSION)"
if [[ -z "$VERSION_RAW" ]]; then
echo "VERSION is empty" >&2
exit 1
fi
IFS='.' read -r major minor patch <<< "$VERSION_PREFIX_RAW"
IFS='.' read -r major minor patch <<< "$VERSION_RAW"
if [[ -z "$minor" ]]; then
echo "VERSION_PREFIX must contain major and minor components" >&2
echo "VERSION must contain major and minor components" >&2
exit 1
fi
patch="${patch:-0}"
@@ -57,7 +57,7 @@ jobs:
for part_name in major minor patch; do
part="${!part_name}"
if ! [[ "$part" =~ ^[0-9]+$ ]]; then
echo "Invalid ${part_name} component '$part' in VERSION_PREFIX" >&2
echo "Invalid ${part_name} component '$part' in VERSION" >&2
exit 1
fi
done

View File

@@ -2,6 +2,7 @@ enum DashboardComponentType {
Chart = `Chart`,
Value = `Value`,
Text = `Text`,
Logs = `Logs`,
}
export default DashboardComponentType;

View File

@@ -0,0 +1,13 @@
import ObjectID from "../../ObjectID";
import DashboardComponentType from "../DashboardComponentType";
import BaseComponent from "./DashboardBaseComponent";
export default interface DashboardLogsComponent extends BaseComponent {
componentType: DashboardComponentType.Logs;
componentId: ObjectID;
arguments: {
title?: string | undefined;
telemetryServiceIdsCsv?: string | undefined;
logQueryJson?: string | undefined;
};
}

View File

@@ -2,6 +2,7 @@ enum DashboardComponentType {
Chart = "Chart",
Value = "Value",
Text = "Text",
Logs = "Logs",
}
export default DashboardComponentType;

View File

@@ -0,0 +1,68 @@
import DashboardLogsComponent from "../../../Types/Dashboard/DashboardComponents/DashboardLogsComponent";
import { ObjectType } from "../../../Types/JSON";
import ObjectID from "../../../Types/ObjectID";
import DashboardBaseComponentUtil from "./DashboardBaseComponent";
import {
ComponentArgument,
ComponentInputType,
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
export default class DashboardLogsComponentUtil extends DashboardBaseComponentUtil {
public static override getDefaultComponent(): DashboardLogsComponent {
return {
_type: ObjectType.DashboardComponent,
componentType: DashboardComponentType.Logs,
widthInDashboardUnits: 8,
heightInDashboardUnits: 6,
topInDashboardUnits: 0,
leftInDashboardUnits: 0,
componentId: ObjectID.generate(),
minHeightInDashboardUnits: 4,
minWidthInDashboardUnits: 6,
arguments: {
title: "Logs",
telemetryServiceIdsCsv: "",
logQueryJson: "",
},
};
}
public static override getComponentConfigArguments(): Array<
ComponentArgument<DashboardLogsComponent>
> {
const componentArguments: Array<
ComponentArgument<DashboardLogsComponent>
> = [];
componentArguments.push({
name: "Title",
description: "Optional heading shown above the logs widget.",
required: false,
type: ComponentInputType.Text,
id: "title",
});
componentArguments.push({
name: "Telemetry Service IDs",
description:
"Comma separated telemetry service IDs (UUIDs) to scope logs.",
required: false,
type: ComponentInputType.Text,
id: "telemetryServiceIdsCsv",
placeholder: "service-id-1, service-id-2",
});
componentArguments.push({
name: "Advanced Log Query (JSON)",
description:
"Optional JSON object merged with the generated query for advanced filters.",
required: false,
type: ComponentInputType.LongText,
id: "logQueryJson",
placeholder: '{ "attributes.environment": "prod" }',
});
return componentArguments;
}
}

View File

@@ -5,6 +5,7 @@ import BadDataException from "../../../Types/Exception/BadDataException";
import DashboardChartComponentUtil from "./DashboardChartComponent";
import DashboardTextComponentUtil from "./DashboardTextComponent";
import DashboardValueComponentUtil from "./DashboardValueComponent";
import DashboardLogsComponentUtil from "./DashboardLogsComponent";
export default class DashboardComponentsUtil {
public static getComponentSettingsArguments(
@@ -28,6 +29,12 @@ export default class DashboardComponentsUtil {
>;
}
if (dashboardComponentType === DashboardComponentType.Logs) {
return DashboardLogsComponentUtil.getComponentConfigArguments() as Array<
ComponentArgument<DashboardBaseComponent>
>;
}
throw new BadDataException(
`Unknown dashboard component type: ${dashboardComponentType}`,
);

View File

@@ -3,9 +3,11 @@ import DashboardTextComponentType from "Common/Types/Dashboard/DashboardComponen
import DashboardChartComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardChartComponent";
import DashboardValueComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardValueComponent";
import DashboardBaseComponent from "Common/Types/Dashboard/DashboardComponents/DashboardBaseComponent";
import DashboardLogsComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardLogsComponent";
import DashboardChartComponent from "./DashboardChartComponent";
import DashboardValueComponent from "./DashboardValueComponent";
import DashboardTextComponent from "./DashboardTextComponent";
import DashboardLogsComponent from "./DashboardLogsComponent";
import DefaultDashboardSize, {
GetDashboardComponentHeightInDashboardUnits,
GetDashboardComponentWidthInDashboardUnits,
@@ -404,6 +406,14 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
component={component as DashboardValueComponentType}
/>
)}
{component.componentType === DashboardComponentType.Logs && (
<DashboardLogsComponent
{...props}
isSelected={props.isSelected}
isEditMode={props.isEditMode}
component={component as DashboardLogsComponentType}
/>
)}
{getResizeWidthElement()}
{getResizeHeightElement()}

View File

@@ -0,0 +1,125 @@
import React, { FunctionComponent, ReactElement, useMemo } from "react";
import { DashboardBaseComponentProps } from "./DashboardBaseComponent";
import DashboardLogsComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardLogsComponent";
import DashboardLogsViewer from "../../Logs/LogsViewer";
import ObjectID from "Common/Types/ObjectID";
import Query from "Common/Types/BaseDatabase/Query";
import Log from "Common/Models/AnalyticsModels/Log";
import JSONFunctions from "Common/Types/JSONFunctions";
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
import InBetween from "Common/Types/BaseDatabase/InBetween";
import { RangeStartAndEndDateTimeUtil } from "Common/Types/Time/RangeStartAndEndDateTime";
export interface ComponentProps extends DashboardBaseComponentProps {
component: DashboardLogsComponentType;
}
const DashboardLogsComponent: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const args: DashboardLogsComponentType["arguments"] =
props.component.arguments || {};
const sanitizeCsv: (value?: string) => Array<string> = (
value?: string,
): Array<string> => {
if (!value) {
return [];
}
return value
.split(",")
.map((entry: string) => entry.trim())
.filter((entry: string) => entry.length > 0);
};
const telemetryServiceIds: Array<ObjectID> = useMemo(() => {
return sanitizeCsv(args.telemetryServiceIdsCsv).reduce(
(ids: Array<ObjectID>, id: string) => {
try {
ids.push(ObjectID.fromString(id));
} catch (err) {
// ignore invalid ids to avoid breaking the widget.
}
return ids;
},
[],
);
}, [args.telemetryServiceIdsCsv]);
const limit: number = 100;
const logQueryResult: {
query: Query<Log>;
error: string | null;
} = useMemo(() => {
const mergedQuery: Query<Log> = {};
let error: string | null = null;
const rawQuery: string = (args.logQueryJson || "").trim();
if (rawQuery) {
try {
const parsedQuery: Query<Log> = JSONFunctions.parseJSONObject(
rawQuery,
) as Query<Log>;
Object.assign(mergedQuery, parsedQuery);
} catch (err) {
error = `Invalid log query JSON. ${(err as Error).message}`;
}
}
if (!(mergedQuery as Record<string, unknown>)["time"]) {
const range: InBetween<Date> =
RangeStartAndEndDateTimeUtil.getStartAndEndDate(
props.dashboardStartAndEndDate,
);
Object.assign(mergedQuery as Record<string, unknown>, {
time: new InBetween<Date>(range.startValue, range.endValue),
});
}
return {
query: mergedQuery,
error,
};
}, [
args.logQueryJson,
props.dashboardStartAndEndDate,
]);
if (logQueryResult.error) {
return <ErrorMessage message={logQueryResult.error} />;
}
const showFilters: boolean = true;
const enableRealtime: boolean = true;
const noLogsMessage: string = "No logs found.";
const title: string = args.title || "Logs";
return (
<div className="flex h-full flex-col overflow-hidden">
{title ? (
<div className="mb-2 text-sm font-medium text-gray-500">{title}</div>
) : (
<></>
)}
<div className="flex-1 overflow-auto" style={{ minHeight: 0 }}>
<DashboardLogsViewer
id={props.component.componentId.toString()}
showFilters={showFilters}
telemetryServiceIds={telemetryServiceIds}
logQuery={logQueryResult.query}
limit={limit}
enableRealtime={enableRealtime}
noLogsMessage={noLogsMessage}
/>
</div>
</div>
);
};
export default DashboardLogsComponent;

View File

@@ -15,6 +15,7 @@ import DashboardBaseComponent from "Common/Types/Dashboard/DashboardComponents/D
import DashboardChartComponentUtil from "Common/Utils/Dashboard/Components/DashboardChartComponent";
import DashboardValueComponentUtil from "Common/Utils/Dashboard/Components/DashboardValueComponent";
import DashboardTextComponentUtil from "Common/Utils/Dashboard/Components/DashboardTextComponent";
import DashboardLogsComponentUtil from "Common/Utils/Dashboard/Components/DashboardLogsComponent";
import BadDataException from "Common/Types/Exception/BadDataException";
import ObjectID from "Common/Types/ObjectID";
import Dashboard from "Common/Models/DatabaseModels/Dashboard";
@@ -253,6 +254,10 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
newComponent = DashboardTextComponentUtil.getDefaultComponent();
}
if (componentType === DashboardComponentType.Logs) {
newComponent = DashboardLogsComponentUtil.getDefaultComponent();
}
if (!newComponent) {
throw new BadDataException(
`Unknown component type: ${componentType}`,

View File

@@ -81,6 +81,13 @@ const DashboardToolbar: FunctionComponent<ComponentProps> = (
props.onAddComponentClick(DashboardComponentType.Text);
}}
/>
<MoreMenuItem
text={"Add Logs"}
key={"add-logs"}
onClick={() => {
props.onAddComponentClick(DashboardComponentType.Logs);
}}
/>
</MoreMenu>
) : (
<></>