diff --git a/client/clients/clientPersistedQueries.jsx b/client/clients/clientPersistedQueries.jsx
new file mode 100644
index 00000000..2e09b763
--- /dev/null
+++ b/client/clients/clientPersistedQueries.jsx
@@ -0,0 +1,42 @@
+// eslint-disable-next-line
+import React from 'react';
+import { useQuery } from '@apollo/client';
+
+import PersistedQuery from '../persisted-queries/PersistedQuery';
+import SpinnerCenter from '../components/SpinnerCenter';
+import { CLIENT_VERSION_PERSISTED_QUERIES } from '../utils/queries';
+
+const ClientPersistedQueries = ({ selectedVersion }) => {
+ if (!selectedVersion) {
+ return null;
+ }
+
+ const { loading, data } = useQuery(CLIENT_VERSION_PERSISTED_QUERIES, {
+ variables: { clientVersionId: selectedVersion },
+ });
+
+ if (loading) {
+ return ;
+ }
+
+ if (!data) {
+ return
No queries found
;
+ }
+
+ return (
+
+ {data?.persistedQueries.length > 0 && (
+
Persisted Queries
+ )}
+ {data?.persistedQueries.map((pq) => {
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default ClientPersistedQueries;
diff --git a/client/clients/index.jsx b/client/clients/index.jsx
new file mode 100644
index 00000000..3bb62557
--- /dev/null
+++ b/client/clients/index.jsx
@@ -0,0 +1,73 @@
+//eslint-disable-next-line
+import React, { useState } from 'react';
+import { HashRouter as Router } from 'react-router-dom';
+import { useQuery } from '@apollo/client';
+import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+
+import SpinnerCenter from '../components/SpinnerCenter';
+import { CLIENTS_LIST } from '../utils/queries';
+import { ColumnPanel } from '../components/styled';
+import ClientVersions from './versions';
+import ClientPersistedQueries from './clientPersistedQueries';
+
+export default function Clients() {
+ const { loading, data } = useQuery(CLIENTS_LIST);
+
+ const [selectedClient, setSelectedClient] = useState(null);
+ const [selectedVersion, setSelectedVersion] = useState(null);
+
+ if (loading) {
+ return ;
+ }
+
+ if (!data || data.clients.length === 0) {
+ return No clients found
;
+ }
+
+ const clients = data.clients.map((client) => {
+ return (
+ {
+ setSelectedClient(client.name);
+ }}
+ >
+
+
+
+
+
+ );
+ });
+
+ return (
+
+
+
+ {clients}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/clients/versions.jsx b/client/clients/versions.jsx
new file mode 100644
index 00000000..594643e0
--- /dev/null
+++ b/client/clients/versions.jsx
@@ -0,0 +1,57 @@
+//eslint-disable-next-line
+import React from 'react';
+import { formatDistance } from 'date-fns';
+
+import { List, ListItem } from '@material-ui/core';
+import { FlexRow, EntryGrid } from '../components/styled';
+
+const Versions = ({
+ clients,
+ selectedClient,
+ selectedVersion,
+ setSelectedVersion,
+}) => {
+ let versions = null;
+
+ if (!selectedClient) {
+ return null;
+ }
+
+ clients.map((client) => {
+ if (selectedClient === client.name && client.versions) {
+ versions = client.versions.map((version) => (
+ setSelectedVersion(version.id)}
+ >
+
+
+
+ {version.version}
+
+
+ Updated{' '}
+ {formatDistance(
+ new Date(version.updatedTime),
+ new Date(),
+ {
+ addSuffix: true,
+ }
+ )}
+
+
+
+
+ ));
+ }
+ });
+
+ if (!versions) {
+ versions = No versions found
;
+ }
+
+ return {versions}
;
+};
+
+export default Versions;
diff --git a/client/components/Main.jsx b/client/components/Main.jsx
index bcd9802b..5d369d7d 100644
--- a/client/components/Main.jsx
+++ b/client/components/Main.jsx
@@ -5,6 +5,7 @@ import TopMenu from './TopMenu';
import TabPanel from './TabPanel';
import Schema from '../schema';
import PersistedQueries from '../persisted-queries';
+import Clients from '../clients';
import ServicesTab from '../schema/Tab';
import PersistedQueriesTab from '../persisted-queries/Tab';
@@ -13,12 +14,15 @@ const UITabs = [
{
Title: ,
href: '/schema',
- icon: 'dashboard',
component: Schema,
},
+ {
+ Title: Clients,
+ href: '/clients',
+ component: Clients,
+ },
{
Title: ,
- icon: 'ac-document',
href: '/persisted-queries',
component: PersistedQueries,
},
diff --git a/client/components/styled.js b/client/components/styled.js
index cb715cc4..a3765234 100644
--- a/client/components/styled.js
+++ b/client/components/styled.js
@@ -1,8 +1,8 @@
import styled from 'styled-components';
-import { Container, Button } from '@material-ui/core';
+import { Button } from '@material-ui/core';
import { colors, elevations } from '../utils';
-export const EntryPanel = styled(Container)`
+export const EntryPanel = styled.div`
transition: 0.2s ease-in;
box-shadow: ${elevations[2]};
cursor: ${({ onClick }) => onClick && 'pointer'};
@@ -24,6 +24,7 @@ export const EntryPanel = styled(Container)`
export const EntryName = styled.h3`
font-weight: 400;
margin: 0;
+ padding: 10px;
& > span {
font-weight: normal;
}
@@ -36,10 +37,6 @@ export const EntryGrid = styled.div`
& > :first-child {
flex: 2;
}
-
- .cui4-icon {
- margin: 0 5px;
- }
`;
export const FlexCenter = styled.div`
@@ -80,3 +77,16 @@ export const CopyButton = styled(Button)`
right: 8px;
z-index: 2;
`;
+
+export const ColumnPanel = styled.div`
+ min-width: 170px;
+ flex-shrink: 0;
+`;
+
+export const FlexRow = styled.div`
+ display: flex;
+ flex-flow: row nowrap;
+ overflow: hidden;
+ height: 100%;
+ justify-content: space-between;
+`;
diff --git a/client/utils/queries.js b/client/utils/queries.js
index c63ba6f7..ddc0a50c 100644
--- a/client/utils/queries.js
+++ b/client/utils/queries.js
@@ -99,3 +99,25 @@ export const SCHEMA_USAGE_SDL = gql`
}
}
`;
+
+export const CLIENTS_LIST = gql`
+ query getClients {
+ clients {
+ name
+ versions {
+ id
+ version
+ updatedTime
+ }
+ }
+ }
+`;
+
+export const CLIENT_VERSION_PERSISTED_QUERIES = gql`
+ query gerClientPersistedQueries($clientVersionId: Int!) {
+ persistedQueries(clientVersionId: $clientVersionId) {
+ key
+ query
+ }
+ }
+`;
diff --git a/migrations/20221003232700_client_pq_rel.sql b/migrations/20221003232700_client_pq_rel.sql
new file mode 100644
index 00000000..2a998b60
--- /dev/null
+++ b/migrations/20221003232700_client_pq_rel.sql
@@ -0,0 +1,6 @@
+ALTER TABLE `clients_persisted_queries_rel` ADD `added_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP AFTER `pq_key`;
+
+ALTER TABLE `clients_persisted_queries_rel`
+CHANGE `pq_key` `pq_key` VARCHAR(100)
+CHARACTER SET utf8mb4
+COLLATE utf8mb4_general_ci NOT NULL DEFAULT '';
diff --git a/src/database/clients.ts b/src/database/clients.ts
index 27458180..dc231f6e 100644
--- a/src/database/clients.ts
+++ b/src/database/clients.ts
@@ -166,25 +166,6 @@ const clientsModel = {
connection('clients')
.select('id', 'version', 'updated_time as updatedTime')
.where({ name }),
-
- getClientVersionsSince: async ({ since }) => {
- if (isNil(since)) {
- return [];
- }
-
- return connection('clients')
- .select([
- 'id',
- 'name',
- 'version',
- 'added_time as addedTime',
- 'updated_time',
- ])
- .where((knex) => {
- return knex.where('added_time', '>', since);
- })
- .limit(100);
- },
};
export default clientsModel;
diff --git a/src/database/persisted_queries.ts b/src/database/persisted_queries.ts
index ec8cecaf..7adfffca 100644
--- a/src/database/persisted_queries.ts
+++ b/src/database/persisted_queries.ts
@@ -9,6 +9,23 @@ const PersistedQueriesModel = {
)[0].amount;
},
+ getVersionPersistedQueries: async function ({ version_id }) {
+ return await connection('persisted_queries')
+ .innerJoin(
+ 'clients_persisted_queries_rel',
+ 'persisted_queries.key',
+ 'clients_persisted_queries_rel.pq_key'
+ )
+ .select([
+ 'query',
+ 'key',
+ 'clients_persisted_queries_rel.added_time as addedTime',
+ ])
+ .where({
+ 'clients_persisted_queries_rel.version_id': version_id,
+ });
+ },
+
list: async function ({ searchFragment = '', limit = 100, offset = 0 }) {
return connection('persisted_queries')
.select(['query', 'key', 'added_time'])
diff --git a/src/graphql/resolvers.itest.ts b/src/graphql/resolvers.itest.ts
new file mode 100644
index 00000000..9be7aa38
--- /dev/null
+++ b/src/graphql/resolvers.itest.ts
@@ -0,0 +1,44 @@
+import resolvers from './resolvers';
+import { cleanTables } from '../../test/integration/database';
+
+import clientsModel from '../database/clients';
+
+describe('app/graphql', () => {
+ beforeEach(async () => {
+ await cleanTables();
+ });
+
+ describe('POST /graphql', () => {
+ describe('Query { clients } ', () => {
+ it('returns empty set', async () => {
+ const result = await resolvers.Query.clients();
+
+ expect(result).toEqual([]);
+ });
+
+ it('returns two clients', async () => {
+ // ARRANGE
+ clientsModel.add({
+ name: 'client_a',
+ version: 'v1',
+ persistedQueryHash: 'abc',
+ });
+ clientsModel.add({
+ name: 'client_b',
+ version: 'v2',
+ persistedQueryHash: 'def',
+ });
+ await clientsModel.syncUniqueClientsToDb();
+
+ // ACT
+ const result = await resolvers.Query.clients();
+
+ // ASSERT
+ expect(result).toEqual([
+ { name: 'client_a' },
+ { name: 'client_b' },
+ ]);
+ });
+ });
+ });
+});
diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts
index 443a86c3..9b51dd4e 100644
--- a/src/graphql/resolvers.ts
+++ b/src/graphql/resolvers.ts
@@ -32,7 +32,16 @@ export default {
schemaPropertyHitsByClient: async (_, { entity, property }) =>
await schemaHit.get({ entity, property }),
- persistedQueries: async (parent, { searchFragment, limit, offset }) => {
+ persistedQueries: async (
+ parent,
+ { searchFragment, limit, offset, clientVersionId }
+ ) => {
+ if (clientVersionId) {
+ return await PersistedQueriesModel.getVersionPersistedQueries({
+ version_id: clientVersionId,
+ });
+ }
+
return await PersistedQueriesModel.list({
searchFragment,
limit,
@@ -45,8 +54,6 @@ export default {
persistedQueriesCount: async () => await PersistedQueriesModel.count(),
clients: async () => await clientsModel.getClients(),
- clientVersions: async (_, { since }) =>
- await clientsModel.getClientVersionsSince({ since }),
},
Mutation: {
deactivateSchema: async (parent, { id }) => {
diff --git a/src/graphql/schema.ts b/src/graphql/schema.ts
index 864b15c1..ca40c1e4 100644
--- a/src/graphql/schema.ts
+++ b/src/graphql/schema.ts
@@ -20,6 +20,7 @@ export default gql`
searchFragment: String
limit: Int
offset: Int
+ clientVersionId: Int
): [PersistedQuery]
persistedQuery(key: String!): PersistedQuery
persistedQueriesCount: Int!
diff --git a/src/helpers/federation.ts b/src/helpers/federation.ts
index 9c434f0e..7b90eaad 100644
--- a/src/helpers/federation.ts
+++ b/src/helpers/federation.ts
@@ -27,6 +27,7 @@ export function composeAndValidateSchema(servicesSchemaMap) {
}
if (errors && errors.length) {
+ logger.error(errors);
throw new PublicError('Schema validation failed', {
details: errors,
});