mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
API client generation: Create new IAM api client and use in query library (#99888)
* create new generated iam api client and use in query library * update betterer * use new createBaseQuery method * update CODEOWNERS * fix unit tests * use shared type * update comment * fix test
This commit is contained in:
parent
a21265a7ad
commit
9693212475
@ -5777,9 +5777,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||
],
|
||||
"public/app/features/query-library/api/query.ts:5381": [
|
||||
[0, 0, 0, "\'@grafana/runtime/src/services/backendSrv\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
|
||||
],
|
||||
"public/app/features/query/components/QueryEditorRow.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -492,6 +492,7 @@ playwright.config.ts @grafana/plugins-platform-frontend
|
||||
/public/app/features/explore/ @grafana/observability-traces-and-profiling
|
||||
/public/app/features/expressions/ @grafana/observability-metrics
|
||||
/public/app/features/folders/ @grafana/grafana-frontend-platform
|
||||
/public/app/features/iam/ @grafana/grafana-frontend-platform
|
||||
/public/app/features/inspector/ @grafana/dashboards-squad
|
||||
/public/app/features/invites/ @grafana/grafana-frontend-platform
|
||||
/public/app/features/library-panels/ @grafana/dashboards-squad
|
||||
|
3766
pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json
Normal file
3766
pkg/tests/apis/openapi_snapshots/iam.grafana.app-v0alpha1.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -67,6 +67,9 @@ func TestIntegrationOpenAPIs(t *testing.T) {
|
||||
}, {
|
||||
Group: "peakq.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
}, {
|
||||
Group: "iam.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
}}
|
||||
for _, gv := range groups {
|
||||
VerifyOpenAPISnapshots(t, dir, gv, h)
|
||||
|
@ -28,6 +28,7 @@ import usersReducers from 'app/features/users/state/reducers';
|
||||
import templatingReducers from 'app/features/variables/state/keyedVariablesReducer';
|
||||
|
||||
import { alertingApi } from '../../features/alerting/unified/api/alertingApi';
|
||||
import { iamApi } from '../../features/iam/api/api';
|
||||
import { userPreferencesAPI } from '../../features/preferences/api';
|
||||
import { queryLibraryApi } from '../../features/query-library/api/factory';
|
||||
import { cleanUpAction } from '../actions/cleanUp';
|
||||
@ -60,6 +61,7 @@ const rootReducers = {
|
||||
[browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer,
|
||||
[cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer,
|
||||
[queryLibraryApi.reducerPath]: queryLibraryApi.reducer,
|
||||
[iamApi.reducerPath]: iamApi.reducer,
|
||||
[userPreferencesAPI.reducerPath]: userPreferencesAPI.reducer,
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { render, waitFor, screen } from '@testing-library/react';
|
||||
|
||||
import { AnnoKeyCreatedBy } from '../../apiserver/types';
|
||||
import { ListQueryTemplateApiResponse } from '../../query-library/api/endpoints.gen';
|
||||
import { CREATED_BY_KEY } from '../../query-library/api/types';
|
||||
|
||||
import { QueryTemplatesList } from './QueryTemplatesList';
|
||||
import { QueryActionButtonProps } from './types';
|
||||
@ -117,7 +117,7 @@ const testItems = [
|
||||
name: 'TEST_QUERY',
|
||||
creationTimestamp: '2025-01-01T11:11:11.00Z',
|
||||
annotations: {
|
||||
[CREATED_BY_KEY]: 'viewer:JohnDoe',
|
||||
[AnnoKeyCreatedBy]: 'viewer:JohnDoe',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
|
@ -10,7 +10,6 @@ import { useListQueryTemplateQuery } from 'app/features/query-library';
|
||||
import { QueryTemplate } from 'app/features/query-library/types';
|
||||
|
||||
import { convertDataQueryResponseToQueryTemplates } from '../../query-library/api/mappers';
|
||||
import { UserDataQueryResponse } from '../../query-library/api/types';
|
||||
|
||||
import { QueryLibraryProps } from './QueryLibrary';
|
||||
import { queryLibraryTrackFilterDatasource } from './QueryLibraryAnalyticsEvents';
|
||||
@ -33,9 +32,9 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const loadUsersResult = useLoadUsersWithError(data);
|
||||
const userNames = loadUsersResult.value ? loadUsersResult.value.display.map((user) => user.displayName) : [];
|
||||
const userNames = loadUsersResult.data ? loadUsersResult.data.display.map((user) => user.displayName) : [];
|
||||
|
||||
const loadQueryMetadataResult = useLoadQueryMetadataWithError(data, loadUsersResult.value);
|
||||
const loadQueryMetadataResult = useLoadQueryMetadataWithError(data, loadUsersResult.data);
|
||||
|
||||
// Filtering right now is done just on the frontend until there is better backend support for this.
|
||||
const filteredRows = useMemo(
|
||||
@ -61,7 +60,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || loadUsersResult.loading || loadQueryMetadataResult.loading) {
|
||||
if (isLoading || loadUsersResult.isLoading || loadQueryMetadataResult.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
@ -109,7 +108,7 @@ export function QueryTemplatesList(props: QueryTemplatesListProps) {
|
||||
<Trans i18nKey="query-library.user-names">User name(s):</Trans>
|
||||
</InlineLabel>
|
||||
<MultiSelect
|
||||
isLoading={loadUsersResult.loading}
|
||||
isLoading={loadUsersResult.isLoading}
|
||||
className={styles.multiSelect}
|
||||
onChange={(items, actionMeta) => {
|
||||
setUserFilters(items);
|
||||
@ -166,7 +165,7 @@ function useLoadUsersWithError(data: QueryTemplate[] | undefined) {
|
||||
*/
|
||||
function useLoadQueryMetadataWithError(
|
||||
queryTemplates: QueryTemplate[] | undefined,
|
||||
userDataList: UserDataQueryResponse | undefined
|
||||
userDataList: ReturnType<typeof useLoadUsers>['data']
|
||||
) {
|
||||
const result = useLoadQueryMetadata(queryTemplates, userDataList);
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { compact, uniq } from 'lodash';
|
||||
import { useAsync } from 'react-use';
|
||||
import { AsyncState } from 'react-use/lib/useAsync';
|
||||
@ -6,20 +7,20 @@ import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { DataQuery, DataSourceRef } from '@grafana/schema';
|
||||
|
||||
import { createQueryText } from '../../../../core/utils/richHistory';
|
||||
import { useGetDisplayListQuery } from '../../../iam';
|
||||
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
|
||||
import { UserDataQueryResponse } from '../../../query-library/api/types';
|
||||
import { getUserInfo } from '../../../query-library/api/user';
|
||||
import { QueryTemplate } from '../../../query-library/types';
|
||||
|
||||
export function useLoadUsers(userUIDs: string[] | undefined) {
|
||||
return useAsync(async () => {
|
||||
if (!userUIDs) {
|
||||
return undefined;
|
||||
}
|
||||
const userQtList = uniq(compact(userUIDs));
|
||||
const usersParam = userQtList.map((userUid) => `key=${encodeURIComponent(userUid)}`).join('&');
|
||||
return await getUserInfo(`?${usersParam}`);
|
||||
}, [userUIDs]);
|
||||
const userQtList = uniq(compact(userUIDs));
|
||||
const usersParam = userQtList.map((userUid) => `key=${encodeURIComponent(userUid)}`).join('&');
|
||||
return useGetDisplayListQuery(
|
||||
userUIDs
|
||||
? {
|
||||
name: `name?${usersParam}`,
|
||||
}
|
||||
: skipToken
|
||||
);
|
||||
}
|
||||
|
||||
// Explicitly type the result so TS knows to discriminate between the error result and good result by the error prop
|
||||
@ -54,7 +55,7 @@ type MetadataValue =
|
||||
*/
|
||||
export function useLoadQueryMetadata(
|
||||
queryTemplates: QueryTemplate[] | undefined,
|
||||
userDataList: UserDataQueryResponse | undefined
|
||||
userDataList: ReturnType<typeof useLoadUsers>['data']
|
||||
): AsyncState<MetadataValue[]> {
|
||||
return useAsync(async () => {
|
||||
if (!(queryTemplates && userDataList)) {
|
||||
@ -87,7 +88,7 @@ export function useLoadQueryMetadata(
|
||||
user: {
|
||||
uid: queryTemplate.user?.uid || '',
|
||||
displayName: extendedUserData?.displayName || '',
|
||||
avatarUrl: extendedUserData?.avatarUrl || '',
|
||||
avatarUrl: extendedUserData?.avatarURL || '',
|
||||
},
|
||||
error: undefined,
|
||||
};
|
||||
|
@ -27,6 +27,31 @@ interface MockQuery extends DataQuery {
|
||||
expr: string;
|
||||
}
|
||||
|
||||
jest.mock('../QueryLibrary/utils/dataFetching', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
...jest.requireActual('../QueryLibrary/utils/dataFetching'),
|
||||
useLoadUsers: () => {
|
||||
return {
|
||||
data: {
|
||||
display: [
|
||||
{
|
||||
avatarUrl: '',
|
||||
displayName: 'john doe',
|
||||
identity: {
|
||||
name: 'JohnDoe',
|
||||
type: 'viewer',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
reportInteraction: (...args: object[]) => {
|
||||
|
14
public/app/features/iam/api/api.ts
Normal file
14
public/app/features/iam/api/api.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||
|
||||
import { createBaseQuery } from '../../../api/createBaseQuery';
|
||||
import { getAPINamespace } from '../../../api/utils';
|
||||
|
||||
export const API_VERSION = 'iam.grafana.app/v0alpha1';
|
||||
|
||||
export const BASE_URL = `/apis/${API_VERSION}/namespaces/${getAPINamespace()}`;
|
||||
|
||||
export const iamApi = createApi({
|
||||
baseQuery: createBaseQuery({ baseURL: BASE_URL }),
|
||||
reducerPath: 'iamAPI',
|
||||
endpoints: () => ({}),
|
||||
});
|
59
public/app/features/iam/api/endpoints.gen.ts
Normal file
59
public/app/features/iam/api/endpoints.gen.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { iamApi as api } from './api';
|
||||
export const addTagTypes = ['DisplayList'] as const;
|
||||
const injectedRtkApi = api
|
||||
.enhanceEndpoints({
|
||||
addTagTypes,
|
||||
})
|
||||
.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getDisplayList: build.query<GetDisplayListApiResponse, GetDisplayListApiArg>({
|
||||
query: (queryArg) => ({ url: `/display/${queryArg.name}` }),
|
||||
providesTags: ['DisplayList'],
|
||||
}),
|
||||
}),
|
||||
overrideExisting: false,
|
||||
});
|
||||
export { injectedRtkApi as generatedIamApi };
|
||||
export type GetDisplayListApiResponse = /** status 200 OK */ DisplayList;
|
||||
export type GetDisplayListApiArg = {
|
||||
/** name of the DisplayList */
|
||||
name: string;
|
||||
};
|
||||
export type IdentityRef = {
|
||||
/** Name is the unique identifier for identity, guaranteed jo be a unique value for the type within a namespace. */
|
||||
name: string;
|
||||
/** Type of identity e.g. "user". For a full list see https://github.com/grafana/authlib/blob/2f8d13a83ca3e82da08b53726de1697ee5b5b4cc/claims/type.go#L15-L24 */
|
||||
type: string;
|
||||
};
|
||||
export type Display = {
|
||||
/** AvatarURL is the url where we can get the avatar for identity */
|
||||
avatarURL?: string;
|
||||
/** Display name for identity. */
|
||||
displayName: string;
|
||||
identity: IdentityRef;
|
||||
/** InternalID is the legacy numreric id for identity, this is deprecated and should be phased out */
|
||||
internalId?: number;
|
||||
};
|
||||
export type ListMeta = {
|
||||
/** continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. */
|
||||
continue?: string;
|
||||
/** remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact. */
|
||||
remainingItemCount?: number;
|
||||
/** String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency */
|
||||
resourceVersion?: string;
|
||||
/** Deprecated: selfLink is a legacy read-only field that is no longer populated by the system. */
|
||||
selfLink?: string;
|
||||
};
|
||||
export type DisplayList = {
|
||||
/** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */
|
||||
apiVersion?: string;
|
||||
/** Matching items (the caller may need to remap from keys to results) */
|
||||
display: Display[];
|
||||
/** Input keys that were not useable */
|
||||
invalidKeys?: string[];
|
||||
/** Request keys used to lookup the display value */
|
||||
keys: string[];
|
||||
/** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */
|
||||
kind?: string;
|
||||
metadata?: ListMeta;
|
||||
};
|
38
public/app/features/iam/api/scripts/generate-rtk-apis.ts
Normal file
38
public/app/features/iam/api/scripts/generate-rtk-apis.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* To generate iam k8s APIs, run:
|
||||
* `yarn process-specs && npx rtk-query-codegen-openapi ./public/app/features/iam/api/scripts/generate-rtk-apis.ts` from the root of the repo
|
||||
*/
|
||||
|
||||
import { ConfigFile } from '@rtk-query/codegen-openapi';
|
||||
import { accessSync } from 'fs';
|
||||
|
||||
const schemaFile = '../../../../../../data/openapi/iam.grafana.app-v0alpha1.json';
|
||||
|
||||
try {
|
||||
// Check we have the OpenAPI before generating query library RTK APIs,
|
||||
// as this is currently a manual process
|
||||
accessSync(schemaFile);
|
||||
} catch (e) {
|
||||
console.error('\nCould not find OpenAPI definition.\n');
|
||||
console.error(
|
||||
'Please run go test pkg/tests/apis/openapi_test.go to generate the OpenAPI definition, then try running this script again.\n'
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const config: ConfigFile = {
|
||||
schemaFile,
|
||||
apiFile: '',
|
||||
tag: true,
|
||||
outputFiles: {
|
||||
'../endpoints.gen.ts': {
|
||||
apiFile: '../api.ts',
|
||||
apiImport: 'iamApi',
|
||||
filterEndpoints: ['getDisplayList'],
|
||||
exportName: 'generatedIamApi',
|
||||
flattenArg: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
3
public/app/features/iam/index.ts
Normal file
3
public/app/features/iam/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { generatedIamApi } from './api/endpoints.gen';
|
||||
|
||||
export const { useGetDisplayListQuery } = generatedIamApi;
|
@ -10,5 +10,6 @@ export const QUERY_LIBRARY_GET_LIMIT = 1000;
|
||||
|
||||
export const queryLibraryApi = createApi({
|
||||
baseQuery: createBaseQuery({ baseURL: BASE_URL }),
|
||||
reducerPath: 'queryLibraryAPI',
|
||||
endpoints: () => ({}),
|
||||
});
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AnnoKeyCreatedBy } from '../../apiserver/types';
|
||||
import { AddQueryTemplateCommand, QueryTemplate } from '../types';
|
||||
|
||||
import { ListQueryTemplateApiResponse, QueryTemplate as QT } from './endpoints.gen';
|
||||
import { API_VERSION, QueryTemplateKinds } from './query';
|
||||
import { CREATED_BY_KEY } from './types';
|
||||
|
||||
export const convertDataQueryResponseToQueryTemplates = (result: ListQueryTemplateApiResponse): QueryTemplate[] => {
|
||||
if (!result.items) {
|
||||
@ -21,7 +21,7 @@ export const convertDataQueryResponseToQueryTemplates = (result: ListQueryTempla
|
||||
})) ?? [],
|
||||
createdAtTimestamp: new Date(spec.metadata?.creationTimestamp ?? '').getTime(),
|
||||
user: {
|
||||
uid: spec.metadata?.annotations?.[CREATED_BY_KEY] ?? '',
|
||||
uid: spec.metadata?.annotations?.[AnnoKeyCreatedBy] ?? '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -1,26 +0,0 @@
|
||||
// pkg/apis/iam/v0alpha1/types_display.go
|
||||
export type UserDataQueryResponse = {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: {
|
||||
selfLink: string;
|
||||
resourceVersion: string;
|
||||
continue: string;
|
||||
remainingItemCount: number;
|
||||
};
|
||||
display: UserSpecResponse[];
|
||||
keys: string[];
|
||||
};
|
||||
|
||||
// pkg/apis/iam/v0alpha1/types_display.go
|
||||
export type UserSpecResponse = {
|
||||
avatarUrl: string;
|
||||
displayName: string;
|
||||
identity: {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
internalId: number;
|
||||
};
|
||||
|
||||
export const CREATED_BY_KEY = 'grafana.app/createdBy';
|
@ -1,20 +0,0 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { getAPINamespace } from '../../../api/utils';
|
||||
|
||||
import { UserDataQueryResponse } from './types';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export const API_VERSION = 'iam.grafana.app/v0alpha1';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
const BASE_URL = `apis/${API_VERSION}/namespaces/${getAPINamespace()}/display`;
|
||||
|
||||
export async function getUserInfo(url?: string): Promise<UserDataQueryResponse> {
|
||||
const userInfo = await getBackendSrv().get(`${BASE_URL}${url}`);
|
||||
return userInfo;
|
||||
}
|
@ -11,6 +11,7 @@ import { StoreState } from 'app/types/store';
|
||||
import { buildInitialState } from '../core/reducers/navModel';
|
||||
import { addReducer, createRootReducer } from '../core/reducers/root';
|
||||
import { alertingApi } from '../features/alerting/unified/api/alertingApi';
|
||||
import { iamApi } from '../features/iam/api/api';
|
||||
import { queryLibraryApi } from '../features/query-library/api/factory';
|
||||
|
||||
import { setStore } from './store';
|
||||
@ -41,6 +42,7 @@ export function configureStore(initialState?: Partial<StoreState>) {
|
||||
cloudMigrationAPI.middleware,
|
||||
queryLibraryApi.middleware,
|
||||
userPreferencesAPI.middleware,
|
||||
iamApi.middleware,
|
||||
...extraMiddleware
|
||||
),
|
||||
devTools: process.env.NODE_ENV !== 'production',
|
||||
|
Loading…
Reference in New Issue
Block a user