mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
feat: plausible metrics chart
This commit is contained in:
parent
0e26f463e9
commit
a547e51bc9
@ -6,6 +6,7 @@ import { PageHeader } from 'component/common/PageHeader/PageHeader.tsx';
|
|||||||
import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
import { useImpactMetricsMetadata } from 'hooks/api/getters/useImpactMetricsMetadata/useImpactMetricsMetadata';
|
||||||
import { ChartConfigModal } from './ChartConfigModal/ChartConfigModal.tsx';
|
import { ChartConfigModal } from './ChartConfigModal/ChartConfigModal.tsx';
|
||||||
import { ChartItem } from './ChartItem.tsx';
|
import { ChartItem } from './ChartItem.tsx';
|
||||||
|
import { PlausibleChartItem } from './PlausibleChartItem.tsx';
|
||||||
import { GridLayoutWrapper, type GridItem } from './GridLayoutWrapper.tsx';
|
import { GridLayoutWrapper, type GridItem } from './GridLayoutWrapper.tsx';
|
||||||
import { useImpactMetricsState } from './hooks/useImpactMetricsState.ts';
|
import { useImpactMetricsState } from './hooks/useImpactMetricsState.ts';
|
||||||
import type { ChartConfig } from './types.ts';
|
import type { ChartConfig } from './types.ts';
|
||||||
@ -13,6 +14,7 @@ import useToast from 'hooks/useToast';
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton.tsx';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton.tsx';
|
||||||
import { ADMIN } from '../providers/AccessProvider/permissions.ts';
|
import { ADMIN } from '../providers/AccessProvider/permissions.ts';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
const StyledEmptyState = styled(Paper)(({ theme }) => ({
|
const StyledEmptyState = styled(Paper)(({ theme }) => ({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
@ -39,6 +41,7 @@ export const ImpactMetrics: FC = () => {
|
|||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [editingChart, setEditingChart] = useState<ChartConfig | undefined>();
|
const [editingChart, setEditingChart] = useState<ChartConfig | undefined>();
|
||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
|
const plausibleMetricsEnabled = useUiFlag('plausibleMetrics');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
charts,
|
charts,
|
||||||
@ -100,33 +103,56 @@ export const ImpactMetrics: FC = () => {
|
|||||||
[deleteChart],
|
[deleteChart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const gridItems: GridItem[] = useMemo(
|
const gridItems: GridItem[] = useMemo(() => {
|
||||||
() =>
|
const items: GridItem[] = [];
|
||||||
charts.map((config, index) => {
|
|
||||||
const existingLayout = layout?.find(
|
if (plausibleMetricsEnabled) {
|
||||||
(item) => item.i === config.id,
|
const plausibleChartItem: GridItem = {
|
||||||
);
|
id: 'plausible-analytics',
|
||||||
return {
|
component: <PlausibleChartItem />,
|
||||||
id: config.id,
|
w: 12,
|
||||||
component: (
|
h: 4,
|
||||||
<ChartItem
|
x: 0,
|
||||||
config={config}
|
y: 0,
|
||||||
onEdit={handleEditChart}
|
minW: 6,
|
||||||
onDelete={handleDeleteChart}
|
minH: 3,
|
||||||
/>
|
maxW: 12,
|
||||||
),
|
maxH: 6,
|
||||||
w: existingLayout?.w ?? 6,
|
};
|
||||||
h: existingLayout?.h ?? 4,
|
items.push(plausibleChartItem);
|
||||||
x: existingLayout?.x,
|
}
|
||||||
y: existingLayout?.y,
|
|
||||||
minW: 4,
|
const impactMetricsItems: GridItem[] = charts.map((config, index) => {
|
||||||
minH: 2,
|
const existingLayout = layout?.find((item) => item.i === config.id);
|
||||||
maxW: 12,
|
const yOffset = plausibleMetricsEnabled ? 4 : 0;
|
||||||
maxH: 8,
|
return {
|
||||||
};
|
id: config.id,
|
||||||
}),
|
component: (
|
||||||
[charts, layout, handleEditChart, handleDeleteChart],
|
<ChartItem
|
||||||
);
|
config={config}
|
||||||
|
onEdit={handleEditChart}
|
||||||
|
onDelete={handleDeleteChart}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
w: existingLayout?.w ?? 6,
|
||||||
|
h: existingLayout?.h ?? 4,
|
||||||
|
x: existingLayout?.x,
|
||||||
|
y: existingLayout?.y ? existingLayout.y + yOffset : yOffset,
|
||||||
|
minW: 4,
|
||||||
|
minH: 2,
|
||||||
|
maxW: 12,
|
||||||
|
maxH: 8,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...items, ...impactMetricsItems];
|
||||||
|
}, [
|
||||||
|
charts,
|
||||||
|
layout,
|
||||||
|
handleEditChart,
|
||||||
|
handleDeleteChart,
|
||||||
|
plausibleMetricsEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
const hasError = metadataError || settingsError;
|
const hasError = metadataError || settingsError;
|
||||||
const isLoading = metadataLoading || settingsLoading;
|
const isLoading = metadataLoading || settingsLoading;
|
||||||
@ -156,7 +182,7 @@ export const ImpactMetrics: FC = () => {
|
|||||||
{charts.length === 0 && !isLoading && !hasError ? (
|
{charts.length === 0 && !isLoading && !hasError ? (
|
||||||
<StyledEmptyState>
|
<StyledEmptyState>
|
||||||
<Typography variant='h6' gutterBottom>
|
<Typography variant='h6' gutterBottom>
|
||||||
No charts configured
|
No impact metrics charts configured
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant='body2'
|
variant='body2'
|
||||||
@ -175,7 +201,7 @@ export const ImpactMetrics: FC = () => {
|
|||||||
Add Chart
|
Add Chart
|
||||||
</Button>
|
</Button>
|
||||||
</StyledEmptyState>
|
</StyledEmptyState>
|
||||||
) : charts.length > 0 ? (
|
) : gridItems.length > 0 ? (
|
||||||
<GridLayoutWrapper items={gridItems} />
|
<GridLayoutWrapper items={gridItems} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
173
frontend/src/component/impact-metrics/PlausibleChart.tsx
Normal file
173
frontend/src/component/impact-metrics/PlausibleChart.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import type { FC, ReactNode } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Alert, Box } from '@mui/material';
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
NotEnoughData,
|
||||||
|
} from '../insights/components/LineChart/LineChart.tsx';
|
||||||
|
import { usePlausibleMetrics } from 'hooks/api/getters/usePlausibleMetrics/usePlausibleMetrics';
|
||||||
|
import { usePlaceholderData } from '../insights/hooks/usePlaceholderData.js';
|
||||||
|
import { formatLargeNumbers } from './metricsFormatters.js';
|
||||||
|
import type { ChartData } from 'chart.js';
|
||||||
|
|
||||||
|
type PlausibleChartProps = {
|
||||||
|
aspectRatio?: number;
|
||||||
|
overrideOptions?: Record<string, unknown>;
|
||||||
|
errorTitle?: string;
|
||||||
|
emptyDataDescription?: string;
|
||||||
|
noSeriesPlaceholder?: ReactNode;
|
||||||
|
isPreview?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PlausibleChart: FC<PlausibleChartProps> = ({
|
||||||
|
aspectRatio,
|
||||||
|
overrideOptions = {},
|
||||||
|
errorTitle = 'Failed to load Plausible metrics.',
|
||||||
|
emptyDataDescription = 'No Plausible analytics data available.',
|
||||||
|
noSeriesPlaceholder,
|
||||||
|
isPreview,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
data: plausibleData,
|
||||||
|
loading: dataLoading,
|
||||||
|
error: dataError,
|
||||||
|
} = usePlausibleMetrics();
|
||||||
|
|
||||||
|
const placeholderData = usePlaceholderData({
|
||||||
|
fill: true,
|
||||||
|
type: 'constant',
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData: ChartData<'line'> = useMemo(() => {
|
||||||
|
if (!plausibleData?.data || plausibleData.data.length === 0) {
|
||||||
|
return {
|
||||||
|
labels: [],
|
||||||
|
datasets: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedData = [...plausibleData.data].sort(
|
||||||
|
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels: sortedData.map((item) => item.date),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Events',
|
||||||
|
data: sortedData.map((item) => item.count),
|
||||||
|
borderColor: 'rgb(129, 122, 254)',
|
||||||
|
backgroundColor: 'rgba(129, 122, 254, 0.1)',
|
||||||
|
fill: true,
|
||||||
|
tension: 0.1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}, [plausibleData]);
|
||||||
|
|
||||||
|
const hasError = !!dataError;
|
||||||
|
const isLoading = dataLoading;
|
||||||
|
const notEnoughData = useMemo(
|
||||||
|
() =>
|
||||||
|
!isLoading &&
|
||||||
|
(!plausibleData?.data ||
|
||||||
|
plausibleData.data.length === 0 ||
|
||||||
|
!chartData.datasets.some((d) => d.data.length > 1)),
|
||||||
|
[chartData, isLoading, plausibleData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const placeholder = noSeriesPlaceholder ? (
|
||||||
|
noSeriesPlaceholder
|
||||||
|
) : (
|
||||||
|
<NotEnoughData
|
||||||
|
title='No Plausible data available'
|
||||||
|
description={emptyDataDescription}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const cover = notEnoughData ? placeholder : isLoading;
|
||||||
|
|
||||||
|
const chartOptions = {
|
||||||
|
...overrideOptions,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
unit: 'hour',
|
||||||
|
displayFormats: {
|
||||||
|
hour: 'MMM dd, HH:mm',
|
||||||
|
},
|
||||||
|
tooltipFormat: 'PPpp',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
maxRotation: 45,
|
||||||
|
minRotation: 45,
|
||||||
|
maxTicksLimit: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Events',
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
precision: 0,
|
||||||
|
callback: (value: unknown): string | number =>
|
||||||
|
typeof value === 'number'
|
||||||
|
? formatLargeNumbers(value)
|
||||||
|
: (value as number),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'bottom' as const,
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
boxWidth: 8,
|
||||||
|
padding: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animations: {
|
||||||
|
x: { duration: 0 },
|
||||||
|
y: { duration: 0 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={
|
||||||
|
!isPreview
|
||||||
|
? {
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
'& > div': {
|
||||||
|
height: '100% !important',
|
||||||
|
width: '100% !important',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<LineChart
|
||||||
|
data={
|
||||||
|
notEnoughData || isLoading ? placeholderData : chartData
|
||||||
|
}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
overrideOptions={chartOptions}
|
||||||
|
cover={
|
||||||
|
hasError ? (
|
||||||
|
<Alert severity='error'>{errorTitle}</Alert>
|
||||||
|
) : (
|
||||||
|
cover
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
69
frontend/src/component/impact-metrics/PlausibleChartItem.tsx
Normal file
69
frontend/src/component/impact-metrics/PlausibleChartItem.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
import { Box, Typography, styled, Paper } from '@mui/material';
|
||||||
|
import { PlausibleChart } from './PlausibleChart.tsx';
|
||||||
|
|
||||||
|
const StyledWidget = styled(Paper)(({ theme }) => ({
|
||||||
|
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||||
|
boxShadow: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledChartContent = styled(Box)({
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledImpactChartContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
minWidth: 0,
|
||||||
|
flexGrow: 1,
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
margin: 'auto 0',
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledHeader = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing(1.5, 2),
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledChartTitle = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const PlausibleChartItem: FC = () => (
|
||||||
|
<StyledWidget>
|
||||||
|
<StyledHeader>
|
||||||
|
<StyledChartTitle>
|
||||||
|
<Typography variant='h6'>Plausible Analytics</Typography>
|
||||||
|
<Typography variant='body2' color='text.secondary'>
|
||||||
|
Favorite events from Plausible analytics
|
||||||
|
</Typography>
|
||||||
|
</StyledChartTitle>
|
||||||
|
</StyledHeader>
|
||||||
|
|
||||||
|
<StyledChartContent>
|
||||||
|
<StyledImpactChartContainer>
|
||||||
|
<PlausibleChart
|
||||||
|
aspectRatio={1.5}
|
||||||
|
overrideOptions={{ maintainAspectRatio: false }}
|
||||||
|
emptyDataDescription='No Plausible analytics data available for favorite events.'
|
||||||
|
/>
|
||||||
|
</StyledImpactChartContainer>
|
||||||
|
</StyledChartContent>
|
||||||
|
</StyledWidget>
|
||||||
|
);
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { fetcher, useApiGetter } from '../useApiGetter/useApiGetter.js';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import type { PlausibleMetricsResponseSchema } from 'openapi/models/plausibleMetricsResponseSchema.js';
|
||||||
|
|
||||||
|
export const usePlausibleMetrics = () => {
|
||||||
|
const PATH = 'api/admin/impact-metrics/plausible';
|
||||||
|
const { data, refetch, loading, error } =
|
||||||
|
useApiGetter<PlausibleMetricsResponseSchema>(
|
||||||
|
formatApiPath(PATH),
|
||||||
|
() => fetcher(formatApiPath(PATH), 'Plausible metrics'),
|
||||||
|
{
|
||||||
|
refreshInterval: 30 * 1_000,
|
||||||
|
revalidateOnFocus: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data || { data: [] },
|
||||||
|
refetch,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -85,6 +85,7 @@ export type UiFlags = {
|
|||||||
edgeObservability?: boolean;
|
edgeObservability?: boolean;
|
||||||
customMetrics?: boolean;
|
customMetrics?: boolean;
|
||||||
impactMetrics?: boolean;
|
impactMetrics?: boolean;
|
||||||
|
plausibleMetrics?: boolean;
|
||||||
lifecycleGraphs?: boolean;
|
lifecycleGraphs?: boolean;
|
||||||
newStrategyModal?: boolean;
|
newStrategyModal?: boolean;
|
||||||
globalChangeRequestList?: boolean;
|
globalChangeRequestList?: boolean;
|
||||||
|
|||||||
@ -141,7 +141,6 @@ export default class ClientApplicationsStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
async upsert(details: Partial<IClientApplication>): Promise<void> {
|
async upsert(details: Partial<IClientApplication>): Promise<void> {
|
||||||
const stopTimer = this.timer('upsert');
|
|
||||||
const row = remapRow(details);
|
const row = remapRow(details);
|
||||||
await this.db(TABLE).insert(row).onConflict('app_name').merge();
|
await this.db(TABLE).insert(row).onConflict('app_name').merge();
|
||||||
const usageRows = this.remapUsageRow(details);
|
const usageRows = this.remapUsageRow(details);
|
||||||
@ -149,11 +148,9 @@ export default class ClientApplicationsStore
|
|||||||
.insert(usageRows)
|
.insert(usageRows)
|
||||||
.onConflict(['app_name', 'project', 'environment'])
|
.onConflict(['app_name', 'project', 'environment'])
|
||||||
.merge();
|
.merge();
|
||||||
stopTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkUpsert(apps: Partial<IClientApplication>[]): Promise<void> {
|
async bulkUpsert(apps: Partial<IClientApplication>[]): Promise<void> {
|
||||||
const stopTimer = this.timer('bulkUpsert');
|
|
||||||
const rows = apps.map(remapRow);
|
const rows = apps.map(remapRow);
|
||||||
const uniqueRows = Object.values(
|
const uniqueRows = Object.values(
|
||||||
rows.reduce((acc, row) => {
|
rows.reduce((acc, row) => {
|
||||||
@ -179,38 +176,33 @@ export default class ClientApplicationsStore
|
|||||||
.insert(uniqueUsageRows)
|
.insert(uniqueUsageRows)
|
||||||
.onConflict(['app_name', 'project', 'environment'])
|
.onConflict(['app_name', 'project', 'environment'])
|
||||||
.merge();
|
.merge();
|
||||||
stopTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async exists(appName: string): Promise<boolean> {
|
async exists(appName: string): Promise<boolean> {
|
||||||
const stopTimer = this.timer('exists');
|
|
||||||
const result = await this.db.raw(
|
const result = await this.db.raw(
|
||||||
`SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE app_name = ?) AS present`,
|
`SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE app_name = ?) AS present`,
|
||||||
[appName],
|
[appName],
|
||||||
);
|
);
|
||||||
const { present } = result.rows[0];
|
const { present } = result.rows[0];
|
||||||
stopTimer();
|
|
||||||
return present;
|
return present;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(): Promise<IClientApplication[]> {
|
async getAll(): Promise<IClientApplication[]> {
|
||||||
const stopTimer = this.timer('getAll');
|
|
||||||
const rows = await this.db
|
const rows = await this.db
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
.orderBy('app_name', 'asc');
|
.orderBy('app_name', 'asc');
|
||||||
stopTimer();
|
|
||||||
return rows.map(mapRow);
|
return rows.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApplication(appName: string): Promise<IClientApplication> {
|
async getApplication(appName: string): Promise<IClientApplication> {
|
||||||
const stopTimer = this.timer('getApplication');
|
|
||||||
const row = await this.db
|
const row = await this.db
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
.where('app_name', appName)
|
.where('app_name', appName)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
.first();
|
.first();
|
||||||
stopTimer();
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new NotFoundError(`Could not find appName=${appName}`);
|
throw new NotFoundError(`Could not find appName=${appName}`);
|
||||||
}
|
}
|
||||||
@ -225,7 +217,6 @@ export default class ClientApplicationsStore
|
|||||||
async getApplications(
|
async getApplications(
|
||||||
params: IClientApplicationsSearchParams,
|
params: IClientApplicationsSearchParams,
|
||||||
): Promise<IClientApplications> {
|
): Promise<IClientApplications> {
|
||||||
const stopTimer = this.timer('getApplications');
|
|
||||||
const { limit, offset, sortOrder = 'asc', searchParams } = params;
|
const { limit, offset, sortOrder = 'asc', searchParams } = params;
|
||||||
const validatedSortOrder =
|
const validatedSortOrder =
|
||||||
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
||||||
@ -266,7 +257,6 @@ export default class ClientApplicationsStore
|
|||||||
.whereBetween('rank', [offset + 1, offset + limit]);
|
.whereBetween('rank', [offset + 1, offset + limit]);
|
||||||
|
|
||||||
const rows = await query;
|
const rows = await query;
|
||||||
stopTimer();
|
|
||||||
|
|
||||||
if (rows.length !== 0) {
|
if (rows.length !== 0) {
|
||||||
const applications = reduceRows(rows);
|
const applications = reduceRows(rows);
|
||||||
@ -283,11 +273,9 @@ export default class ClientApplicationsStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUnannounced(): Promise<IClientApplication[]> {
|
async getUnannounced(): Promise<IClientApplication[]> {
|
||||||
const stopTimer = this.timer('getUnannounced');
|
|
||||||
const rows = await this.db(TABLE)
|
const rows = await this.db(TABLE)
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
.where('announced', false);
|
.where('announced', false);
|
||||||
stopTimer();
|
|
||||||
return rows.map(mapRow);
|
return rows.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,38 +284,31 @@ export default class ClientApplicationsStore
|
|||||||
* @return {[app]} - Apps that hadn't been announced
|
* @return {[app]} - Apps that hadn't been announced
|
||||||
*/
|
*/
|
||||||
async setUnannouncedToAnnounced(): Promise<IClientApplication[]> {
|
async setUnannouncedToAnnounced(): Promise<IClientApplication[]> {
|
||||||
const stopTimer = this.timer('setUnannouncedToAnnounced');
|
|
||||||
const rows = await this.db(TABLE)
|
const rows = await this.db(TABLE)
|
||||||
.update({ announced: true })
|
.update({ announced: true })
|
||||||
.where('announced', false)
|
.where('announced', false)
|
||||||
.whereNotNull('announced')
|
.whereNotNull('announced')
|
||||||
.returning(COLUMNS);
|
.returning(COLUMNS);
|
||||||
stopTimer();
|
|
||||||
return rows.map(mapRow);
|
return rows.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(key: string): Promise<void> {
|
async delete(key: string): Promise<void> {
|
||||||
const stopTimer = this.timer('delete');
|
|
||||||
await this.db(TABLE).where('app_name', key).del();
|
await this.db(TABLE).where('app_name', key).del();
|
||||||
stopTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAll(): Promise<void> {
|
async deleteAll(): Promise<void> {
|
||||||
const stopTimer = this.timer('deleteAll');
|
|
||||||
await this.db(TABLE).del();
|
await this.db(TABLE).del();
|
||||||
stopTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {}
|
destroy(): void {}
|
||||||
|
|
||||||
async get(appName: string): Promise<IClientApplication> {
|
async get(appName: string): Promise<IClientApplication> {
|
||||||
const stopTimer = this.timer('get');
|
|
||||||
const row = await this.db
|
const row = await this.db
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
.where('app_name', appName)
|
.where('app_name', appName)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
.first();
|
.first();
|
||||||
stopTimer();
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new NotFoundError(`Could not find appName=${appName}`);
|
throw new NotFoundError(`Could not find appName=${appName}`);
|
||||||
}
|
}
|
||||||
@ -500,11 +481,10 @@ export default class ClientApplicationsStore
|
|||||||
};
|
};
|
||||||
|
|
||||||
async removeInactiveApplications(): Promise<number> {
|
async removeInactiveApplications(): Promise<number> {
|
||||||
const stopTimer = this.timer('removeInactiveApplications');
|
|
||||||
const rows = await this.db(TABLE)
|
const rows = await this.db(TABLE)
|
||||||
.whereRaw("seen_at < now() - interval '30 days'")
|
.whereRaw("seen_at < now() - interval '30 days'")
|
||||||
.del();
|
.del();
|
||||||
stopTimer();
|
|
||||||
if (rows > 0) {
|
if (rows > 0) {
|
||||||
this.logger.debug(`Deleted ${rows} applications`);
|
this.logger.debug(`Deleted ${rows} applications`);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user