diff --git a/packages/nodes-base/credentials/GrafanaApi.credentials.ts b/packages/nodes-base/credentials/GrafanaApi.credentials.ts new file mode 100644 index 0000000000..f03c6f4ac1 --- /dev/null +++ b/packages/nodes-base/credentials/GrafanaApi.credentials.ts @@ -0,0 +1,28 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class GrafanaApi implements ICredentialType { + name = 'grafanaApi'; + displayName = 'Grafana API'; + documentationUrl = 'grafana'; + properties: INodeProperties[] = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string', + default: '', + required: true, + }, + { + displayName: 'Base URL', + name: 'baseUrl', + type: 'string', + default: '', + description: 'Base URL of your Grafana instance', + placeholder: 'e.g. https://n8n.grafana.net/', + required: true, + }, + ]; +} diff --git a/packages/nodes-base/nodes/Grafana/GenericFunctions.ts b/packages/nodes-base/nodes/Grafana/GenericFunctions.ts new file mode 100644 index 0000000000..2bb64d530c --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/GenericFunctions.ts @@ -0,0 +1,111 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +import { + GrafanaCredentials, +} from './types'; + +export async function grafanaApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const { + apiKey, + baseUrl: rawBaseUrl, + } = await this.getCredentials('grafanaApi') as GrafanaCredentials; + + const baseUrl = tolerateTrailingSlash(rawBaseUrl); + + const options: OptionsWithUri = { + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: `${baseUrl}/api${endpoint}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + if (error?.response?.data?.message === 'Team member not found') { + error.response.data.message += '. Are you sure the user is a member of this team?'; + } + + if (error?.response?.data?.message === 'Team not found') { + error.response.data.message += ' with the provided ID'; + } + + if (error?.response?.data?.message === 'A dashboard with the same name in the folder already exists') { + error.response.data.message = 'A dashboard with the same name already exists in the selected folder'; + } + + if (error?.response?.data?.message === 'Team name taken') { + error.response.data.message = 'This team name is already taken. Please choose a new one.'; + } + + if (error?.code === 'ECONNREFUSED') { + error.message = 'Invalid credentials or error in establishing connection with given credentials'; + } + + throw new NodeApiError(this.getNode(), error); + } +} + +export function throwOnEmptyUpdate( + this: IExecuteFunctions, + resource: string, + updateFields: IDataObject, +) { + if (!Object.keys(updateFields).length) { + throw new NodeOperationError( + this.getNode(), + `Please enter at least one field to update for the ${resource}.`, + ); + } +} + +export function tolerateTrailingSlash(baseUrl: string) { + return baseUrl.endsWith('/') + ? baseUrl.substr(0, baseUrl.length - 1) + : baseUrl; +} + +export function deriveUid(this: IExecuteFunctions, uidOrUrl: string) { + if (!uidOrUrl.startsWith('http')) return uidOrUrl; + + const urlSegments = uidOrUrl.split('/'); + const uid = urlSegments[urlSegments.indexOf('d') + 1]; + + if (!uid) { + throw new NodeOperationError(this.getNode(), 'Failed to derive UID from URL'); + } + + return uid; +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Grafana/Grafana.node.ts b/packages/nodes-base/nodes/Grafana/Grafana.node.ts new file mode 100644 index 0000000000..f914b105aa --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/Grafana.node.ts @@ -0,0 +1,577 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + ICredentialsDecrypted, + ICredentialTestFunctions, + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + NodeApiError, + NodeCredentialTestResult, +} from 'n8n-workflow'; + +import { + deriveUid, + grafanaApiRequest, + throwOnEmptyUpdate, + tolerateTrailingSlash, +} from './GenericFunctions'; + +import { + dashboardFields, + dashboardOperations, + teamFields, + teamMemberFields, + teamMemberOperations, + teamOperations, + userFields, + userOperations, +} from './descriptions'; + +import { + OptionsWithUri, +} from 'request'; + +import { + DashboardUpdateFields, + DashboardUpdatePayload, + GrafanaCredentials, + LoadedDashboards, + LoadedFolders, + LoadedTeams, + LoadedUsers, +} from './types'; + +export class Grafana implements INodeType { + description: INodeTypeDescription = { + displayName: 'Grafana', + name: 'grafana', + icon: 'file:grafana.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Grafana API', + defaults: { + name: 'Grafana', + color: '#fb755a', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'grafanaApi', + required: true, + testedBy: 'grafanaApiTest', + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + noDataExpression: true, + type: 'options', + options: [ + { + name: 'Dashboard', + value: 'dashboard', + }, + { + name: 'Team', + value: 'team', + }, + { + name: 'Team Member', + value: 'teamMember', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'dashboard', + }, + ...dashboardOperations, + ...dashboardFields, + ...teamOperations, + ...teamFields, + ...teamMemberOperations, + ...teamMemberFields, + ...userOperations, + ...userFields, + ], + }; + + methods = { + loadOptions: { + async getDashboards(this: ILoadOptionsFunctions): Promise { + const dashboards = await grafanaApiRequest.call( + this, 'GET', '/search', {}, { qs: 'dash-db' }, + ) as LoadedDashboards; + return dashboards.map(({ id, title }) => ({ value: id, name: title })); + }, + + async getFolders(this: ILoadOptionsFunctions): Promise { + const folders = await grafanaApiRequest.call(this, 'GET', '/folders') as LoadedFolders; + return folders.map(({ id, title }) => ({ value: id, name: title })); + }, + + async getTeams(this: ILoadOptionsFunctions): Promise { + const res = await grafanaApiRequest.call(this, 'GET', '/teams/search') as LoadedTeams; + return res.teams.map(({ id, name }) => ({ value: id, name })); + }, + + async getUsers(this: ILoadOptionsFunctions): Promise { + const users = await grafanaApiRequest.call(this, 'GET', '/org/users') as LoadedUsers; + return users.map(({ userId, email }) => ({ value: userId, name: email })); + }, + }, + + credentialTest: { + async grafanaApiTest( + this: ICredentialTestFunctions, + credential: ICredentialsDecrypted, + ): Promise { + const { apiKey, baseUrl: rawBaseUrl } = credential.data as GrafanaCredentials; + const baseUrl = tolerateTrailingSlash(rawBaseUrl); + + const options: OptionsWithUri = { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + method: 'GET', + uri: `${baseUrl}/api/folders`, + json: true, + }; + + try { + await this.helpers.request(options); + return { + status: 'OK', + message: 'Authentication successful', + }; + } catch (error) { + return { + status: 'Error', + message: error.message, + }; + } + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + let responseData; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'dashboard') { + + // ********************************************************************** + // dashboard + // ********************************************************************** + + if (operation === 'create') { + + // ---------------------------------------- + // dashboard: create + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/dashboard/#create--update-dashboard + + const body = { + dashboard: { + id: null, + title: this.getNodeParameter('title', i), + }, + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + if (additionalFields.folderId === '') delete additionalFields.folderId; + + Object.assign(body, additionalFields); + } + + responseData = await grafanaApiRequest.call(this, 'POST', '/dashboards/db', body); + + } else if (operation === 'delete') { + + // ---------------------------------------- + // dashboard: delete + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/dashboard/#delete-dashboard-by-uid + + const uidOrUrl = this.getNodeParameter('dashboardUidOrUrl', i) as string; + const uid = deriveUid.call(this, uidOrUrl); + const endpoint = `/dashboards/uid/${uid}`; + responseData = await grafanaApiRequest.call(this, 'DELETE', endpoint); + + } else if (operation === 'get') { + + // ---------------------------------------- + // dashboard: get + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/dashboard/#get-dashboard-by-uid + + const uidOrUrl = this.getNodeParameter('dashboardUidOrUrl', i) as string; + const uid = deriveUid.call(this, uidOrUrl); + const endpoint = `/dashboards/uid/${uid}`; + responseData = await grafanaApiRequest.call(this, 'GET', endpoint); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // dashboard: getAll + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/folder_dashboard_search/#search-folders-and-dashboards + + const qs = { + type: 'dash-db', + }; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + if (Object.keys(filters).length) { + Object.assign(qs, filters); + } + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + Object.assign(qs, { limit }); + } + + responseData = await grafanaApiRequest.call(this, 'GET', '/search', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // dashboard: update + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/dashboard/#create--update-dashboard + + const uidOrUrl = this.getNodeParameter('dashboardUidOrUrl', i) as string; + const uid = deriveUid.call(this, uidOrUrl); + + // ensure dashboard to update exists + await grafanaApiRequest.call(this, 'GET', `/dashboards/uid/${uid}`); + + const body: DashboardUpdatePayload = { + overwrite: true, + dashboard: { uid }, + }; + + const updateFields = this.getNodeParameter('updateFields', i) as DashboardUpdateFields; + + throwOnEmptyUpdate.call(this, resource, updateFields); + + const { title, ...rest } = updateFields; + + if (!title) { + const { dashboard } = await grafanaApiRequest.call(this, 'GET', `/dashboards/uid/${uid}`); + body.dashboard.title = dashboard.title; + } else { + const dashboards = await grafanaApiRequest.call(this, 'GET', '/search') as Array<{ title: string }>; + const titles = dashboards.map(({ title }) => title); + + if (titles.includes(title)) { + throw new NodeApiError( + this.getNode(), + { message: 'A dashboard with the same name already exists in the selected folder' }, + ); + } + + body.dashboard.title = title; + } + + if (title) { + body.dashboard.title = title; + } + + if (Object.keys(rest).length) { + if (rest.folderId === '') delete rest.folderId; + Object.assign(body, rest); + } + + responseData = await grafanaApiRequest.call(this, 'POST', '/dashboards/db', body); + + } + + } else if (resource === 'team') { + + // ********************************************************************** + // team + // ********************************************************************** + + if (operation === 'create') { + + // ---------------------------------------- + // team: create + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/team/#add-team + + const body = { + name: this.getNodeParameter('name', i) as string, + }; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, additionalFields); + } + + responseData = await grafanaApiRequest.call(this, 'POST', '/teams', body); + + } else if (operation === 'delete') { + + // ---------------------------------------- + // team: delete + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/team/#delete-team-by-id + + const teamId = this.getNodeParameter('teamId', i); + responseData = await grafanaApiRequest.call(this, 'DELETE', `/teams/${teamId}`); + + } else if (operation === 'get') { + + // ---------------------------------------- + // team: get + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/team/#get-team-by-id + + const teamId = this.getNodeParameter('teamId', i); + responseData = await grafanaApiRequest.call(this, 'GET', `/teams/${teamId}`); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // team: getAll + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/team/#team-search-with-paging + + const qs = {} as IDataObject; + + const filters = this.getNodeParameter('filters', i) as IDataObject; + + if (Object.keys(filters).length) { + Object.assign(qs, filters); + } + + responseData = await grafanaApiRequest.call(this, 'GET', '/teams/search', {}, qs); + responseData = responseData.teams; + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + + } else if (operation === 'update') { + + // ---------------------------------------- + // team: update + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/team/#update-team + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + throwOnEmptyUpdate.call(this, resource, updateFields); + + const body: IDataObject = {}; + + const teamId = this.getNodeParameter('teamId', i); + + // check if team exists, since API does not specify update failure reason + await grafanaApiRequest.call(this, 'GET', `/teams/${teamId}`); + + // prevent email from being overridden to empty + if (!updateFields.email) { + const { email } = await grafanaApiRequest.call(this, 'GET', `/teams/${teamId}`); + body.email = email; + } + + if (Object.keys(updateFields).length) { + Object.assign(body, updateFields); + } + + responseData = await grafanaApiRequest.call(this, 'PUT', `/teams/${teamId}`, body); + + } + + } else if (resource === 'teamMember') { + + // ********************************************************************** + // teamMember + // ********************************************************************** + + if (operation === 'add') { + + // ---------------------------------------- + // teamMember: add + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/team/#add-team-member + + const userId = this.getNodeParameter('userId', i) as string; + + const body = { + userId: parseInt(userId, 10), + }; + + const teamId = this.getNodeParameter('teamId', i); + const endpoint = `/teams/${teamId}/members`; + responseData = await grafanaApiRequest.call(this, 'POST', endpoint, body); + + } else if (operation === 'remove') { + + // ---------------------------------------- + // teamMember: remove + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/team/#remove-member-from-team + + const teamId = this.getNodeParameter('teamId', i); + const memberId = this.getNodeParameter('memberId', i); + const endpoint = `/teams/${teamId}/members/${memberId}`; + responseData = await grafanaApiRequest.call(this, 'DELETE', endpoint); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // teamMember: getAll + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/team/#get-team-members + + const teamId = this.getNodeParameter('teamId', i); + + // check if team exists, since API returns all members if team does not exist + await grafanaApiRequest.call(this, 'GET', `/teams/${teamId}`); + + const endpoint = `/teams/${teamId}/members`; + responseData = await grafanaApiRequest.call(this, 'GET', endpoint); + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + + } + + } else if (resource === 'user') { + + // ********************************************************************** + // user + // ********************************************************************** + + if (operation === 'create') { + + // ---------------------------------------- + // user: create + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/org/#add-a-new-user-to-the-current-organization + + const body = { + role: this.getNodeParameter('role', i), + loginOrEmail: this.getNodeParameter('loginOrEmail', i), + }; + + responseData = await grafanaApiRequest.call(this, 'POST', '/org/users', body); + + } else if (operation === 'delete') { + + // ---------------------------------------- + // user: delete + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/org/#delete-user-in-current-organization + + const userId = this.getNodeParameter('userId', i); + responseData = await grafanaApiRequest.call(this, 'DELETE', `/org/users/${userId}`); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // user: getAll + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/org/#get-all-users-within-the-current-organization + + responseData = await grafanaApiRequest.call(this, 'GET', '/org/users'); + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + responseData = responseData.slice(0, limit); + } + + } else if (operation === 'update') { + + // ---------------------------------------- + // user: update + // ---------------------------------------- + + // https://grafana.com/docs/grafana/latest/http_api/org/#updates-the-given-user + + const body: IDataObject = {}; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + throwOnEmptyUpdate.call(this, resource, updateFields); + + if (Object.keys(updateFields).length) { + Object.assign(body, updateFields); + } + + const userId = this.getNodeParameter('userId', i) as string; + responseData = await grafanaApiRequest.call(this, 'PATCH', `/org/users/${userId}`, body); + + } + + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Grafana/descriptions/DashboardDescription.ts b/packages/nodes-base/nodes/Grafana/descriptions/DashboardDescription.ts new file mode 100644 index 0000000000..7691a63a0e --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/descriptions/DashboardDescription.ts @@ -0,0 +1,271 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const dashboardOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a dashboard', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a dashboard', + }, + { + name: 'Get', + value: 'get', + description: 'Get a dashboard', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all dashboards', + }, + { + name: 'Update', + value: 'update', + description: 'Update a dashboard', + }, + ], + default: 'create', + }, +]; + +export const dashboardFields: INodeProperties[] = [ + { + displayName: 'Title', + name: 'title', + description: 'Title of the dashboard to create', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Folder Name or ID', + name: 'folderId', + type: 'options', + default: '', + description: 'Folder to create the dashboard in - if the folder is unspecified, the dashboard will be saved to the General folder. Choose from the list or specify an ID. You can also specify the ID using an expression.', + typeOptions: { + loadOptionsMethod: 'getFolders', + }, + }, + ], + }, + // ---------------------------------------- + // dashboard: delete + // ---------------------------------------- + { + displayName: 'Dashboard UID or URL', + name: 'dashboardUidOrUrl', + description: 'Unique alphabetic identifier or URL of the dashboard to delete', + placeholder: 'cIBgcSjkk', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // dashboard: get + // ---------------------------------------- + { + displayName: 'Dashboard UID or URL', + name: 'dashboardUidOrUrl', + description: 'Unique alphabetic identifier or URL of the dashboard to retrieve', + placeholder: 'cIBgcSjkk', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------- + // dashboard: getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + maxValue: 100, + }, + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Search Query', + name: 'query', + type: 'string', + default: '', + }, + ], + }, + + // ---------------------------------------- + // dashboard: update + // ---------------------------------------- + { + displayName: 'Dashboard UID or URL', + name: 'dashboardUidOrUrl', + description: 'Unique alphabetic identifier or URL of the dashboard to update', + placeholder: 'cIBgcSjkk', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'dashboard', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Folder Name or ID', + name: 'folderId', + type: 'options', + default: '', + description: 'Folder to move the dashboard into - if the folder is unspecified, the dashboard will be saved to the General folder. Choose from the list or specify an ID using an expression.', + typeOptions: { + loadOptionsMethod: 'getFolders', + }, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'New title of the dashboard', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Grafana/descriptions/TeamDescription.ts b/packages/nodes-base/nodes/Grafana/descriptions/TeamDescription.ts new file mode 100644 index 0000000000..9497ac6c6b --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/descriptions/TeamDescription.ts @@ -0,0 +1,270 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const teamOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'team', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a team', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a team', + }, + { + name: 'Get', + value: 'get', + description: 'Get a team', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all teams', + }, + { + name: 'Update', + value: 'update', + description: 'Update a team', + }, + ], + default: 'create', + }, +]; + +export const teamFields: INodeProperties[] = [ + // ---------------------------------------- + // team: create + // ---------------------------------------- + { + displayName: 'Name', + name: 'name', + description: 'Name of the team to create', + placeholder: 'Engineering', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'engineering@n8n.io', + default: '', + description: 'Email of the team to create', + }, + ], + }, + + // ---------------------------------------- + // team: delete + // ---------------------------------------- + { + displayName: 'Team ID', + name: 'teamId', + description: 'ID of the team to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // team: get + // ---------------------------------------- + { + displayName: 'Team ID', + name: 'teamId', + description: 'ID of the team to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // team: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the team to filter by', + }, + ], + }, + + // ---------------------------------------- + // team: update + // ---------------------------------------- + { + displayName: 'Team ID', + name: 'teamId', + description: 'ID of the team to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'team', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + placeholder: 'engineering@n8n.io', + default: '', + description: 'Email of the team to update', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + placeholder: 'Engineering Team', + default: '', + description: 'Name of the team to update', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Grafana/descriptions/TeamMemberDescription.ts b/packages/nodes-base/nodes/Grafana/descriptions/TeamMemberDescription.ts new file mode 100644 index 0000000000..91ef9fd164 --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/descriptions/TeamMemberDescription.ts @@ -0,0 +1,196 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const teamMemberOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'teamMember', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a member to a team', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all team members', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a member from a team', + }, + ], + default: 'add', + }, +]; + +export const teamMemberFields: INodeProperties[] = [ + // ---------------------------------------- + // teamMember: add + // ---------------------------------------- + { + displayName: 'User Name or ID', + name: 'userId', + description: 'User to add to a team. Choose a name from the list, or specify an ID using an expression.', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + resource: [ + 'teamMember', + ], + operation: [ + 'add', + ], + }, + }, + }, + { + displayName: 'Team Name or ID', + name: 'teamId', + description: 'Team to add the user to. Choose a name from the list, or specify an ID using an expression.', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + displayOptions: { + show: { + resource: [ + 'teamMember', + ], + operation: [ + 'add', + ], + }, + }, + }, + + // ---------------------------------------- + // teamMember: remove + // ---------------------------------------- + { + displayName: 'User Name or ID', + name: 'memberId', + description: 'User to remove from the team. Choose a name from the list, or specify an ID using an expression.', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + resource: [ + 'teamMember', + ], + operation: [ + 'remove', + ], + }, + }, + }, + { + displayName: 'Team Name or ID', + name: 'teamId', + description: 'Team to remove the user from. Choose a name from the list, or specify an ID using an expression.', + type: 'options', + required: true, + default: '', + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + displayOptions: { + show: { + resource: [ + 'teamMember', + ], + operation: [ + 'remove', + ], + }, + }, + }, + + // ---------------------------------------- + // teamMember: getAll + // ---------------------------------------- + { + displayName: 'Team Name or ID', + name: 'teamId', + description: 'Team to retrieve all members from. Choose a name from the list, or specify an ID using an expression.', + typeOptions: { + loadOptionsMethod: 'getTeams', + }, + type: 'options', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'teamMember', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'teamMember', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'teamMember', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Grafana/descriptions/UserDescription.ts b/packages/nodes-base/nodes/Grafana/descriptions/UserDescription.ts new file mode 100644 index 0000000000..390e258eaf --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/descriptions/UserDescription.ts @@ -0,0 +1,167 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete a user from the current organization', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all users in the current organization', + }, + { + name: 'Update', + value: 'update', + description: 'Update a user in the current organization', + }, + ], + default: 'getAll', + }, +]; + +export const userFields: INodeProperties[] = [ + // ---------------------------------------- + // user: update + // ---------------------------------------- + { + displayName: 'User ID', + name: 'userId', + description: 'ID of the user to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Role', + name: 'role', + type: 'options', + default: 'Admin', + description: 'New role for the user', + options: [ + { + name: 'Admin', + value: 'Admin', + }, + { + name: 'Editor', + value: 'Editor', + }, + { + name: 'Viewer', + value: 'Viewer', + }, + ], + }, + ], + }, + + // ---------------------------------------- + // user: delete + // ---------------------------------------- + { + displayName: 'User ID', + name: 'userId', + description: 'ID of the user to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // user: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'Max number of results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, +]; diff --git a/packages/nodes-base/nodes/Grafana/descriptions/index.ts b/packages/nodes-base/nodes/Grafana/descriptions/index.ts new file mode 100644 index 0000000000..392b6e74b0 --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/descriptions/index.ts @@ -0,0 +1,4 @@ +export * from './DashboardDescription'; +export * from './TeamDescription'; +export * from './TeamMemberDescription'; +export * from './UserDescription'; diff --git a/packages/nodes-base/nodes/Grafana/grafana.svg b/packages/nodes-base/nodes/Grafana/grafana.svg new file mode 100644 index 0000000000..c78f110490 --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/grafana.svg @@ -0,0 +1,191 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/nodes-base/nodes/Grafana/types.d.ts b/packages/nodes-base/nodes/Grafana/types.d.ts new file mode 100644 index 0000000000..d506d86f6e --- /dev/null +++ b/packages/nodes-base/nodes/Grafana/types.d.ts @@ -0,0 +1,36 @@ +export type GrafanaCredentials = { + apiKey: string; + baseUrl: string; +}; + +export type DashboardUpdatePayload = { + overwrite: true, + dashboard: { + uid: string, + title?: string + } +}; + +export type DashboardUpdateFields = { + title?: string; + folderId?: string; +}; + +export type LoadedDashboards = Array<{ + id: number; + title: string; +}>; + +export type LoadedFolders = LoadedDashboards; + +export type LoadedTeams = { + teams: Array<{ + id: number; + name: string; + }> +}; + +export type LoadedUsers = Array<{ + userId: number; + email: string; +}>; \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index e1dc5c1b32..2b4a6d574f 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -127,6 +127,7 @@ "dist/credentials/GotifyApi.credentials.js", "dist/credentials/GoToWebinarOAuth2Api.credentials.js", "dist/credentials/GristApi.credentials.js", + "dist/credentials/GrafanaApi.credentials.js", "dist/credentials/GSuiteAdminOAuth2Api.credentials.js", "dist/credentials/GumroadApi.credentials.js", "dist/credentials/HarvestApi.credentials.js", @@ -437,6 +438,7 @@ "dist/nodes/Google/YouTube/YouTube.node.js", "dist/nodes/Gotify/Gotify.node.js", "dist/nodes/GoToWebinar/GoToWebinar.node.js", + "dist/nodes/Grafana/Grafana.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Grist/Grist.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", @@ -749,4 +751,4 @@ "json" ] } -} +} \ No newline at end of file