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, });