Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Owns the Angular public website, operator dashboard and embeddable authenticatio

Start with the canonical [work state](../user-proxy/docs/WORK-STATE.md), [architecture](../user-proxy/docs/ARCHITECTURE.md), [capabilities](../user-proxy/docs/CAPABILITIES.md), [development](../user-proxy/docs/DEVELOPMENT.md), [security](../user-proxy/docs/SECURITY-OPERATIONS.md) and [design](../user-proxy/docs/DESIGN.md). Preserve existing README contribution rules and DESIGNGUIDE.md. Read widget integration/event guides before changing the custom element contract.

- Branch workflow: use `production` as the frontend base and PR target. Before pushing a feature branch or opening its PR, fetch and merge the latest `origin/production` into that feature branch. Keep release branches unchanged until PR review.
- Inspect status/diff; preserve unrelated changes. New application code uses TypeScript and existing standalone Angular patterns. Use Angular CLI scaffolding when practical; never change architecture solely to satisfy scaffolding.
- Install from lock using `npm ci --ignore-scripts --legacy-peer-deps`; generate ignored env modules with `node tools/set-env`. Review environment base URLs before starting: local config defaults to a remote test API.
- Run `node --test tools/tests/*.test.cjs`, affected builds and `git diff --check`. Existing Nx/Jest targets have known gaps: report exact commands and limitations in canonical work state.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ Read [AGENTS.md](AGENTS.md), [current work state](../user-proxy/docs/WORK-STATE.

The existing contribution and style guidance below remains applicable. Use the canonical development and design guides where obsolete runtime/module instructions conflict with the current standalone Angular architecture.

## Isolated design review

Product and API guidance lives at [developers.36blocks.com](https://developers.36blocks.com). `/docs` is a legacy handoff page, not another documentation site.

Run `npm run review:build`, then `npm run review:serve` for the local review at `http://127.0.0.1:4360`. It renders the real routes using synthetic fixtures and a separate bootstrap. All mutations are rejected; no live API calls or credentials are issued. `review-version.json` identifies the source fingerprint. Release configurations continue to use `src/main.ts`. This review does not verify live authentication or providers.

## Contributing Guide

Before contributing it is required that you should have a proper knowledge of **Git** and **Nx**. If you're unfamiliar with Git or Nx you should first visit the recommended [Udacity Git course](https://blog.udacity.com/2015/06/a-beginners-git-github-tutorial.html) and [Nx Docs](https://nx.dev/getting-started/intro)
Expand Down
14 changes: 14 additions & 0 deletions apps/36-blocks/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@
"optimization": false,
"extractLicenses": false,
"sourceMap": true
},
"review": {
"browser": "apps/36-blocks/src/main.review.ts",
"tsConfig": "apps/36-blocks/tsconfig.review.json",
"outputPath": "dist/review",
"optimization": false,
"extractLicenses": false,
"sourceMap": false,
"fileReplacements": [
{
"replace": "apps/36-blocks/src/environments/environment.ts",
"with": "apps/36-blocks/src/environments/environment.review.ts"
}
]
}
},
"defaultConfiguration": "production"
Expand Down
133 changes: 75 additions & 58 deletions apps/36-blocks/src/app/panel/activity/activity.component.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<section class="activity-page" aria-labelledby="activity-title">
<header>
<h1 id="activity-title">Activity</h1>
<p>
Investigate operator actions for the current client. Login as records include the actor, target, reason and
outcome.
</p>
<p>
Older operations may have no audit details. A started attempt without a final outcome needs investigation.
</p>
<header class="page-heading">
<div>
<span class="page-kicker">INVESTIGATE WITH CONTEXT</span>
<h1 id="activity-title">Operator activity</h1>
<p>Who acted, what changed, and the recorded outcome in this workspace.</p>
</div>
<a routerLink="/app/users">Find a person →</a>
</header>

<div class="experience-notice">
<span
>Older operations may have no audit details. A started attempt without a final outcome needs
investigation.</span
><a href="https://developers.36blocks.com">Activity guidance ↗</a>
</div>
<details class="login-as">
<summary>Login as an end user</summary>
<p>
Expand All @@ -18,9 +21,9 @@ <h1 id="activity-title">Activity</h1>
client. If no company is specified, the user's first company is checked for eligibility.
</p>
<p>
Confirm the user and company before continuing. Configure an HTTPS application redirect on the block
before starting; this panel does not display raw session tokens. The application remains responsible for
business permissions.
Confirm the user and company before continuing. Configure an HTTPS application redirect on the block before
starting; this panel does not display raw session tokens. The application remains responsible for business
permissions.
</p>
<form
class="activity-filters"
Expand Down Expand Up @@ -97,52 +100,66 @@ <h1 id="activity-title">Activity</h1>
}
</details>

