import type { PropertyValueMap } from 'lit' import { css, html, svg } from 'lit' import { property, query, state } from 'lit/decorators.js' import { classMap } from 'lit/directives/class-map.js' import { when } from 'lit/directives/when.js' import { actionConnectedApp, actionConnectingApp, actionFailedConnectinggApp } from '@upswot/core' import { customElementIfNotExist } from '../../utils/customElementIfNotExist' import { dispatchCustomEvent } from '../../utils/dispatchCustomEvent' import { toggleImageDarkTheme } from '../../utils/toggleImageDarkTheme' import { UsBaseElement } from '../base/UsBaseElement' import { localizeManager, localized } from '../../dependencies' import type { ISource, ISourceView } from './model/ISource' import type { IDataSourcesProps } from './model/IDataSourcesProps' import style_css from './dataSources.pcss' import '../button/UsButton' import '../icon/UsIcon' declare global { interface HTMLElementTagNameMap { 'us-data-source': UsDataSource } } @localized() @customElementIfNotExist('us-data-sources') export class UsDataSource extends UsBaseElement implements IDataSourcesProps { static override shadowRootOptions: ShadowRootInit = { mode: 'open', delegatesFocus: true } static styles = [ UsBaseElement.styles, css([style_css] as TemplateStringsArray | any), ] @property({ type: String }) widgetView = 'vertical' @property({ type: Boolean }) show = false @property({ type: Boolean }) isDarkTheme = false @property({ type: Boolean }) disabled = false @property({ type: Boolean }) readonly = false @property({ type: Array }) sources: ISource[] = [] @property({ type: String }) locale!: string @property({ type: Boolean }) disableApps = false @state() hiddenCount = 0 @state() elements: ISourceView[] = [] @query('.sources') _sourcesContainer!: HTMLElement resizeObserver: ResizeObserver | null = null childWidth = 48 lineWidth = 150 statusCodeWeights = { 1: 1, 2: 2, 3: 3, 4: 0, } connectedCallback(): void { super.connectedCallback() this.resizeObserver = new ResizeObserver(this.handleResize) } firstUpdated(): void { this.resizeObserver?.observe(this._sourcesContainer) } willUpdate(changedProperties: PropertyValueMap | Map): void { if (changedProperties.has('show')) { if (this.show) this._sourcesContainer && this.resizeObserver?.observe(this._sourcesContainer) else this._sourcesContainer && this.resizeObserver?.unobserve(this._sourcesContainer) } if (changedProperties.has('hiddenCount') || changedProperties.has('sources') || changedProperties.has('disabled')) { const newHiddenCount = changedProperties.get('hiddenCount') const oldHiddenCount = this.hiddenCount if (newHiddenCount !== oldHiddenCount) this.updateElements() } } disconnectedCallback(): void { this.resizeObserver?.disconnect() super.disconnectedCallback() } render() { const loadingStatuses = [1, 2] return html`

${this.disabled ? `${localizeManager.loading_data_sources()}` : this.elements.length ? `${localizeManager.data_sources()}:` : localizeManager.no_data_sources()}

${this.elements.map(source => html` ${when(!this.disabled, () => html`
source image ${when(loadingStatuses.includes(source.adaptedStatusCode), () => svg` `)}
`)} `)}
+${this.hiddenCount}
${this.renderButtonApps()}
` } renderButtonApps() { if (!this.disableApps) { return html` ${localizeManager.add_apps()} ` } else { return null } } updateElements() { const filteredElements = this.filterSources(this.sources) const elements = this.mapSourcesToElements(filteredElements) this.elements = elements } getPercentProgress(source: ISource) { const percentProgress = source.percentProgress || 0 if (percentProgress <= 5) return 5 return percentProgress } mapSourcesToElements(sources: ISource[]) { return sources.map((source, index, sources) => { const percentProgress = this.getPercentProgress(source) return { ...source, isHidden: index >= sources.length - this.hiddenCount, percentProgress, dashoffset: this.lineWidth - (this.lineWidth * (percentProgress || 0)) / 100, } }) } filterSources(sources: ISource[]) { const localizedSources = sources.map(source => ({ ...source, fieldForSortingByTitle: source.title ? source.title : source.name, })) const sortByTitle = (a: any, b: any) => b.fieldForSortingByTitle > a.fieldForSortingByTitle ? -1 : 1 return [ ...localizedSources.filter(item => item?.statusCode && actionConnectingApp.includes(item.statusCode) && item.status === 'Active').sort(sortByTitle), ...localizedSources.filter(item => item.status === 'Disconnecting').sort(sortByTitle), ...localizedSources.filter(item => item?.statusCode && actionFailedConnectinggApp.includes(item.statusCode) && item.status === 'Active').sort(sortByTitle), ...localizedSources.filter(item => item.status === 'Suspended').sort(sortByTitle), ...localizedSources.filter(item => item?.statusCode && actionConnectedApp.includes(item.statusCode) && item.status === 'Active').sort(sortByTitle), ] } handleResize = () => { this.hiddenCount = this.calculateHiddenComponentsCount() } calculateHiddenComponentsCount() { if (!this._sourcesContainer) return 0 const elementWidth = 48 const gapElementWidth = 4 const elementsLength = this.elements.length const oneDigitIndicatorWidth = 21 const twoDigitIndicatorWidth = 28 const containerWidth = this._sourcesContainer.offsetWidth const totalElementsWidth = elementsLength * elementWidth + (elementsLength - 1) * gapElementWidth const totalWidth = totalElementsWidth const calculate = (totalW: number, containerW: number, indicatorW = 0) => { const totalWidth = totalW + indicatorW const overflowWidth = totalWidth - containerW const hiddenCount = Math.ceil(overflowWidth / (elementWidth + gapElementWidth)) if (indicatorW === 0) { const indicatorWidth = hiddenCount >= 10 ? twoDigitIndicatorWidth : oneDigitIndicatorWidth return calculate(totalWidth, containerW, indicatorWidth) } return hiddenCount } return totalWidth > containerWidth ? calculate(totalWidth, containerWidth) : 0 } handleEnter(e: KeyboardEvent) { if (e.key === 'Enter') this.dispatchRedirectEvent() } dispatchRedirectEvent() { dispatchCustomEvent('redirect', null, this) } canClickElementSource() { return (!this.readonly || !this.disableApps) } private getContainerClasses() { const widgetViewsClasses = this.widgetView.split(' ') return classMap({ 'data-source': true, 'hide': !this.show, ...widgetViewsClasses.reduce((acc, className) => ({ ...acc, [className]: true }), {}), }) } private getSourceClasses(source: ISourceView) { return classMap({ source: true, source_suspended: source.status === 'Suspended', source_loading: source.adaptedStatusCode === 1 || source.adaptedStatusCode === 2, source_error: source.adaptedStatusCode === 4, source_hidden: !!source.isHidden, source_disable: this.readonly || this.disableApps, }) } private getIndicatorClasses() { return classMap({ indicator: true, indicator_active: this.hiddenCount > 0 && !this.disabled, }) } }