Skip to content
Merged
2 changes: 2 additions & 0 deletions redisinsight/api/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default {
logs: join(homedir, 'logs'),
defaultPlugins: join(staticDir, 'plugins'),
customPlugins: join(homedir, 'plugins'),
customTutorials: join(homedir, 'custom-tutorials'),
pluginsAssets: join(staticDir, 'resources', 'plugins'),
commands: join(homedir, 'commands'),
defaultCommandsDir: join(defaultsDir, 'commands'),
Expand All @@ -45,6 +46,7 @@ export default {
staticUri: '/static',
guidesUri: '/static/guides',
tutorialsUri: '/static/tutorials',
customTutorialsUri: '/static/custom-tutorials',
contentUri: '/static/content',
defaultPluginsUri: '/static/plugins',
pluginsAssetsUri: '/static/resources/plugins',
Expand Down
2 changes: 2 additions & 0 deletions redisinsight/api/config/ormconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ClientCertificateEntity } from 'src/modules/certificate/entities/client
import { DatabaseEntity } from 'src/modules/database/entities/database.entity';
import { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';
import { BrowserHistoryEntity } from 'src/modules/browser/entities/browser-history.entity';
import { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';
import migrations from '../migration';
import * as config from '../src/utils/config';

Expand All @@ -35,6 +36,7 @@ const ormConfig = {
DatabaseAnalysisEntity,
BrowserHistoryEntity,
SshOptionsEntity,
CustomTutorialEntity,
],
migrations,
};
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/config/production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default {
prevHomedir,
logs: join(homedir, 'logs'),
customPlugins: join(homedir, 'plugins'),
customTutorials: join(homedir, 'custom-tutorials'),
commands: join(homedir, 'commands'),
guides: process.env.GUIDES_DEV_PATH || join(homedir, 'guides'),
tutorials: process.env.TUTORIALS_DEV_PATH || join(homedir, 'tutorials'),
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/config/staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default {
prevHomedir,
logs: join(homedir, 'logs'),
customPlugins: join(homedir, 'plugins'),
customTutorials: join(homedir, 'custom-tutorials'),
commands: join(homedir, 'commands'),
guides: process.env.GUIDES_DEV_PATH || join(homedir, 'guides'),
tutorials: process.env.TUTORIALS_DEV_PATH || join(homedir, 'tutorials'),
Expand Down
14 changes: 14 additions & 0 deletions redisinsight/api/migration/1677135091633-custom-tutorials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class customTutorials1677135091633 implements MigrationInterface {
name = 'customTutorials1677135091633'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "custom_tutorials" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "link" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')))`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "custom_tutorials"`);
}

}
2 changes: 2 additions & 0 deletions redisinsight/api/migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { workbenchAndAnalysisDbIndex1673934231410 } from './1673934231410-workbe
import { browserHistory1674539211397 } from './1674539211397-browser-history';
import { databaseAnalysisRecommendations1674660306971 } from './1674660306971-database-analysis-recommendations';
import { databaseTimeout1675398140189 } from './1675398140189-database-timeout';
import { customTutorials1677135091633 } from './1677135091633-custom-tutorials';

export default [
initialMigration1614164490968,
Expand Down Expand Up @@ -58,4 +59,5 @@ export default [
databaseAnalysisRecommendations1674660306971,
browserHistory1674539211397,
databaseTimeout1675398140189,
customTutorials1677135091633,
];
1 change: 1 addition & 0 deletions redisinsight/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"lodash": "^4.17.20",
"nest-router": "^1.0.9",
"nest-winston": "^1.4.0",
"nestjs-form-data": "^1.8.7",
"node-version-compare": "^1.0.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.5.6",
Expand Down
160 changes: 160 additions & 0 deletions redisinsight/api/src/__mocks__/custom-tutorial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { CustomTutorial, CustomTutorialActions } from 'src/modules/custom-tutorial/models/custom-tutorial';
import { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';
import { CustomTutorialManifestType } from 'src/modules/custom-tutorial/models/custom-tutorial.manifest';
import { MemoryStoredFile } from 'nestjs-form-data';
import { UploadCustomTutorialDto } from 'src/modules/custom-tutorial/dto/upload.custom-tutorial.dto';

export const mockCustomTutorialId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ct-id';

export const mockCustomTutorialId2 = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-ct-id-2';

export const mockCustomTutorialTmpPath = '/tmp/path';

export const mockCustomTutorial = Object.assign(new CustomTutorial(), {
id: mockCustomTutorialId,
name: 'custom tutorial',
createdAt: new Date(),
});

export const mockCustomTutorialEntity = Object.assign(new CustomTutorialEntity(), {
...mockCustomTutorial,
});

export const mockCustomTutorial2 = Object.assign(new CustomTutorial(), {
id: mockCustomTutorialId2,
name: 'custom tutorial 2',
createdAt: new Date(),
});

export const mockCustomTutorialZipFile = Object.assign(new MemoryStoredFile(), {
size: 100,
buffer: Buffer.from('zip-content', 'utf8'),
});

export const mockUploadCustomTutorialDto = Object.assign(new UploadCustomTutorialDto(), {
name: mockCustomTutorial.name,
file: mockCustomTutorialZipFile,
});

export const mockCustomTutorialManifestManifestJson = {
'ct-folder-1': {
type: 'group',
id: 'ct-folder-1',
label: 'ct-folder-1',
// args: {
// withBorder: true,
// initialIsOpen: true,
// },
children: {
'ct-sub-folder-1': {
type: CustomTutorialManifestType.Group,
id: 'ct-sub-folder-1',
label: 'ct-sub-folder-1',
// args: {
// initialIsOpen: false,
// },
children: {
introduction: {
type: CustomTutorialManifestType.InternalLink,
id: 'introduction',
label: 'introduction',
args: {
path: '/ct-folder-1/ct-sub-folder-1/introduction.md',
},
},
'working-with-hashes': {
type: CustomTutorialManifestType.InternalLink,
id: 'working-with-hashes',
label: 'working-with-hashes',
args: {
path: '/ct-folder-1/ct-sub-folder-1/working-with-hashes.md',
},
},
},
},
'ct-sub-folder-2': {
type: CustomTutorialManifestType.Group,
id: 'ct-sub-folder-2',
label: 'ct-sub-folder-2',
// args: {
// withBorder: true,
// initialIsOpen: false,
// },
children: {
introduction: {
type: CustomTutorialManifestType.InternalLink,
id: 'introduction',
label: 'introduction',
args: {
path: '/ct-folder-1/ct-sub-folder-2/introduction.md',
},
},
'working-with-graphs': {
type: CustomTutorialManifestType.InternalLink,
id: 'working-with-graphs',
label: 'working-with-graphs',
args: {
path: '/ct-folder-1/ct-sub-folder-2/working-with-graphs.md',
},
},
},
},
},
},
};

export const mockCustomTutorialManifestManifest = {
type: CustomTutorialManifestType.Group,
id: mockCustomTutorialId,
label: mockCustomTutorial.name,
_actions: mockCustomTutorial.actions,
_path: mockCustomTutorial.path,
children: mockCustomTutorialManifestManifestJson,
};

export const mockCustomTutorialManifestManifest2 = {
type: CustomTutorialManifestType.Group,
id: mockCustomTutorialId2,
label: mockCustomTutorial2.name,
_actions: mockCustomTutorial2.actions,
_path: mockCustomTutorial2.path,
children: mockCustomTutorialManifestManifestJson,
};

export const globalCustomTutorialManifest = {
'custom-tutorials': {
type: CustomTutorialManifestType.Group,
id: 'custom-tutorials',
label: 'My Tutorials',
_actions: [CustomTutorialActions.CREATE],
args: {
withBorder: true,
initialIsOpen: true,
},
children: {
[mockCustomTutorialManifestManifest.id]: mockCustomTutorialManifestManifest,
[mockCustomTutorialManifestManifest2.id]: mockCustomTutorialManifestManifest2,
},
},
};

export const mockCustomTutorialFsProvider = jest.fn(() => ({
unzipToTmpFolder: jest.fn().mockResolvedValue(mockCustomTutorialTmpPath),
moveFolder: jest.fn(),
removeFolder: jest.fn(),
}));

export const mockCustomTutorialManifestProvider = jest.fn(() => ({
getManifestJson: jest.fn().mockResolvedValue(mockCustomTutorialManifestManifestJson),
generateTutorialManifest: jest.fn().mockResolvedValue(mockCustomTutorialManifestManifest),
}));

export const mockCustomTutorialRepository = jest.fn(() => ({
get: jest.fn().mockResolvedValue(mockCustomTutorial),
create: jest.fn().mockResolvedValue(mockCustomTutorial),
delete: jest.fn(),
list: jest.fn().mockResolvedValue([
mockCustomTutorial,
mockCustomTutorial2,
]),
}));
1 change: 1 addition & 0 deletions redisinsight/api/src/__mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './analytics';
export * from './profiler';
export * from './user';
export * from './databases';
export * from './custom-tutorial';
export * from './autodiscovery';
export * from './redis';
export * from './server';
Expand Down
2 changes: 2 additions & 0 deletions redisinsight/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CoreModule } from 'src/core.module';
import { AutodiscoveryModule } from 'src/modules/autodiscovery/autodiscovery.module';
import { DatabaseImportModule } from 'src/modules/database-import/database-import.module';
import { DummyAuthMiddleware } from 'src/common/middlewares/dummy-auth.middleware';
import { CustomTutorialModule } from 'src/modules/custom-tutorial/custom-tutorial.module';
import { BrowserModule } from './modules/browser/browser.module';
import { RedisEnterpriseModule } from './modules/redis-enterprise/redis-enterprise.module';
import { RedisSentinelModule } from './modules/redis-sentinel/redis-sentinel.module';
Expand Down Expand Up @@ -53,6 +54,7 @@ const PATH_CONFIG = config.get('dir_path');
NotificationModule,
BulkActionsModule,
ClusterMonitorModule,
CustomTutorialModule.register(),
DatabaseAnalysisModule,
DatabaseImportModule,
...(SERVER_CONFIG.staticContent
Expand Down
9 changes: 9 additions & 0 deletions redisinsight/api/src/common/utils/errors.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { HttpException, InternalServerErrorException } from '@nestjs/common';

export const wrapHttpError = (error: Error) => {
if (error instanceof HttpException) {
return error;
}

return new InternalServerErrorException(error.message);
};
1 change: 1 addition & 0 deletions redisinsight/api/src/common/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './certificate-import.util';
export * from './errors.util';
1 change: 1 addition & 0 deletions redisinsight/api/src/constants/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default {
PROFILER_LOG_FILE_NOT_FOUND: 'Profiler log file was not found.',
CONSUMER_GROUP_NOT_FOUND: 'Consumer Group with such name was not found.',
PLUGIN_STATE_NOT_FOUND: 'Plugin state was not found.',
CUSTOM_TUTORIAL_NOT_FOUND: 'Custom Tutorial was not found.',
UNDEFINED_INSTANCE_ID: 'Undefined redis database instance id.',
NO_CONNECTION_TO_REDIS_DB: 'No connection to the Redis Database.',
WRONG_DATABASE_TYPE: 'Wrong database type.',
Expand Down
3 changes: 3 additions & 0 deletions redisinsight/api/src/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
import { RedisModule } from 'src/modules/redis/redis.module';
import { AnalyticsModule } from 'src/modules/analytics/analytics.module';
import { SshModule } from 'src/modules/ssh/ssh.module';
import { NestjsFormDataModule } from 'nestjs-form-data';

@Global()
@Module({
Expand All @@ -19,6 +20,7 @@ import { SshModule } from 'src/modules/ssh/ssh.module';
DatabaseModule.register(),
RedisModule,
SshModule,
NestjsFormDataModule,
],
exports: [
EncryptionModule,
Expand All @@ -27,6 +29,7 @@ import { SshModule } from 'src/modules/ssh/ssh.module';
DatabaseModule,
RedisModule,
SshModule,
NestjsFormDataModule,
],
})
export class CoreModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
Body,
ClassSerializerInterceptor,
Controller, Delete, Get, HttpCode, Param, Post,
UseInterceptors, UsePipes, ValidationPipe,
} from '@nestjs/common';
import {
ApiConsumes, ApiExtraModels, ApiTags,
} from '@nestjs/swagger';
import { CustomTutorialService } from 'src/modules/custom-tutorial/custom-tutorial.service';
import { UploadCustomTutorialDto } from 'src/modules/custom-tutorial/dto/upload.custom-tutorial.dto';
import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator';
import { FormDataRequest } from 'nestjs-form-data';
import { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto';
import { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto';
import { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto';
import { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto';
import { CreateBasicSshOptionsDto } from 'src/modules/ssh/dto/create.basic-ssh-options.dto';
import { CreateCertSshOptionsDto } from 'src/modules/ssh/dto/create.cert-ssh-options.dto';

@ApiExtraModels(
CreateCaCertificateDto, UseCaCertificateDto,
CreateClientCertificateDto, UseClientCertificateDto,
CreateBasicSshOptionsDto, CreateCertSshOptionsDto,
)
@UsePipes(new ValidationPipe({ transform: true }))
@UseInterceptors(ClassSerializerInterceptor)
@ApiTags('Tutorials')
@Controller('/custom-tutorials')
export class CustomTutorialController {
constructor(private readonly service: CustomTutorialService) {}

@Post('')
@HttpCode(201)
@ApiConsumes('multipart/form-data')
@FormDataRequest()
@ApiEndpoint({
description: 'Create new tutorial',
statusCode: 201,
responses: [
{
type: Object,
},
],
})
async create(
@Body() dto: UploadCustomTutorialDto,
): Promise<Record<string, any>> {
return this.service.create(dto);
}

@Get('manifest')
@ApiEndpoint({
description: 'Get global manifest for custom tutorials',
statusCode: 200,
responses: [
{
type: Object,
},
],
})
async getGlobalManifest(): Promise<any> {
return this.service.getGlobalManifest();
}

@Delete('/:id')
@ApiEndpoint({
statusCode: 200,
description: 'Delete custom tutorial and its files',
})
async delete(
@Param('id') id: string,
): Promise<void> {
return this.service.delete(id);
}
}
Loading