mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 09:38:26 +00:00
filter operations by client name (#2487)
This commit is contained in:
parent
048a250320
commit
90e242bc07
11 changed files with 429 additions and 35 deletions
|
|
@ -16,6 +16,7 @@ export default gql`
|
|||
target: ID!
|
||||
period: DateRangeInput!
|
||||
operations: [ID!]
|
||||
clientNames: [String!]
|
||||
}
|
||||
|
||||
input OperationBodyByHashInput {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,12 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
}: { period: DateRange; operations?: readonly string[] } & TargetSelector) {
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info('Counting unique operations (period=%o, target=%s)', period, target);
|
||||
await this.authManager.ensureTargetAccess({
|
||||
organization,
|
||||
|
|
@ -140,6 +145,7 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +171,11 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
}: { period: DateRange; operations?: readonly string[] } & Listify<TargetSelector, 'target'>) {
|
||||
clients,
|
||||
}: { period: DateRange; operations?: readonly string[]; clients?: readonly string[] } & Listify<
|
||||
TargetSelector,
|
||||
'target'
|
||||
>) {
|
||||
this.logger.info('Counting requests (period=%s, target=%s)', period, target);
|
||||
await this.authManager.ensureTargetAccess({
|
||||
organization,
|
||||
|
|
@ -179,6 +189,7 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
})
|
||||
.then(r => r.total);
|
||||
}
|
||||
|
|
@ -189,7 +200,12 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
}: { period: DateRange; operations?: readonly string[] } & TargetSelector) {
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info('Counting failures (period=%o, target=%s)', period, target);
|
||||
await this.authManager.ensureTargetAccess({
|
||||
organization,
|
||||
|
|
@ -202,6 +218,7 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -311,9 +328,11 @@ export class OperationsManager {
|
|||
project,
|
||||
target,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info('Reading operations stats (period=%o, target=%s)', period, target);
|
||||
await this.authManager.ensureTargetAccess({
|
||||
|
|
@ -328,6 +347,7 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -441,10 +461,12 @@ export class OperationsManager {
|
|||
project,
|
||||
target,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
resolution: number;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info(
|
||||
'Reading requests over time (period=%o, resolution=%s, target=%s)',
|
||||
|
|
@ -464,6 +486,7 @@ export class OperationsManager {
|
|||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -474,10 +497,12 @@ export class OperationsManager {
|
|||
project,
|
||||
target,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
resolution: number;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info(
|
||||
'Reading failures over time (period=%o, resolution=%s, target=%s)',
|
||||
|
|
@ -497,6 +522,7 @@ export class OperationsManager {
|
|||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -507,10 +533,12 @@ export class OperationsManager {
|
|||
project,
|
||||
target,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
resolution: number;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info(
|
||||
'Reading duration over time (period=%o, resolution=%s, target=%s)',
|
||||
|
|
@ -530,6 +558,7 @@ export class OperationsManager {
|
|||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -539,7 +568,12 @@ export class OperationsManager {
|
|||
project,
|
||||
target,
|
||||
operations,
|
||||
}: { period: DateRange; operations?: readonly string[] } & TargetSelector) {
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info('Reading overall duration percentiles (period=%o, target=%s)', period, target);
|
||||
await this.authManager.ensureTargetAccess({
|
||||
organization,
|
||||
|
|
@ -552,6 +586,7 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -562,11 +597,17 @@ export class OperationsManager {
|
|||
project,
|
||||
target,
|
||||
operations,
|
||||
}: { period: DateRange; operations?: readonly string[] } & TargetSelector) {
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info(
|
||||
'Reading detailed duration percentiles (period=%o, target=%s)',
|
||||
'Reading detailed duration percentiles (period=%o, target=%s, clientFilter=%s)',
|
||||
period,
|
||||
target,
|
||||
clients,
|
||||
);
|
||||
await this.authManager.ensureTargetAccess({
|
||||
organization,
|
||||
|
|
@ -579,6 +620,7 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -588,7 +630,12 @@ export class OperationsManager {
|
|||
project,
|
||||
target,
|
||||
operations,
|
||||
}: { period: DateRange; operations?: readonly string[] } & TargetSelector) {
|
||||
clients,
|
||||
}: {
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
} & TargetSelector) {
|
||||
this.logger.info('Reading duration histogram (period=%o, target=%s)', period, target);
|
||||
await this.authManager.ensureTargetAccess({
|
||||
organization,
|
||||
|
|
@ -601,6 +648,7 @@ export class OperationsManager {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -262,10 +262,12 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string | readonly string[];
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
},
|
||||
span?: Span,
|
||||
): Promise<{
|
||||
|
|
@ -281,6 +283,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
},
|
||||
)}`,
|
||||
queryId: 'count_operations_daily',
|
||||
|
|
@ -293,6 +296,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
},
|
||||
)}`,
|
||||
queryId: 'count_operations_hourly',
|
||||
|
|
@ -305,6 +309,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
},
|
||||
)}`,
|
||||
queryId: 'count_operations_regular',
|
||||
|
|
@ -335,12 +340,14 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
}): Promise<number> {
|
||||
return this.countOperations({ target, period, operations }).then(r => r.notOk);
|
||||
return this.countOperations({ target, period, operations, clients }).then(r => r.notOk);
|
||||
}
|
||||
|
||||
@sentry('OperationsReader.countUniqueDocuments')
|
||||
|
|
@ -349,10 +356,12 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
},
|
||||
span?: Span,
|
||||
): Promise<number> {
|
||||
|
|
@ -366,6 +375,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
})}
|
||||
`,
|
||||
queryId: 'count_unique_documents_daily',
|
||||
|
|
@ -380,6 +390,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
})}
|
||||
`,
|
||||
queryId: 'count_unique_documents_hourly',
|
||||
|
|
@ -394,6 +405,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
})}
|
||||
`,
|
||||
queryId: 'count_unique_documents',
|
||||
|
|
@ -417,10 +429,12 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
},
|
||||
span?: Span,
|
||||
): Promise<
|
||||
|
|
@ -443,6 +457,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
})}
|
||||
GROUP BY hash
|
||||
`,
|
||||
|
|
@ -461,6 +476,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
})}
|
||||
GROUP BY hash
|
||||
`,
|
||||
|
|
@ -476,6 +492,7 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
})}
|
||||
GROUP BY hash
|
||||
`,
|
||||
|
|
@ -1043,17 +1060,20 @@ export class OperationsReader {
|
|||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
resolution: number;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
}) {
|
||||
const results = await this.getDurationAndCountOverTime({
|
||||
target,
|
||||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
|
||||
return results.map(row => ({
|
||||
|
|
@ -1068,17 +1088,20 @@ export class OperationsReader {
|
|||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
resolution: number;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
}) {
|
||||
const result = await this.getDurationAndCountOverTime({
|
||||
target,
|
||||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
|
||||
return result.map(row => ({
|
||||
|
|
@ -1093,11 +1116,13 @@ export class OperationsReader {
|
|||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
resolution: number;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
}): Promise<
|
||||
Array<{
|
||||
date: any;
|
||||
|
|
@ -1109,6 +1134,7 @@ export class OperationsReader {
|
|||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1118,10 +1144,12 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
},
|
||||
span?: Span,
|
||||
): Promise<
|
||||
|
|
@ -1143,7 +1171,7 @@ export class OperationsReader {
|
|||
(
|
||||
SELECT log10(duration) as logDuration
|
||||
FROM operations_new
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
)
|
||||
ORDER BY latency
|
||||
`,
|
||||
|
|
@ -1166,10 +1194,12 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
},
|
||||
span?: Span,
|
||||
): Promise<Percentiles> {
|
||||
|
|
@ -1183,7 +1213,7 @@ export class OperationsReader {
|
|||
SELECT
|
||||
quantilesMerge(0.75, 0.90, 0.95, 0.99)(duration_quantiles) as percentiles
|
||||
FROM operations_daily
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
`,
|
||||
queryId: 'general_duration_percentiles_daily',
|
||||
timeout: 15_000,
|
||||
|
|
@ -1194,7 +1224,7 @@ export class OperationsReader {
|
|||
SELECT
|
||||
quantilesMerge(0.75, 0.90, 0.95, 0.99)(duration_quantiles) as percentiles
|
||||
FROM operations_hourly
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
`,
|
||||
queryId: 'general_duration_percentiles_hourly',
|
||||
timeout: 15_000,
|
||||
|
|
@ -1205,7 +1235,7 @@ export class OperationsReader {
|
|||
SELECT
|
||||
quantiles(0.75, 0.90, 0.95, 0.99)(duration) as percentiles
|
||||
FROM operations
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
`,
|
||||
queryId: 'general_duration_percentiles_regular',
|
||||
timeout: 15_000,
|
||||
|
|
@ -1225,10 +1255,12 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
},
|
||||
span?: Span,
|
||||
) {
|
||||
|
|
@ -1244,7 +1276,7 @@ export class OperationsReader {
|
|||
hash,
|
||||
quantilesMerge(0.75, 0.90, 0.95, 0.99)(duration_quantiles) as percentiles
|
||||
FROM operations_daily
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
GROUP BY hash
|
||||
`,
|
||||
queryId: 'duration_percentiles_daily',
|
||||
|
|
@ -1257,7 +1289,7 @@ export class OperationsReader {
|
|||
hash,
|
||||
quantilesMerge(0.75, 0.90, 0.95, 0.99)(duration_quantiles) as percentiles
|
||||
FROM operations_hourly
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
GROUP BY hash
|
||||
`,
|
||||
queryId: 'duration_percentiles_hourly',
|
||||
|
|
@ -1270,7 +1302,7 @@ export class OperationsReader {
|
|||
hash,
|
||||
quantiles(0.75, 0.90, 0.95, 0.99)(duration) as percentiles
|
||||
FROM operations
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
GROUP BY hash
|
||||
`,
|
||||
queryId: 'duration_percentiles_regular',
|
||||
|
|
@ -1319,11 +1351,13 @@ export class OperationsReader {
|
|||
period,
|
||||
resolution,
|
||||
operations,
|
||||
clients,
|
||||
}: {
|
||||
target: string;
|
||||
period: DateRange;
|
||||
resolution: number;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
},
|
||||
span?: Span,
|
||||
) {
|
||||
|
|
@ -1350,7 +1384,8 @@ export class OperationsReader {
|
|||
sum(total) as total,
|
||||
sum(total_ok) as totalOk
|
||||
FROM operations_daily
|
||||
${this.createFilter({ target, period, operations })}
|
||||
}: {
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
GROUP BY date
|
||||
ORDER BY date
|
||||
`,
|
||||
|
|
@ -1372,7 +1407,7 @@ export class OperationsReader {
|
|||
sum(total) as total,
|
||||
sum(total_ok) as totalOk
|
||||
FROM operations_hourly
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
GROUP BY date
|
||||
ORDER BY date
|
||||
`,
|
||||
|
|
@ -1394,7 +1429,7 @@ export class OperationsReader {
|
|||
count(*) as total,
|
||||
sum(ok) as totalOk
|
||||
FROM operations
|
||||
${this.createFilter({ target, period, operations })}
|
||||
${this.createFilter({ target, period, operations, clients })}
|
||||
GROUP BY date
|
||||
ORDER BY date
|
||||
`,
|
||||
|
|
@ -1638,11 +1673,13 @@ export class OperationsReader {
|
|||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
extra = [],
|
||||
}: {
|
||||
target?: string | readonly string[];
|
||||
period?: DateRange;
|
||||
operations?: readonly string[];
|
||||
clients?: readonly string[];
|
||||
extra?: SqlValue[];
|
||||
}): SqlValue {
|
||||
const where: SqlValue[] = [];
|
||||
|
|
@ -1666,6 +1703,10 @@ export class OperationsReader {
|
|||
where.push(sql`(hash) IN (${sql.array(operations, 'String')})`);
|
||||
}
|
||||
|
||||
if (clients?.length) {
|
||||
where.push(sql`"client_name" IN (${sql.array(clients, 'String')})`);
|
||||
}
|
||||
|
||||
if (extra.length) {
|
||||
where.push(...extra);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
project,
|
||||
target,
|
||||
operations,
|
||||
clients:
|
||||
// TODO: figure out if the mapping should actually happen here :thinking:
|
||||
selector.clientNames?.map(clientName => (clientName === 'unknown' ? '' : clientName)) ??
|
||||
[],
|
||||
};
|
||||
},
|
||||
async clientStatsByTargets(_, { selector }, { injector }) {
|
||||
|
|
@ -124,8 +128,8 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
},
|
||||
OperationsStats: {
|
||||
async operations(
|
||||
{ organization, project, target, period, operations: operationsFilter },
|
||||
_,
|
||||
{ organization, project, target, period, operations: operationsFilter, clients },
|
||||
args,
|
||||
{ injector },
|
||||
) {
|
||||
const operationsManager = injector.get(OperationsManager);
|
||||
|
|
@ -136,6 +140,7 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
target,
|
||||
period,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
}),
|
||||
operationsManager.readDetailedDurationPercentiles({
|
||||
organization,
|
||||
|
|
@ -143,6 +148,7 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
target,
|
||||
period,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
@ -161,17 +167,18 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
})
|
||||
.sort((a, b) => b.count - a.count);
|
||||
},
|
||||
totalRequests({ organization, project, target, period, operations }, _, { injector }) {
|
||||
totalRequests({ organization, project, target, period, operations, clients }, _, { injector }) {
|
||||
return injector.get(OperationsManager).countRequests({
|
||||
organization,
|
||||
project,
|
||||
target,
|
||||
period,
|
||||
operations,
|
||||
clients,
|
||||
});
|
||||
},
|
||||
totalFailures(
|
||||
{ organization, project, target, period, operations: operationsFilter },
|
||||
{ organization, project, target, period, operations: operationsFilter, clients },
|
||||
_,
|
||||
{ injector },
|
||||
) {
|
||||
|
|
@ -181,10 +188,11 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
target,
|
||||
period,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
});
|
||||
},
|
||||
totalOperations(
|
||||
{ organization, project, target, period, operations: operationsFilter },
|
||||
{ organization, project, target, period, operations: operationsFilter, clients },
|
||||
_,
|
||||
{ injector },
|
||||
) {
|
||||
|
|
@ -194,10 +202,11 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
target,
|
||||
period,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
});
|
||||
},
|
||||
requestsOverTime(
|
||||
{ organization, project, target, period, operations: operationsFilter },
|
||||
{ organization, project, target, period, operations: operationsFilter, clients },
|
||||
{ resolution },
|
||||
{ injector },
|
||||
) {
|
||||
|
|
@ -208,10 +217,11 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
period,
|
||||
resolution,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
});
|
||||
},
|
||||
failuresOverTime(
|
||||
{ organization, project, target, period, operations: operationsFilter },
|
||||
{ organization, project, target, period, operations: operationsFilter, clients },
|
||||
{ resolution },
|
||||
{ injector },
|
||||
) {
|
||||
|
|
@ -222,10 +232,11 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
period,
|
||||
resolution,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
});
|
||||
},
|
||||
durationOverTime(
|
||||
{ organization, project, target, period, operations: operationsFilter },
|
||||
{ organization, project, target, period, operations: operationsFilter, clients },
|
||||
{ resolution },
|
||||
{ injector },
|
||||
) {
|
||||
|
|
@ -236,6 +247,7 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
period,
|
||||
resolution,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
});
|
||||
},
|
||||
clients(
|
||||
|
|
@ -252,7 +264,7 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
});
|
||||
},
|
||||
duration(
|
||||
{ organization, project, target, period, operations: operationsFilter },
|
||||
{ organization, project, target, period, operations: operationsFilter, clients },
|
||||
_,
|
||||
{ injector },
|
||||
) {
|
||||
|
|
@ -262,10 +274,11 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
target,
|
||||
period,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
});
|
||||
},
|
||||
async durationHistogram(
|
||||
{ organization, project, target, period, operations: operationsFilter },
|
||||
{ organization, project, target, period, operations: operationsFilter, clients },
|
||||
_,
|
||||
{ injector },
|
||||
) {
|
||||
|
|
@ -275,6 +288,7 @@ export const resolvers: OperationsModule.Resolvers = {
|
|||
target,
|
||||
period,
|
||||
operations: operationsFilter,
|
||||
clients,
|
||||
});
|
||||
|
||||
const uniqueDurations = new Map<
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ export interface OperationsStats {
|
|||
target: string;
|
||||
period: DateRange;
|
||||
operations: readonly string[];
|
||||
clients: readonly string[];
|
||||
}
|
||||
|
||||
export interface DurationStats {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import { formatISO, subDays, subHours, subMinutes } from 'date-fns';
|
|||
import { useQuery } from 'urql';
|
||||
import { authenticated } from '@/components/authenticated-container';
|
||||
import { TargetLayout } from '@/components/layouts/target';
|
||||
import { OperationsFilterTrigger } from '@/components/target/operations/Filters';
|
||||
import {
|
||||
ClientsFilterTrigger,
|
||||
OperationsFilterTrigger,
|
||||
} from '@/components/target/operations/Filters';
|
||||
import { OperationsList } from '@/components/target/operations/List';
|
||||
import { OperationsStats } from '@/components/target/operations/Stats';
|
||||
import { Subtitle, Title } from '@/components/ui/page';
|
||||
|
|
@ -43,7 +46,7 @@ function OperationsView({
|
|||
const selectedPeriod: PeriodKey =
|
||||
(new URLSearchParams(periodParam).get('period') as PeriodKey) ?? '1d';
|
||||
const [selectedOperations, setSelectedOperations] = useState<string[]>([]);
|
||||
|
||||
const [selectedClients, setSelectedClients] = useState<string[]>([]);
|
||||
const period = useMemo(() => {
|
||||
const now = floorDate(new Date());
|
||||
const sub = selectedPeriod.endsWith('h') ? 'h' : selectedPeriod.endsWith('m') ? 'm' : 'd';
|
||||
|
|
@ -81,6 +84,11 @@ function OperationsView({
|
|||
selected={selectedOperations}
|
||||
onFilter={setSelectedOperations}
|
||||
/>
|
||||
<ClientsFilterTrigger
|
||||
period={period}
|
||||
selected={selectedClients}
|
||||
onFilter={setSelectedClients}
|
||||
/>
|
||||
<RadixSelect
|
||||
onChange={updatePeriod}
|
||||
defaultValue={selectedPeriod}
|
||||
|
|
@ -94,6 +102,7 @@ function OperationsView({
|
|||
target={targetCleanId}
|
||||
period={period}
|
||||
operationsFilter={selectedOperations}
|
||||
clientNamesFilter={selectedClients}
|
||||
/>
|
||||
<OperationsList
|
||||
className="mt-12"
|
||||
|
|
@ -102,6 +111,7 @@ function OperationsView({
|
|||
project={projectCleanId}
|
||||
target={targetCleanId}
|
||||
operationsFilter={selectedOperations}
|
||||
clientNamesFilter={selectedClients}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -260,3 +260,250 @@ export function OperationsFilterTrigger({
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientRow({
|
||||
client,
|
||||
selected,
|
||||
onSelect,
|
||||
style,
|
||||
}: {
|
||||
client: { name: string; count: number };
|
||||
selected: boolean;
|
||||
onSelect(id: string, selected: boolean): void;
|
||||
style: any;
|
||||
}): ReactElement {
|
||||
const requests = useFormattedNumber(client.count);
|
||||
const hash = client.name;
|
||||
const change = useCallback(() => {
|
||||
if (hash) {
|
||||
onSelect(hash, !selected);
|
||||
}
|
||||
}, [onSelect, hash, selected]);
|
||||
|
||||
return (
|
||||
<div style={style} className="flex items-center gap-4">
|
||||
<Checkbox checked={selected} onCheckedChange={change} id={hash} />
|
||||
<label
|
||||
htmlFor={hash}
|
||||
className="flex items-center justify-between overflow-hidden gap-4 w-full cursor-pointer"
|
||||
>
|
||||
<span className="grow text-ellipsis overflow-hidden">{client.name}</span>
|
||||
<div className="shrink-0 text-right text-gray-500">{requests}</div>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientsFilter({
|
||||
onClose,
|
||||
isOpen,
|
||||
onFilter,
|
||||
clients,
|
||||
selected,
|
||||
}: {
|
||||
onClose(): void;
|
||||
onFilter(keys: string[]): void;
|
||||
isOpen: boolean;
|
||||
clients: readonly { name: string; count: number }[];
|
||||
selected?: string[];
|
||||
}): ReactElement {
|
||||
function getClientNames() {
|
||||
return clients.map(client => client.name);
|
||||
}
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<string[]>(() =>
|
||||
selected?.length ? selected : getClientNames(),
|
||||
);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(operationHash: string, selected: boolean) => {
|
||||
const itemAt = selectedItems.findIndex(hash => hash === operationHash);
|
||||
const exists = itemAt > -1;
|
||||
|
||||
if (selected && !exists) {
|
||||
setSelectedItems([...selectedItems, operationHash]);
|
||||
} else if (!selected && exists) {
|
||||
setSelectedItems(selectedItems.filter(hash => hash !== operationHash));
|
||||
}
|
||||
},
|
||||
[selectedItems, setSelectedItems],
|
||||
);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const debouncedFilter = useDebouncedCallback((value: string) => {
|
||||
setVisibleOperations(
|
||||
clients.filter(client => client.name.toLocaleLowerCase().includes(value.toLocaleLowerCase())),
|
||||
);
|
||||
}, 500);
|
||||
|
||||
const onChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.currentTarget;
|
||||
|
||||
setSearchTerm(value);
|
||||
debouncedFilter(value);
|
||||
},
|
||||
[setSearchTerm, debouncedFilter],
|
||||
);
|
||||
|
||||
const [visibleOperations, setVisibleOperations] = useState(clients);
|
||||
|
||||
const selectAll = useCallback(() => {
|
||||
setSelectedItems(getClientNames());
|
||||
}, [clients]);
|
||||
const selectNone = useCallback(() => {
|
||||
setSelectedItems([]);
|
||||
}, [setSelectedItems]);
|
||||
|
||||
const renderRow = useCallback<ComponentType<ListChildComponentProps>>(
|
||||
({ index, style }) => {
|
||||
const client = visibleOperations[index];
|
||||
|
||||
return (
|
||||
<ClientRow
|
||||
style={style}
|
||||
key={client.name}
|
||||
client={client}
|
||||
selected={selectedItems.includes(client.name || '')}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[visibleOperations, selectedItems, onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<Drawer open={isOpen} onOpenChange={onClose} width={500}>
|
||||
<Drawer.Title>Filter by client</Drawer.Title>
|
||||
|
||||
<div className="flex flex-col h-full space-y-3">
|
||||
<Input
|
||||
size="medium"
|
||||
placeholder="Search for operation..."
|
||||
onChange={onChange}
|
||||
value={searchTerm}
|
||||
onClear={() => {
|
||||
setSearchTerm('');
|
||||
setVisibleOperations(clients);
|
||||
}}
|
||||
/>
|
||||
<div className="flex gap-2 items-center w-full">
|
||||
<Button variant="link" onClick={selectAll}>
|
||||
All
|
||||
</Button>
|
||||
<Button variant="link" onClick={selectNone}>
|
||||
None
|
||||
</Button>
|
||||
<Button className="ml-auto" onClick={selectAll}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={selectedItems.length === 0}
|
||||
onClick={() => {
|
||||
onFilter(selectedItems);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="pl-1 grow">
|
||||
<AutoSizer>
|
||||
{({ height, width }) =>
|
||||
!height || !width ? (
|
||||
<></>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={visibleOperations.length}
|
||||
itemSize={24}
|
||||
overscanCount={5}
|
||||
>
|
||||
{renderRow}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientsFilterContainer({
|
||||
period,
|
||||
isOpen,
|
||||
onClose,
|
||||
onFilter,
|
||||
selected,
|
||||
}: {
|
||||
onFilter(keys: string[]): void;
|
||||
onClose(): void;
|
||||
isOpen: boolean;
|
||||
period: DateRangeInput;
|
||||
selected?: string[];
|
||||
}): ReactElement | null {
|
||||
const router = useRouteSelector();
|
||||
const [query] = useQuery({
|
||||
query: OperationsStatsDocument,
|
||||
variables: {
|
||||
selector: {
|
||||
organization: router.organizationId,
|
||||
project: router.projectId,
|
||||
target: router.targetId,
|
||||
period,
|
||||
operations: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (query.fetching || query.error || !query.data) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const allClients = query.data.operationsStats?.clients?.nodes ?? [];
|
||||
|
||||
return (
|
||||
<ClientsFilter
|
||||
clients={allClients}
|
||||
selected={selected}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onFilter={clientNames => {
|
||||
onFilter(clientNames.length === allClients.length ? [] : clientNames);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClientsFilterTrigger({
|
||||
period,
|
||||
onFilter,
|
||||
selected,
|
||||
}: {
|
||||
period: DateRangeInput;
|
||||
onFilter(keys: string[]): void;
|
||||
selected?: string[];
|
||||
}): ReactElement {
|
||||
const [isOpen, toggle] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="secondary" className="gap-2 rounded-md" onClick={toggle}>
|
||||
Clients ({selected?.length || 'all'})<ChevronUpIcon className="rotate-180" />
|
||||
</Button>
|
||||
<ClientsFilterContainer
|
||||
isOpen={isOpen}
|
||||
onClose={toggle}
|
||||
period={period}
|
||||
selected={selected}
|
||||
onFilter={onFilter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,6 +203,9 @@ function OperationsTable({
|
|||
organization: string;
|
||||
project: string;
|
||||
target: string;
|
||||
clients: readonly { name: string }[];
|
||||
clientFilter: string | null;
|
||||
setClientFilter: (filter: string) => void;
|
||||
}): ReactElement {
|
||||
const tableInstance = useTableInstance(table, {
|
||||
columns,
|
||||
|
|
@ -235,6 +238,7 @@ function OperationsTable({
|
|||
<div className={clsx('rounded-md p-5 border border-gray-800 bg-gray-900/50', className)}>
|
||||
<Section.Title>Operations</Section.Title>
|
||||
<Section.Subtitle>List of all operations with their statistics</Section.Subtitle>
|
||||
|
||||
<Table>
|
||||
<THead>
|
||||
<Tooltip.Provider>
|
||||
|
|
@ -324,6 +328,9 @@ function OperationsTableContainer({
|
|||
organization,
|
||||
project,
|
||||
target,
|
||||
clients,
|
||||
clientFilter,
|
||||
setClientFilter,
|
||||
className,
|
||||
}: {
|
||||
operations: readonly OperationStatsFieldsFragment[];
|
||||
|
|
@ -331,6 +338,9 @@ function OperationsTableContainer({
|
|||
organization: string;
|
||||
project: string;
|
||||
target: string;
|
||||
clients: readonly { name: string }[];
|
||||
clientFilter: string | null;
|
||||
setClientFilter: (client: string) => void;
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
const data = useMemo(() => {
|
||||
|
|
@ -393,6 +403,9 @@ function OperationsTableContainer({
|
|||
organization={organization}
|
||||
project={project}
|
||||
target={target}
|
||||
clients={clients}
|
||||
clientFilter={clientFilter}
|
||||
setClientFilter={setClientFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -404,6 +417,7 @@ export function OperationsList({
|
|||
target,
|
||||
period,
|
||||
operationsFilter = [],
|
||||
clientNamesFilter = [],
|
||||
}: {
|
||||
className?: string;
|
||||
organization: string;
|
||||
|
|
@ -411,7 +425,9 @@ export function OperationsList({
|
|||
target: string;
|
||||
period: DateRangeInput;
|
||||
operationsFilter: readonly string[];
|
||||
clientNamesFilter: string[];
|
||||
}): ReactElement {
|
||||
const [clientFilter, setClientFilter] = useState<string | null>(null);
|
||||
const [query, refetch] = useQuery({
|
||||
query: OperationsStatsDocument,
|
||||
variables: {
|
||||
|
|
@ -421,10 +437,12 @@ export function OperationsList({
|
|||
target,
|
||||
period,
|
||||
operations: [],
|
||||
clientNames: clientNamesFilter,
|
||||
},
|
||||
},
|
||||
});
|
||||
const operations = query.data?.operationsStats?.operations?.nodes ?? [];
|
||||
const clients = query.data?.operationsStats?.clients?.nodes ?? [];
|
||||
|
||||
return (
|
||||
<OperationsFallback
|
||||
|
|
@ -436,6 +454,9 @@ export function OperationsList({
|
|||
operations={operations}
|
||||
operationsFilter={operationsFilter}
|
||||
className={className}
|
||||
clients={clients}
|
||||
setClientFilter={setClientFilter}
|
||||
clientFilter={clientFilter}
|
||||
organization={organization}
|
||||
project={project}
|
||||
target={target}
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ export function OperationsStats({
|
|||
target,
|
||||
period,
|
||||
operationsFilter,
|
||||
clientNamesFilter,
|
||||
}: {
|
||||
organization: string;
|
||||
project: string;
|
||||
|
|
@ -698,6 +699,7 @@ export function OperationsStats({
|
|||
to: string;
|
||||
};
|
||||
operationsFilter: string[];
|
||||
clientNamesFilter: Array<string>;
|
||||
}): ReactElement {
|
||||
const resolution = 90;
|
||||
const [query, refetchQuery] = useQuery({
|
||||
|
|
@ -709,6 +711,7 @@ export function OperationsStats({
|
|||
target,
|
||||
period,
|
||||
operations: operationsFilter,
|
||||
clientNames: clientNamesFilter,
|
||||
},
|
||||
resolution,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,5 +6,11 @@ query operationsStats($selector: OperationsStatsSelectorInput!) {
|
|||
}
|
||||
total
|
||||
}
|
||||
clients {
|
||||
nodes {
|
||||
name
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,13 +88,15 @@ async function single() {
|
|||
const randNumber = Math.random() * 100;
|
||||
console.log('Reporting usage query...');
|
||||
|
||||
const done = hiveInstance.collectUsage({
|
||||
document: randNumber > 50 ? query1 : query2,
|
||||
schema,
|
||||
variableValues: {},
|
||||
});
|
||||
const done = hiveInstance.collectUsage();
|
||||
|
||||
done(
|
||||
{
|
||||
document: randNumber > 50 ? query1 : query2,
|
||||
schema,
|
||||
variableValues: {},
|
||||
contextValue: {},
|
||||
},
|
||||
randNumber > 90
|
||||
? {
|
||||
errors: undefined,
|
||||
|
|
|
|||
Loading…
Reference in a new issue