<form
class="activity-filters"
#filterForm="ngForm"
(ngSubmit)="filterForm.valid && applyFilters()"
aria-label="Filter activity"
>
<mat-form-field
><mat-label>Operation</mat-label
><input matInput name="operation" [(ngModel)]="filters.operation" placeholder="For example, login_as"
/></mat-form-field>
<mat-form-field
><mat-label>Actor name or email</mat-label><input matInput name="actor" [(ngModel)]="filters.name"
/></mat-form-field>
<mat-form-field
><mat-label>Outcome</mat-label
><mat-select name="outcome" [(ngModel)]="filters.outcome">
<mat-option value="">All outcomes</mat-option>
<mat-option value="started">Started</mat-option><mat-option value="succeeded">Succeeded</mat-option>
<mat-option value="denied">Denied</mat-option><mat-option value="failed">Failed</mat-option>
</mat-select></mat-form-field
<details class="activity-filter-panel">
<summary>
Filter operator activity
@if (filters.target_user_id) {
<span class="filter-target">User {{ filters.target_user_id }}</span>
}
</summary>
<form
class="activity-filters"
#filterForm="ngForm"
(ngSubmit)="filterForm.valid && applyFilters()"
aria-label="Filter activity"
>
<mat-form-field
><mat-label>Target user ID</mat-label
><input matInput name="target" [(ngModel)]="filters.target_user_id" inputmode="numeric" pattern="[0-9]+"
/></mat-form-field>
<mat-form-field
><mat-label>Block ID</mat-label
><input
matInput
name="block"
[(ngModel)]="filters.feature_configuration_id"
inputmode="numeric"
pattern="[0-9]+"
/></mat-form-field>
<mat-form-field
><mat-label>From date</mat-label><input matInput name="from" type="date" [(ngModel)]="filters.startDate"
/></mat-form-field>
<mat-form-field
><mat-label>Through date</mat-label
><input matInput name="through" type="date" [(ngModel)]="filters.endDate"
/></mat-form-field>
<div class="filter-actions">
<button mat-flat-button type="submit" [disabled]="loading() || filterForm.invalid">Apply filters</button
><button mat-button type="button" (click)="clearFilters()" [disabled]="loading()">Clear</button>
</div>
</form>
<mat-form-field
><mat-label>Operation</mat-label
><input matInput name="operation" [(ngModel)]="filters.operation" placeholder="For example, login_as"
/></mat-form-field>
<mat-form-field
><mat-label>Actor name or email</mat-label><input matInput name="actor" [(ngModel)]="filters.name"
/></mat-form-field>
<mat-form-field
><mat-label>Outcome</mat-label
><mat-select name="outcome" [(ngModel)]="filters.outcome">
<mat-option value="">All outcomes</mat-option>
<mat-option value="started">Started</mat-option><mat-option value="succeeded">Succeeded</mat-option>
<mat-option value="denied">Denied</mat-option><mat-option value="failed">Failed</mat-option>
</mat-select></mat-form-field
>
<mat-form-field
><mat-label>Target user ID</mat-label
><input
matInput
name="target"
[(ngModel)]="filters.target_user_id"
inputmode="numeric"
pattern="[0-9]+"
/></mat-form-field>
<mat-form-field
><mat-label>Block ID</mat-label
><input
matInput
name="block"
[(ngModel)]="filters.feature_configuration_id"
inputmode="numeric"
pattern="[0-9]+"
/></mat-form-field>
<mat-form-field
><mat-label>From date</mat-label
><input matInput name="from" type="date" [(ngModel)]="filters.startDate"
/></mat-form-field>
<mat-form-field
><mat-label>Through date</mat-label
><input matInput name="through" type="date" [(ngModel)]="filters.endDate"
/></mat-form-field>
<div class="filter-actions">
<button mat-flat-button type="submit" [disabled]="loading() || filterForm.invalid">Apply filters</button
><button mat-button type="button" (click)="clearFilters()" [disabled]="loading()">Clear</button>
</div>
</form>
</details>

@if (loading()) {
<p role="status" aria-live="polite">Loading activity…</p>
Expand Down
33 changes: 31 additions & 2 deletions apps/36-blocks/src/app/panel/activity/activity.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
display: block;
}
.activity-page {
padding: 1.5rem;
padding: 0;
max-width: 1500px;
margin: auto;
}
Expand Down Expand Up @@ -96,9 +96,38 @@ dd {
}
@media (max-width: 600px) {
.activity-page {
padding: 1rem;
padding: 0;
}
.reason-field {
grid-column: auto;
}
}

