Files
catalog/ts_web/elements/upl-statuspage-assetsselector.ts

646 lines
20 KiB
TypeScript

import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
cssManager,
css,
unsafeCSS,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import type { IServiceStatus } from '../interfaces/index.js';
import * as sharedStyles from '../styles/shared.styles.js';
import './internal/uplinternal-miniheading.js';
import { demoFunc } from './upl-statuspage-assetsselector.demo.js';
declare global {
interface HTMLElementTagNameMap {
'upl-statuspage-assetsselector': UplStatuspageAssetsselector;
}
}
@customElement('upl-statuspage-assetsselector')
export class UplStatuspageAssetsselector extends DeesElement {
public static demo = demoFunc;
@property({ type: Array })
accessor services: IServiceStatus[] = [];
@property({ type: String })
accessor filterText: string = '';
@property({ type: String })
accessor filterCategory: string = 'all';
@property({ type: Boolean })
accessor showOnlySelected: boolean = false;
@property({ type: Boolean })
accessor loading: boolean = false;
@property({ type: Boolean })
accessor expanded: boolean = false;
constructor() {
super();
}
public static styles = [
cssManager.defaultStyles,
sharedStyles.commonStyles,
css`
:host {
display: block;
background: transparent;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
color: ${sharedStyles.colors.text.primary};
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)} ${unsafeCSS(sharedStyles.spacing.lg)};
}
.controls {
display: flex;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
margin-bottom: ${unsafeCSS(sharedStyles.spacing.md)};
flex-wrap: wrap;
align-items: center;
}
.search-input {
flex: 1;
min-width: 200px;
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
background: ${sharedStyles.colors.background.card};
color: ${sharedStyles.colors.text.primary};
font-size: 13px;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
height: 32px;
}
.search-input:focus {
outline: none;
border-color: ${sharedStyles.colors.text.primary};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
}
.search-input::placeholder {
color: ${sharedStyles.colors.text.muted};
}
.filter-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: ${unsafeCSS(sharedStyles.spacing.xs)} ${unsafeCSS(sharedStyles.spacing.sm)};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
background: ${sharedStyles.colors.background.card};
color: ${sharedStyles.colors.text.secondary};
cursor: pointer;
font-size: 13px;
font-weight: 500;
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
height: 32px;
}
.filter-button:hover {
border-color: ${sharedStyles.colors.border.muted};
color: ${sharedStyles.colors.text.primary};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
}
.filter-button.active {
background: ${sharedStyles.colors.text.primary};
color: ${sharedStyles.colors.background.primary};
border-color: ${sharedStyles.colors.text.primary};
}
.selected-services {
display: flex;
flex-wrap: wrap;
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
align-items: center;
}
.service-pill {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
padding: 6px ${unsafeCSS(sharedStyles.spacing.md)};
background: ${sharedStyles.colors.background.card};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
font-size: 13px;
font-weight: 500;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
animation: pillFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
}
.service-pill:nth-child(1) { animation-delay: 0ms; }
.service-pill:nth-child(2) { animation-delay: 30ms; }
.service-pill:nth-child(3) { animation-delay: 60ms; }
.service-pill:nth-child(4) { animation-delay: 90ms; }
.service-pill:nth-child(5) { animation-delay: 120ms; }
@keyframes pillFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.service-pill:hover {
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
transform: translateY(-1px);
}
.service-pill .status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.service-pill .status-dot.operational {
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(34, 197, 94, 0.2)', 'rgba(34, 197, 94, 0.3)')};
}
.manage-button {
display: inline-flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
padding: 6px ${unsafeCSS(sharedStyles.spacing.md)};
background: ${sharedStyles.colors.background.card};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
color: ${sharedStyles.colors.text.secondary};
font-family: ${unsafeCSS(sharedStyles.fonts.base)};
}
.manage-button:hover {
border-color: ${sharedStyles.colors.border.muted};
color: ${sharedStyles.colors.text.primary};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
transform: translateY(-1px);
}
.manage-button:active {
transform: translateY(0);
}
.expandable-section {
margin-top: ${unsafeCSS(sharedStyles.spacing.lg)};
overflow: hidden;
animation: expandIn 0.3s ${unsafeCSS(sharedStyles.easings.default)};
}
@keyframes expandIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.expandable-content {
padding: ${unsafeCSS(sharedStyles.spacing.lg)};
background: ${sharedStyles.colors.background.secondary};
border: 1px solid ${sharedStyles.colors.border.default};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.lg)};
}
.assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
}
.asset-card {
display: flex;
align-items: center;
padding: ${unsafeCSS(sharedStyles.spacing.md)};
background: ${sharedStyles.colors.background.card};
border-radius: ${unsafeCSS(sharedStyles.borderRadius.base)};
cursor: pointer;
transition: all ${unsafeCSS(sharedStyles.durations.fast)} ${unsafeCSS(sharedStyles.easings.default)};
border: 1px solid ${sharedStyles.colors.border.default};
gap: ${unsafeCSS(sharedStyles.spacing.sm)};
animation: cardFadeIn 0.3s ${unsafeCSS(sharedStyles.easings.default)} both;
}
@keyframes cardFadeIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.asset-card:hover {
border-color: ${sharedStyles.colors.border.muted};
box-shadow: ${unsafeCSS(sharedStyles.shadows.sm)};
transform: translateY(-2px);
}
.asset-card.selected {
border-color: ${sharedStyles.colors.text.primary};
background: ${sharedStyles.colors.background.secondary};
}
.asset-card.selected:hover {
box-shadow: ${unsafeCSS(sharedStyles.shadows.md)};
}
.asset-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: ${sharedStyles.colors.text.primary};
flex-shrink: 0;
}
.asset-info {
flex: 1;
min-width: 0;
}
.asset-name {
font-weight: 600;
font-size: 14px;
margin-bottom: ${unsafeCSS(sharedStyles.spacing.xs)};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-description {
font-size: 13px;
color: ${sharedStyles.colors.text.secondary};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.asset-status {
display: flex;
align-items: center;
gap: ${unsafeCSS(sharedStyles.spacing.xs)};
flex-shrink: 0;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: ${unsafeCSS(sharedStyles.borderRadius.full)};
flex-shrink: 0;
}
.status-indicator.operational, .status-dot.operational {
background: ${sharedStyles.colors.status.operational};
}
.status-indicator.degraded, .status-dot.degraded {
background: ${sharedStyles.colors.status.degraded};
}
.status-indicator.partial_outage, .status-dot.partial_outage {
background: ${sharedStyles.colors.status.partial};
}
.status-indicator.major_outage, .status-dot.major_outage {
background: ${sharedStyles.colors.status.major};
}
.status-indicator.maintenance, .status-dot.maintenance {
background: ${sharedStyles.colors.status.maintenance};
}
.status-text {
font-size: 12px;
text-transform: capitalize;
color: ${sharedStyles.colors.text.secondary};
}
.loading-message,
.no-results {
grid-column: 1 / -1;
text-align: center;
padding: ${unsafeCSS(sharedStyles.spacing.xl)};
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
font-size: 13px;
}
.summary {
text-align: right;
font-size: 12px;
margin-top: ${unsafeCSS(sharedStyles.spacing.md)};
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
}
.no-services {
padding: ${unsafeCSS(sharedStyles.spacing.xl)};
text-align: center;
color: ${cssManager.bdTheme('#9ca3af', '#71717a')};
font-size: 13px;
}
@media (max-width: 640px) {
.container {
padding: 0 ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)} ${unsafeCSS(sharedStyles.spacing.md)};
}
.controls {
flex-direction: column;
align-items: stretch;
}
.search-input {
width: 100%;
}
.selected-services {
flex-direction: column;
align-items: stretch;
}
.service-pill {
width: auto;
}
.expandable-content {
padding: ${unsafeCSS(sharedStyles.spacing.md)};
}
.assets-grid {
grid-template-columns: 1fr;
}
.asset-card {
padding: ${unsafeCSS(sharedStyles.spacing.sm)};
}
}
`,
]
public render(): TemplateResult {
const selectedServices = this.services.filter(s => s.selected);
const selectedCount = selectedServices.length;
const categories = this.getUniqueCategories();
return html`
<div class="container">
<uplinternal-miniheading>Monitored Assets</uplinternal-miniheading>
<div class="selected-services">
${selectedCount === 0 ? html`
<span style="color: ${cssManager.bdTheme('#9ca3af', '#71717a')}; font-size: 13px;">
No services selected
</span>
` : selectedCount > 5 && !this.expanded ? html`
${selectedServices.slice(0, 4).map(service => html`
<div class="service-pill">
<span class="status-dot ${service.currentStatus}"></span>
<span>${service.displayName}</span>
</div>
`)}
<span style="color: ${cssManager.bdTheme('#6b7280', '#a1a1aa')}; font-size: 12px;">
+${selectedCount - 4} more
</span>
` : selectedServices.map(service => html`
<div class="service-pill">
<span class="status-dot ${service.currentStatus}"></span>
<span>${service.displayName}</span>
</div>
`)}
<button
class="manage-button"
@click=${() => { this.expanded = !this.expanded; }}
>
${this.expanded ? 'Close' : 'Manage Services'}
<svg
width="10"
height="6"
viewBox="0 0 10 6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style="transform: rotate(${this.expanded ? '180deg' : '0'}); transition: transform 0.2s;"
>
<path
d="M1 1L5 5L9 1"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
${this.expanded ? html`
<div class="expandable-section">
<div class="expandable-content">
<div class="controls">
<input
type="text"
class="search-input"
placeholder="Search services..."
.value=${this.filterText}
@input=${(e: Event) => {
this.filterText = (e.target as HTMLInputElement).value;
}}
/>
<button
class="filter-button ${this.filterCategory === 'all' ? 'active' : ''}"
@click=${() => { this.filterCategory = 'all'; }}
>
All
</button>
${categories.map(category => html`
<button
class="filter-button ${this.filterCategory === category ? 'active' : ''}"
@click=${() => { this.filterCategory = category; }}
>
${category}
</button>
`)}
<button
class="filter-button ${this.showOnlySelected ? 'active' : ''}"
@click=${() => { this.showOnlySelected = !this.showOnlySelected; }}
>
${this.showOnlySelected ? 'Show All' : 'Selected Only'}
</button>
<button
class="filter-button"
@click=${() => this.selectAll()}
>
Select All
</button>
<button
class="filter-button"
@click=${() => this.selectNone()}
>
Select None
</button>
</div>
<div class="assets-grid">
${this.loading ? html`
<div class="loading-message">Loading services...</div>
` : this.renderServiceGrid()}
</div>
<div class="summary">
${selectedCount} of ${this.services.length} services selected
</div>
</div>
</div>
` : ''}
</div>
`;
}
private getFilteredServices(): IServiceStatus[] {
return this.services.filter(service => {
// Apply text filter
if (this.filterText && !service.displayName.toLowerCase().includes(this.filterText.toLowerCase()) &&
(!service.description || !service.description.toLowerCase().includes(this.filterText.toLowerCase()))) {
return false;
}
// Apply category filter
if (this.filterCategory !== 'all' && service.category !== this.filterCategory) {
return false;
}
// Apply selected filter
if (this.showOnlySelected && !service.selected) {
return false;
}
return true;
});
}
private getUniqueCategories(): string[] {
const categories = new Set<string>();
this.services.forEach(service => {
if (service.category) {
categories.add(service.category);
}
});
return Array.from(categories).sort();
}
private toggleService(serviceId: string) {
const service = this.services.find(s => s.id === serviceId);
if (service) {
service.selected = !service.selected;
this.requestUpdate();
this.dispatchEvent(new CustomEvent('selectionChanged', {
detail: {
serviceId,
selected: service.selected,
selectedServices: this.services.filter(s => s.selected).map(s => s.id)
},
bubbles: true,
composed: true
}));
}
}
private selectAll() {
const filteredServices = this.getFilteredServices();
filteredServices.forEach(service => {
service.selected = true;
});
this.requestUpdate();
this.emitSelectionUpdate();
}
private selectNone() {
const filteredServices = this.getFilteredServices();
filteredServices.forEach(service => {
service.selected = false;
});
this.requestUpdate();
this.emitSelectionUpdate();
}
private emitSelectionUpdate() {
this.dispatchEvent(new CustomEvent('selectionChanged', {
detail: {
selectedServices: this.services.filter(s => s.selected).map(s => s.id)
},
bubbles: true,
composed: true
}));
}
private renderServiceGrid(): TemplateResult {
const filteredServices = this.getFilteredServices();
if (filteredServices.length === 0) {
return html`<div class="no-results">No services found matching your criteria</div>`;
}
return html`${filteredServices.map(service => html`
<div
class="asset-card ${service.selected ? 'selected' : ''}"
@click=${() => this.toggleService(service.id)}
>
<input
type="checkbox"
class="asset-checkbox"
.checked=${service.selected}
@click=${(e: Event) => e.stopPropagation()}
@change=${(e: Event) => {
e.stopPropagation();
this.toggleService(service.id);
}}
/>
<div class="asset-info">
<div class="asset-name">${service.displayName}</div>
${service.description ? html`
<div class="asset-description">${service.description}</div>
` : ''}
</div>
<div class="asset-status">
<div class="status-indicator ${service.currentStatus}"></div>
<div class="status-text">${service.currentStatus.replace(/_/g, ' ')}</div>
</div>
</div>
`)}`;
}
}