.login-as {
background: var(--experience-surface);
}
.activity-page th {
background: var(--experience-soft);
font-size: 11px;
color: var(--experience-muted);
}
.activity-page td {
font-size: 12px;
}

.activity-filter-panel {
border: 1px solid var(--experience-border);
border-radius: 8px;
background: var(--experience-surface);
padding: 14px 16px;
margin: 18px 0;
}
.activity-filter-panel summary {
font-size: 13px;
}
.filter-target {
margin-left: 14px;
font-weight: 400;
color: var(--experience-muted);
font-size: 12px;
}
18 changes: 17 additions & 1 deletion apps/36-blocks/src/app/panel/activity/activity.component.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ActivatedRoute, RouterModule } from '@angular/router';
import { DatePipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
Expand All @@ -18,11 +19,20 @@ import { ActivityService } from './activity.service';
@Component({
selector: 'proxy-activity',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DatePipe, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatSelectModule],
imports: [
RouterModule,
DatePipe,
FormsModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
],
templateUrl: './activity.component.html',
styleUrl: './activity.component.scss',
})
export class ActivityComponent implements OnInit, OnDestroy {
private readonly route = inject(ActivatedRoute);
private readonly service = inject(ActivityService);
private request?: Subscription;
private loginRequest?: Subscription;
Expand All @@ -44,6 +54,12 @@ export class ActivityComponent implements OnInit, OnDestroy {
readonly loginRedirect = signal<string | null>(null);

ngOnInit(): void {
const target = this.route.snapshot.queryParamMap.get('target_user_id');
if (target && /^[1-9][0-9]*$/.test(target) && Number.isSafeInteger(Number(target))) {
this.filters.target_user_id = target;
this.appliedFilters.target_user_id = target;
this.loginTarget = target;
}
this.load(1);
}

Expand Down
10 changes: 8 additions & 2 deletions apps/36-blocks/src/app/panel/chatbot/chatbot.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'app-chatbot',
imports: [],
template: `<p></p>`,
template: `<div class="page-heading">
<div>
<span class="page-kicker">WORKSPACE ASSISTANT</span>
<h1>Assistant</h1>
<p>Help for the current workspace. Availability depends on your configured integration.</p>
</div>
<a href="https://developers.36blocks.com">Developer documentation ↗</a>
</div>`,
})
export class ChatbotComponent {}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="rounded-2xl bg-color shadow-sm p-5 flex flex-col gap-2 relative">
<div class="rounded-xl bg-color border border-color p-5 flex flex-col gap-2 relative">
<!-- Card Header -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
Expand All @@ -10,7 +10,13 @@
>info_outline</mat-icon
>
</div>
<button mat-icon-button class="w-7 h-7 leading-7" (click)="openExpand()" matTooltip="Expand">
<button
mat-icon-button
class="w-7 h-7 leading-7"
(click)="openExpand()"
matTooltip="Expand"
aria-label="Expand usage breakdown"
>
<mat-icon class="mat-icon-16 text-gray-400">open_in_full</mat-icon>
</button>
</div>
Expand All @@ -24,9 +30,9 @@

<!-- Error -->
@if (error()) {
<div class="flex items-center gap-2 text-red-500 text-sm">
<div class="experience-notice error" role="alert">
<mat-icon class="mat-icon-18">error_outline</mat-icon>
{{ error() }}
{{ error() }}<button mat-button (click)="retry()">Retry breakdown</button>
</div>
}

Expand All @@ -36,7 +42,11 @@
<div #chartEl class="!h-60 !w-full"></div>
<div class="flex justify-end mt-1 xl:absolute top-[40%] right-[10%]">
<mat-form-field class="no-padding w-32">
<mat-select [value]="selectedGroupBy()" (valueChange)="onGroupByChange($event)">
<mat-select
aria-label="Group usage breakdown"
[value]="selectedGroupBy()"
(valueChange)="onGroupByChange($event)"
>
@for (opt of groupByOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
Expand All @@ -50,7 +60,11 @@
</div>
<div class="flex justify-end mt-1">
<mat-form-field class="no-padding w-32">
<mat-select [value]="selectedGroupBy()" (valueChange)="onGroupByChange($event)">
<mat-select
aria-label="Group usage breakdown"
[value]="selectedGroupBy()"
(valueChange)="onGroupByChange($event)"
>
@for (opt of groupByOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
Expand All @@ -66,7 +80,7 @@
<span class="text-base font-semibold">
Breakdown — {{ groupByOptions.find(o => o.value === selectedGroupBy())?.label ?? '' }}
</span>
<button mat-icon-button (click)="closeExpand()">
<button mat-icon-button (click)="closeExpand()" aria-label="Close expanded chart">
<mat-icon>close</mat-icon>
</button>
</div>
Expand Down
Loading
Loading